From 2d744ffc64cf683e811c4b3b956fb5757f3cf6cb Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:21:55 +0200 Subject: [PATCH] [Lens] Select line chart by default if the x-axis contains a timestamp (#190786) ## Summary Partially resolves https://github.com/elastic/kibana/issues/184102 When dropping a field to a workspace that will create the date histogram visualization, it converts to line. It doesn't do it when dropping to a dimension as it's more complicated thing. The reason for it is that we would have to: - decide if we also want to switch to line when choosing a field from the config panel - allow for an extra behavior (switching visualization types) for the onDrop callback except for only modifying dimensions. ## Release notes When dropping a field into the Lens workspace, the default time-series visualization is a line chart instead of a histogram. --- .../lens/public/visualizations/xy/types.ts | 2 + .../visualizations/xy/visualization.tsx | 2 +- .../xy/visualization_helpers.tsx | 1 - .../visualizations/xy/xy_suggestions.test.ts | 35 ++++++++++++++++ .../visualizations/xy/xy_suggestions.ts | 42 +++++++++---------- .../apps/lens/group1/multiple_data_views.ts | 34 +++++++++------ 6 files changed, 80 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 694799b94638e..ee6575bfe0bbd 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -59,6 +59,8 @@ export const SeriesTypes = { BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', } as const; +export const defaultSeriesType = SeriesTypes.BAR_STACKED; + export type YAxisMode = $Values; export type SeriesType = $Values; export interface AxesSettingsConfig { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 6f17a2253a35e..51f79bf58eeac 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -89,7 +89,6 @@ import { } from './annotations/helpers'; import { checkXAccessorCompatibility, - defaultSeriesType, getAnnotationLayerTitle, getAnnotationsLayers, getAxisName, @@ -112,6 +111,7 @@ import { } from './visualization_helpers'; import { getAxesConfiguration, groupAxesByType } from './axes_configuration'; import type { XYByValueAnnotationLayerConfig, XYState } from './types'; +import { defaultSeriesType } from './types'; import { defaultAnnotationLabel } from './annotations/helpers'; import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; import { createAnnotationActions } from './annotations/actions'; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx index 970671128aecf..09f6c0fe18cfd 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx @@ -270,7 +270,6 @@ export function getDescription(state?: State, layerId?: string) { } export const defaultIcon = IconChartBarStacked; -export const defaultSeriesType = 'bar_stacked'; export const supportedDataLayer = { type: layerTypes.DATA, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts index c81dea60da719..7ce86ed903065 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts @@ -854,6 +854,41 @@ describe('xy_suggestions', () => { expect((suggestions[0].state.layers[0] as XYDataLayerConfig).seriesType).toEqual('line'); }); + test('suggests line if changeType is initial and date column is involved', () => { + const currentState: XYState = { + legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', + fittingFunction: 'None', + preferredSeriesType: 'bar_stacked', + layers: [ + { + accessors: [], + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'bar_stacked', + splitAccessor: undefined, + xAccessor: '', + }, + ], + }; + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('price'), dateCol('date')], + layerId: 'first', + changeType: 'initial', + }, + state: currentState, + keptLayerIds: ['first'], + }); + + expect(suggestions).toHaveLength(1); + + expect(suggestions[0].hide).toEqual(false); + expect(suggestions[0].state.preferredSeriesType).toEqual('line'); + expect((suggestions[0].state.layers[0] as XYDataLayerConfig).seriesType).toEqual('line'); + }); + test('makes a visible seriesType suggestion for unchanged table without split', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts index 49531c6b563be..5efaf4d8c949e 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts @@ -25,6 +25,7 @@ import { XYLayerConfig, XYDataLayerConfig, SeriesType, + defaultSeriesType, } from './types'; import { flipSeriesType, getIconForSeries } from './state_helpers'; import { getDataLayers, isDataLayer } from './visualization_helpers'; @@ -101,21 +102,24 @@ function getSuggestionForColumns( allowMixed?: boolean ): VisualizationSuggestion | Array> | undefined { const [buckets, values] = partition(table.columns, (col) => col.operation.isBucketed); + const sharedArgs = { + layerId: table.layerId, + changeType: table.changeType, + currentState, + tableLabel: table.label, + keptLayerIds, + requestedSeriesType: seriesType, + mainPalette, + allowMixed, + }; if (buckets.length === 1 || buckets.length === 2) { - const [x, splitBy] = getBucketMappings(table, currentState); + const [xValue, splitBy] = getBucketMappings(table, currentState); return getSuggestionsForLayer({ - layerId: table.layerId, - changeType: table.changeType, - xValue: x, + ...sharedArgs, + xValue, yValues: values, splitBy, - currentState, - tableLabel: table.label, - keptLayerIds, - requestedSeriesType: seriesType, - mainPalette, - allowMixed, }); } else if (buckets.length === 0) { const [yValues, [xValue, splitBy]] = partition( @@ -123,17 +127,10 @@ function getSuggestionForColumns( (col) => col.operation.dataType === 'number' && !col.operation.isBucketed ); return getSuggestionsForLayer({ - layerId: table.layerId, - changeType: table.changeType, + ...sharedArgs, xValue, yValues, splitBy, - currentState, - tableLabel: table.label, - keptLayerIds, - requestedSeriesType: seriesType, - mainPalette, - allowMixed, }); } } @@ -236,6 +233,9 @@ function getSuggestionsForLayer({ allowMixed, }; + if (changeType === 'initial' && xValue?.operation.dataType === 'date') { + return buildSuggestion({ ...options, seriesType: 'line' }); + } // handles the simplest cases, acting as a chart switcher if (!currentState && changeType === 'unchanged') { // Chart switcher needs to include every chart type @@ -434,18 +434,16 @@ function getSeriesType( layerId: string, xValue?: TableSuggestionColumn ): SeriesType { - const defaultType = 'bar_stacked'; - const oldLayer = getExistingLayer(currentState, layerId); const oldLayerSeriesType = oldLayer && isDataLayer(oldLayer) ? oldLayer.seriesType : false; const closestSeriesType = - oldLayerSeriesType || (currentState && currentState.preferredSeriesType) || defaultType; + oldLayerSeriesType || (currentState && currentState.preferredSeriesType) || defaultSeriesType; // Attempt to keep the seriesType consistent on initial add of a layer // Ordinal scales should always use a bar because there is no interpolation between buckets if (xValue && xValue.operation.scale && xValue.operation.scale === 'ordinal') { - return closestSeriesType.startsWith('bar') ? closestSeriesType : defaultType; + return closestSeriesType.startsWith('bar') ? closestSeriesType : defaultSeriesType; } return closestSeriesType; diff --git a/x-pack/test/functional/apps/lens/group1/multiple_data_views.ts b/x-pack/test/functional/apps/lens/group1/multiple_data_views.ts index 84bf66c26191d..d65b10d0056e1 100644 --- a/x-pack/test/functional/apps/lens/group1/multiple_data_views.ts +++ b/x-pack/test/functional/apps/lens/group1/multiple_data_views.ts @@ -33,16 +33,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { function assertMatchesExpectedData( state: DebugState, - expectedData: Array> + expectedData: Array>, + chartType: 'bars' | 'lines' = 'bars' ) { - expect( - state?.bars?.map(({ bars }) => - bars.map((bar) => ({ - x: bar.x, - y: Math.floor(bar.y * 100) / 100, - })) - ) - ).to.eql(expectedData); + if (chartType === 'lines') { + expect( + state?.lines + ?.map(({ points }) => + points + .map((point) => ({ x: point.x, y: Math.floor(point.y * 100) / 100 })) + .sort(({ x }, { x: x2 }) => x - x2) + ) + .filter((a) => a.length > 0) + ).to.eql(expectedData); + } else { + expect( + state?.bars?.map(({ bars }) => + bars.map((point) => ({ x: point.x, y: Math.floor(point.y * 100) / 100 })) + ) + ).to.eql(expectedData); + } } describe('lens with multiple data views', () => { @@ -93,13 +103,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('fieldToggle-DistanceKilometers'); const data = await lens.getCurrentChartDebugState('xyVisChart'); - assertMatchesExpectedData(data, [expectedLogstashData, expectedFlightsData]); + assertMatchesExpectedData(data, [expectedLogstashData, expectedFlightsData], 'lines'); }); it('ignores global filters on layers using a data view without the filter field', async () => { await filterBar.addFilter({ field: 'Carrier', operation: 'exists' }); const data = await lens.getCurrentChartDebugState('xyVisChart'); - assertMatchesExpectedData(data, [expectedLogstashData, expectedFlightsData]); + assertMatchesExpectedData(data, [expectedLogstashData, expectedFlightsData], 'lines'); await lens.save(visTitle); }); @@ -110,7 +120,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualize.openSavedVisualization(visTitle); const data = await lens.getCurrentChartDebugState('xyVisChart'); - assertMatchesExpectedData(data, [expectedFlightsData]); + assertMatchesExpectedData(data, [expectedFlightsData], 'lines'); }); }); }