From 3fe1eab5a5d588c2be7af3359bef8fd5881c43d3 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 6 Oct 2021 15:39:44 +0200 Subject: [PATCH] [Lens] Thresholds: auto fit thresholds into vertical axis (#113238) * :sparkles: Make threshold fit into view automatically * :bug: do not compute axis threshold extends if no threshold is present * :white_check_mark: One more fix for 0-based extends and tests * :memo: fix typo Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../xy_visualization/expression.test.tsx | 89 +++++++++++++++++++ .../public/xy_visualization/expression.tsx | 40 ++++++++- 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 77364ac72a046..4056aa730c2ab 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -330,6 +330,48 @@ function sampleArgs() { return { data, args }; } +function sampleArgsWithThreshold(thresholdValue: number = 150) { + const { data, args } = sampleArgs(); + + return { + data: { + ...data, + tables: { + ...data.tables, + threshold: { + type: 'datatable', + columns: [ + { + id: 'threshold-a', + meta: { params: { id: 'number' }, type: 'number' }, + name: 'Static value', + }, + ], + rows: [{ 'threshold-a': thresholdValue }], + }, + }, + } as LensMultiTable, + args: { + ...args, + layers: [ + ...args.layers, + { + layerType: layerTypes.THRESHOLD, + accessors: ['threshold-a'], + layerId: 'threshold', + seriesType: 'line', + xScaleType: 'linear', + yScaleType: 'linear', + palette: mockPaletteOutput, + isHistogram: false, + hide: true, + yConfig: [{ axisMode: 'left', forAccessor: 'threshold-a', type: 'lens_xy_yConfig' }], + }, + ], + } as XYArgs, + }; +} + describe('xy_expression', () => { describe('configs', () => { test('legendConfig produces the correct arguments', () => { @@ -829,6 +871,53 @@ describe('xy_expression', () => { max: undefined, }); }); + + test('it does include threshold values when in full extent mode', () => { + const { data, args } = sampleArgsWithThreshold(); + + const component = shallow(); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 0, + max: 150, + }); + }); + + test('it should ignore threshold values when set to custom extents', () => { + const { data, args } = sampleArgsWithThreshold(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 123, + max: 456, + }); + }); + + test('it should work for negative values in thresholds', () => { + const { data, args } = sampleArgsWithThreshold(-150); + + const component = shallow(); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: -150, + max: 5, + }); + }); }); test('it has xDomain undefined if the x is not a time scale or a histogram', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 0cea52b5d3c9e..484032e5ffbd9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -117,6 +117,8 @@ export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { return intervalDuration.as('milliseconds'); } +const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; + export const getXyChartRenderer = (dependencies: { formatFactory: FormatFactory; chartsThemeService: ChartsPluginStart['theme']; @@ -395,6 +397,41 @@ export function XYChart({ min = extent.lowerBound; max = extent.upperBound; } + } else { + const axisHasThreshold = thresholdLayers.some(({ yConfig }) => + yConfig?.some(({ axisMode }) => axisMode === axis.groupId) + ); + if (!fit && axisHasThreshold) { + // Remove this once the chart will support automatic annotation fit for other type of charts + for (const series of axis.series) { + const table = data.tables[series.layer]; + for (const row of table.rows) { + for (const column of table.columns) { + if (column.id === series.accessor) { + const value = row[column.id]; + if (typeof value === 'number') { + // keep the 0 in view + max = Math.max(value, max || 0, 0); + min = Math.min(value, min || 0, 0); + } + } + } + } + } + for (const { layerId, yConfig } of thresholdLayers) { + const table = data.tables[layerId]; + for (const { axisMode, forAccessor } of yConfig || []) { + if (axis.groupId === axisMode) { + for (const row of table.rows) { + const value = row[forAccessor]; + // keep the 0 in view + max = Math.max(value, max || 0, 0); + min = Math.min(value, min || 0, 0); + } + } + } + } + } } return { @@ -652,9 +689,6 @@ export function XYChart({ const table = data.tables[layerId]; - const isPrimitive = (value: unknown): boolean => - value != null && typeof value !== 'object'; - // what if row values are not primitive? That is the case of, for instance, Ranges // remaps them to their serialized version with the formatHint metadata // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on