b87852071b
* [Lens] fix passing 0 as static value * allow computed static_value to be passed * Update x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx Co-authored-by: Marco Liberati <dej611@users.noreply.github.com> * ci fix Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
624 lines
19 KiB
TypeScript
624 lines
19 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
* 2.0.
|
|
*/
|
|
|
|
import { XYLayerConfig } from '../../common/expressions';
|
|
import { FramePublicAPI } from '../types';
|
|
import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers';
|
|
|
|
function getActiveData(json: Array<{ id: string; rows: Array<Record<string, number | null>> }>) {
|
|
return json.reduce((memo, { id, rows }) => {
|
|
const columns = Object.keys(rows[0]).map((columnId) => ({
|
|
id: columnId,
|
|
name: columnId,
|
|
meta: { type: 'number' as const },
|
|
}));
|
|
memo[id] = {
|
|
type: 'datatable' as const,
|
|
columns,
|
|
rows,
|
|
};
|
|
return memo;
|
|
}, {} as NonNullable<FramePublicAPI['activeData']>);
|
|
}
|
|
|
|
describe('reference_line helpers', () => {
|
|
describe('getStaticValue', () => {
|
|
const hasDateHistogram = () => false;
|
|
const hasAllNumberHistogram = () => true;
|
|
|
|
it('should return fallback value on missing data', () => {
|
|
expect(getStaticValue([], 'x', {}, hasAllNumberHistogram)).toBe(100);
|
|
});
|
|
|
|
it('should return fallback value on no-configuration/missing hit on current data', () => {
|
|
// no-config: missing layer
|
|
expect(
|
|
getStaticValue(
|
|
[],
|
|
'x',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(100);
|
|
// accessor id has no hit in data
|
|
expect(
|
|
getStaticValue(
|
|
[{ layerId: 'id-a', seriesType: 'area' } as XYLayerConfig], // missing xAccessor for groupId == x
|
|
'x',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(100);
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['d'],
|
|
} as XYLayerConfig,
|
|
], // missing hit of accessor "d" in data
|
|
'yLeft',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(100);
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a'],
|
|
} as XYLayerConfig,
|
|
], // missing yConfig fallbacks to left axis, but the requested group is yRight
|
|
'yRight',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(100);
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a'],
|
|
} as XYLayerConfig,
|
|
], // same as above with x groupId
|
|
'x',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(100);
|
|
});
|
|
|
|
it('should return 0 as result of calculation', () => {
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a'],
|
|
yConfig: [{ forAccessor: 'a', axisMode: 'right' }],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yRight',
|
|
{
|
|
activeData: getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: [{ a: -30 }, { a: 10 }],
|
|
},
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(0);
|
|
});
|
|
|
|
it('should work for no yConfig defined and fallback to left axis', () => {
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a'],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yLeft',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(75); // 3/4 of "a" only
|
|
});
|
|
|
|
it('should extract axis side from yConfig', () => {
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a'],
|
|
yConfig: [{ forAccessor: 'a', axisMode: 'right' }],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yRight',
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(75); // 3/4 of "a" only
|
|
});
|
|
|
|
it('should correctly distribute axis on left and right with different formatters when in auto', () => {
|
|
const tables = getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) },
|
|
]);
|
|
tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter
|
|
tables['id-a'].columns[1].meta.params = { id: 'percent' }; // b: percent formatter
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a', 'b'],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yLeft',
|
|
{ activeData: tables },
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(75); // 3/4 of "a" only
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a', 'b'],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yRight',
|
|
{ activeData: tables },
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(150); // 3/4 of "b" only
|
|
});
|
|
|
|
it('should ignore hasHistogram for left or right axis', () => {
|
|
const tables = getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) },
|
|
]);
|
|
tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter
|
|
tables['id-a'].columns[1].meta.params = { id: 'percent' }; // b: percent formatter
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a', 'b'],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yLeft',
|
|
{ activeData: tables },
|
|
hasDateHistogram
|
|
)
|
|
).toBe(75); // 3/4 of "a" only
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
accessors: ['a', 'b'],
|
|
} as XYLayerConfig,
|
|
],
|
|
'yRight',
|
|
{ activeData: tables },
|
|
hasDateHistogram
|
|
)
|
|
).toBe(150); // 3/4 of "b" only
|
|
});
|
|
|
|
it('should early exit for x group if a date histogram is detected', () => {
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
xAccessor: 'a',
|
|
accessors: [],
|
|
} as XYLayerConfig,
|
|
],
|
|
'x', // this is influenced by the callback
|
|
{
|
|
activeData: getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
]),
|
|
},
|
|
hasDateHistogram
|
|
)
|
|
).toBe(100);
|
|
});
|
|
|
|
it('should not force zero-based interval for x group', () => {
|
|
expect(
|
|
getStaticValue(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area',
|
|
layerType: 'data',
|
|
xAccessor: 'a',
|
|
accessors: [],
|
|
} as XYLayerConfig,
|
|
],
|
|
'x',
|
|
{
|
|
activeData: getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ a: i % 2 ? 33 : 50 })),
|
|
},
|
|
]),
|
|
},
|
|
hasAllNumberHistogram
|
|
)
|
|
).toBe(45.75); // 33 (min) + (50 - 33) * 3/4
|
|
});
|
|
});
|
|
|
|
describe('computeOverallDataDomain', () => {
|
|
it('should compute the correct value for a single layer with stacked series', () => {
|
|
for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked'])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYLayerConfig],
|
|
['a', 'b', 'c'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
a: i === 0 ? 25 : null,
|
|
b: i === 1 ? 50 : null,
|
|
c: i === 2 ? 75 : null,
|
|
})),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 150 }); // there's just one series with 150, so the lowerbound fallbacks to 0
|
|
});
|
|
|
|
it('should work for percentage series', () => {
|
|
for (const seriesType of [
|
|
'bar_percentage_stacked',
|
|
'bar_horizontal_percentage_stacked',
|
|
'area_percentage_stacked',
|
|
])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYLayerConfig],
|
|
['a', 'b', 'c'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
a: i === 0 ? 0.25 : null,
|
|
b: i === 1 ? 0.25 : null,
|
|
c: i === 2 ? 0.25 : null,
|
|
})),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 0.75 });
|
|
});
|
|
|
|
it('should compute the correct value for multiple layers with stacked series', () => {
|
|
for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) {
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'c', 'd', 'e', 'f'],
|
|
getActiveData([
|
|
{ id: 'id-a', rows: [{ a: 25, b: 100, c: 100 }] },
|
|
{ id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] },
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 375 });
|
|
// same as before but spread on 3 rows with nulls
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'c', 'd', 'e', 'f'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
a: i === 0 ? 25 : null,
|
|
b: i === 1 ? 100 : null,
|
|
c: i === 2 ? 100 : null,
|
|
})),
|
|
},
|
|
{
|
|
id: 'id-b',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
d: i === 0 ? 50 : null,
|
|
e: i === 1 ? 50 : null,
|
|
f: i === 2 ? 50 : null,
|
|
})),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 375 });
|
|
}
|
|
});
|
|
|
|
it('should compute the correct value for multiple layers with non-stacked series', () => {
|
|
for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area'])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'c', 'd', 'e', 'f'],
|
|
getActiveData([
|
|
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
|
{ id: 'id-b', rows: Array(3).fill({ d: 50, e: 50, f: 50 }) },
|
|
])
|
|
)
|
|
).toEqual({ min: 50, max: 100 });
|
|
});
|
|
|
|
it('should compute the correct value for mixed series (stacked + non-stacked)', () => {
|
|
for (const nonStackedSeries of ['bar', 'bar_horizontal', 'line', 'area']) {
|
|
for (const stackedSeries of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) {
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType: nonStackedSeries, accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType: stackedSeries, accessors: ['d', 'e', 'f'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'c', 'd', 'e', 'f'],
|
|
getActiveData([
|
|
{ id: 'id-a', rows: [{ a: 100, b: 100, c: 100 }] },
|
|
{ id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] },
|
|
])
|
|
)
|
|
).toEqual({
|
|
min: 0, // min is 0 as there is at least one stacked series
|
|
max: 150, // max is id-b layer accessor sum
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should compute the correct value for a histogram stacked chart', () => {
|
|
for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked'])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, xAccessor: 'c', accessors: ['a', 'b'] },
|
|
{ layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'd', 'e'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })),
|
|
},
|
|
{
|
|
id: 'id-b',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 375 });
|
|
});
|
|
|
|
it('should compute the correct value for a histogram on stacked chart for the xAccessor', () => {
|
|
for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked'])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, accessors: ['c'] },
|
|
{ layerId: 'id-b', seriesType, accessors: ['f'] },
|
|
] as XYLayerConfig[],
|
|
['c', 'f'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })),
|
|
},
|
|
{
|
|
id: 'id-b',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })),
|
|
},
|
|
]),
|
|
false // this will avoid the stacking behaviour
|
|
)
|
|
).toEqual({ min: 0, max: 2 });
|
|
});
|
|
|
|
it('should compute the correct value for a histogram non-stacked chart', () => {
|
|
for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area'])
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType, xAccessor: 'c', accessors: ['a', 'b'] },
|
|
{ layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b', 'd', 'e'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })),
|
|
},
|
|
{
|
|
id: 'id-b',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 200 });
|
|
});
|
|
|
|
it('should compute the result taking into consideration negative-based intervals too', () => {
|
|
// stacked
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{
|
|
layerId: 'id-a',
|
|
seriesType: 'area_stacked',
|
|
accessors: ['a', 'b', 'c'],
|
|
} as XYLayerConfig,
|
|
],
|
|
['a', 'b', 'c'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
a: i === 0 ? -100 : null,
|
|
b: i === 1 ? 200 : null,
|
|
c: i === 2 ? 100 : null,
|
|
})),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: 0, max: 200 }); // it is stacked, so max is the sum and 0 is the fallback
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[{ layerId: 'id-a', seriesType: 'area', accessors: ['a', 'b', 'c'] } as XYLayerConfig],
|
|
['a', 'b', 'c'],
|
|
getActiveData([
|
|
{
|
|
id: 'id-a',
|
|
rows: Array(3)
|
|
.fill(1)
|
|
.map((_, i) => ({
|
|
a: i === 0 ? -100 : null,
|
|
b: i === 1 ? 200 : null,
|
|
c: i === 2 ? 100 : null,
|
|
})),
|
|
},
|
|
])
|
|
)
|
|
).toEqual({ min: -100, max: 200 });
|
|
});
|
|
|
|
it('should return no result if no layers or accessors are passed', () => {
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[],
|
|
['a', 'b', 'c'],
|
|
getActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }])
|
|
)
|
|
).toEqual({ min: undefined, max: undefined });
|
|
});
|
|
|
|
it('should return no result if data or table is not available', () => {
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType: 'area', accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType: 'line', accessors: ['d', 'e', 'f'] },
|
|
] as XYLayerConfig[],
|
|
['a', 'b'],
|
|
getActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here
|
|
)
|
|
).toEqual({ min: undefined, max: undefined });
|
|
|
|
expect(
|
|
computeOverallDataDomain(
|
|
[
|
|
{ layerId: 'id-a', seriesType: 'bar', accessors: ['a', 'b', 'c'] },
|
|
{ layerId: 'id-b', seriesType: 'bar_stacked' },
|
|
] as XYLayerConfig[],
|
|
['a', 'b'],
|
|
getActiveData([])
|
|
)
|
|
).toEqual({ min: undefined, max: undefined });
|
|
});
|
|
});
|
|
});
|