From f42fce3bc732a8b655f3c5ad994a2c871cae0371 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 12 Jul 2022 01:42:44 -0700 Subject: [PATCH] [XY] Allow multiple split accessors (#134566) * Fixed types and imports * Fixed handlers.inspectorAdapters.tables.logDatatable * Fixed logDatatable * Translations fixed. * Fixed "Visualize App ... cleans filters and query" test. cleans filters and query * Fixed "lens disable auto-apply tests" test. * Updated dashboard tests. * Fixed translations. * Expression tests fixed. * Cleaned up expression_xy. * cleaned up lens xy_visualization. * fixed more tests. * Fix of tsvb. * Fixed more tests. * Fixed xy chart limits. * Fixed new tests. * Fixed types. * Added extended layers expressions. * Added support of tables at layers. * Fixed tests. * Fixed more tests. * Fixed lens types. * Added tables to layers. * Checks fixed. * updated tests. * Fixed types. * First try to fix merge conflicts. * Fixed annotatations. * Fixed types. * Updated snapshots * Fixed tests. * Fixed dependencies. * Fixed i18n. * Moved XY state types to lens. * Fixed more types. * Update src/plugins/chart_expressions/expression_xy/README.md Co-authored-by: Marta Bondyra * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * Removed yConfig from *Layers types * Fixed styles. * Fixed types. * Removed not used utils and styles. * Fixed types and tests. * updated size. * Added right behavior, related to the tables, comming from the expression. * Fixed reference lines. * Fixed jsdoc. * Added annotations to layeredXyVIs. * Fixed limits. * Refactored the implementation to be reusable. * Fixed undefined layers. * Fixed empty arrays problems. * Fixed input translations and removed not used arguments. * Add 'axis' arg * Fixed missing required args error, and added required to arguments. * Simplified expression configuration. * Added strict to all the expressions. * refactored code, according to the nit. * Some fixes * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix CI * Fix checks * Fix some lint errors * Some fixes * Moved dataLayer to the separate component. * Fixed jest tests. * Fixed tests. * Refactored dataLayers helpers and xy_chart. * Some refacroting * Fix types * More fixes of the expression Added extendedYConfig for dataLayers. Added yConfig for referenceLineLayers. Fixed undefined id at tooltip. * Some fixes * Fixed tests and snapshots. * Icons at annotations and reference lines are strict. * Fix CI * axis extent validation added. * Added checks to the legend config. * fillOpacity usage validation is added. * Fixed valueLabels argument options. Removed not used. Added validation for usage. * Removed not used tests and imports. * Fixed valueLabels and added migrations. * Fixed type checks. * Added test for the migrations. * Fixed imports. * Fixed types * Fixed i18n checks. # Conflicts: # src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx * Some updates * Some refactoring * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fixed imports and types. * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix tests and types * Fix checks * Remove unneeded imports * Fix imports * Fix imports * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Update src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts Co-authored-by: Marta Bondyra * Removed extra extends. * Update src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts Co-authored-by: Marta Bondyra * Added guard. * Fixed the code duplication. * Removed table from the annotation layer. * Changed the `convertActiveDataFromIndexesToLayers` location. * Added tests for convertActiveDataFromIndexesToLayers * Reduced the bundle size a little bit. * Reused strings and args. * Refactored expression functions. Added asynchronous behavior. * Fixed tests. * Updated limits. * Updated the limit. * Fixed types. * fixed types. * Turned back layerIds. * Removed convertActiveData from Lens. * Added test to the layerIds generator. * Fixed types. * Some fixes * Fix checks * Fix snapshots * Fixed problems with resetting of the inspector. * Fixed migrations. * Removed types. * Fix i18n * Remove unused translations * Remove unused xAxisId * Removed tones of `areFormatted` calculations. * Fixed `isTimeViz` and `isHistogramViz` by replacing filteredLayers with dataLayers. * Removed referenceLineLayers from the `groupAxesByType` fn. * Added validation to the layeredXyVis. * Fixed extent validation. * Removed comments. * Reduced limit. * Fix validation * Fix test * Some fixes after merging * Fix types * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix CI * Fix translations * Fix CI * Some fixes after resolve conflicts * Fix CI * Some updates for reference lines * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Fix translations * Fix CI * Fix types * Add validations for axes and rename `axes` arg to `yAxisConfigs` * Fix CI * Rename yConfig to decorations * Fix types and i18n * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix lint * Fix some nits * Fix CI * Fix CI * Fix CI * Refactoring auto assignment logic * Add possibility to use multiple split accessors * Some fixes after resolving merge conflicts * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Fixed types * Fix log datatable * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fixed snapshots * Fixed lint * Fixed comments * Some fixes after resolving merge conflicts * Fixed reference line position * Fix nit * Fixed CI * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fixed comments * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix some problems after resolving merge confilcts * Fix snapshot * Fix comments * Fix performance issue * Fix CI * Fix performance part 2 * Fix legend actions Co-authored-by: Yaroslav Kuznietsov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra Co-authored-by: Joe Reuter --- .../expression_xy/common/__mocks__/index.ts | 4 +- .../extended_data_layer.test.ts | 2 +- .../extended_data_layer.ts | 3 +- .../extended_data_layer_fn.ts | 2 +- .../common/expression_functions/xy_vis.ts | 3 +- .../common/expression_functions/xy_vis_fn.ts | 4 +- .../expression_xy/common/helpers/layers.ts | 18 +- .../common/types/expression_functions.ts | 4 +- .../common/utils/log_datatables.ts | 6 +- .../expression_xy/public/__mocks__/index.tsx | 2 +- .../__snapshots__/xy_chart.test.tsx.snap | 1037 ++++++++++++++++- .../public/components/data_layers.tsx | 6 +- .../public/components/legend_action.test.tsx | 32 +- .../public/components/legend_action.tsx | 67 +- .../components/tooltip/tooltip.test.tsx | 52 +- .../public/components/tooltip/tooltip.tsx | 2 +- .../public/components/xy_chart.test.tsx | 120 +- .../public/components/xy_chart.tsx | 55 +- .../public/helpers/axes_configuration.test.ts | 7 +- .../public/helpers/color_assignment.test.ts | 260 ++++- .../public/helpers/color_assignment.ts | 117 +- .../public/helpers/data_layers.tsx | 219 ++-- .../expression_xy/public/helpers/layers.ts | 66 +- .../expression_xy/public/helpers/state.ts | 2 +- .../__snapshots__/to_expression.test.ts.snap | 2 +- .../public/xy_visualization/to_expression.ts | 2 +- 26 files changed, 1717 insertions(+), 377 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index 1bde83c1822b7..300bb7f5de30e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -54,7 +54,7 @@ export const sampleLayer: DataLayerConfig = { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, @@ -72,7 +72,7 @@ export const sampleExtendedLayer: ExtendedDataLayerConfig = { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts index 5ec11188058f5..0041218fe919d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts @@ -17,7 +17,7 @@ describe('extendedDataLayerConfig', () => { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], xScaleType: 'linear', isHistogram: false, isHorizontal: false, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts index 58da88a8d4b25..17c6485c711da 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts @@ -23,9 +23,10 @@ export const extendedDataLayerFunction: ExtendedDataLayerFn = { types: ['string'], help: strings.getXAccessorHelp(), }, - splitAccessor: { + splitAccessors: { types: ['string'], help: strings.getSplitAccessorHelp(), + multi: true, }, accessors: { types: ['string'], diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts index 70a5bc2cf9e24..16905f96f9c2f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts @@ -23,7 +23,7 @@ export const extendedDataLayerFn: ExtendedDataLayerFn['fn'] = async (data, args, const accessors = getAccessors(args, table); validateAccessor(accessors.xAccessor, table.columns); - validateAccessor(accessors.splitAccessor, table.columns); + accessors.splitAccessors?.forEach((accessor) => validateAccessor(accessor, table.columns)); accessors.accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); validateMarkSizeForChartType(args.markSizeAccessor, args.seriesType); validateAccessor(args.markSizeAccessor, table.columns); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 7d2783cf6f1cd..9db238a117b75 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -24,9 +24,10 @@ export const xyVisFunction: XyVisFn = { types: ['string', 'vis_dimension'], help: strings.getXAccessorHelp(), }, - splitAccessor: { + splitAccessors: { types: ['string', 'vis_dimension'], help: strings.getSplitAccessorHelp(), + multi: true, }, accessors: { types: ['string', 'vis_dimension'], diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 00c29436926f4..ea619fa7e1c28 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -73,7 +73,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { accessors, xAccessor, hide, - splitAccessor, + splitAccessors, columnToLabel, xScaleType, isHistogram, @@ -96,7 +96,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const dataLayers: DataLayerConfigResult[] = [createDataLayer({ ...args, showLines }, data)]; validateAccessor(dataLayers[0].xAccessor, data.columns); - validateAccessor(dataLayers[0].splitAccessor, data.columns); + dataLayers[0].splitAccessors?.forEach((accessor) => validateAccessor(accessor, data.columns)); dataLayers[0].accessors.forEach((accessor) => validateAccessor(accessor, data.columns)); validateMarkSizeForChartType(dataLayers[0].markSizeAccessor, args.seriesType); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts index 83ea26eea262a..84ef7e41fbe5d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts @@ -46,22 +46,30 @@ export function getDataLayers(layers: XYExtendedLayerConfigResult[]) { export function getAccessors< T, - U extends { splitAccessor?: T; xAccessor?: T; accessors: T[]; markSizeAccessor?: T } + U extends { splitAccessors?: T[]; xAccessor?: T; accessors: T[]; markSizeAccessor?: T } >(args: U, table: Datatable) { - let splitAccessor: T | string | undefined = args.splitAccessor; + let splitAccessors: Array | undefined = args.splitAccessors; let xAccessor: T | string | undefined = args.xAccessor; let accessors: Array = args.accessors ?? []; let markSizeAccessor: T | string | undefined = args.markSizeAccessor; - if (!splitAccessor && !xAccessor && !(accessors && accessors.length) && !markSizeAccessor) { + if ( + !(splitAccessors && splitAccessors.length) && + !xAccessor && + !(accessors && accessors.length) && + !markSizeAccessor + ) { const y = table.columns.find((column) => column.id === PointSeriesColumnNames.Y)?.id; + const splitColumnId = table.columns.find( + (column) => column.id === PointSeriesColumnNames.COLOR + )?.id; xAccessor = table.columns.find((column) => column.id === PointSeriesColumnNames.X)?.id; - splitAccessor = table.columns.find((column) => column.id === PointSeriesColumnNames.COLOR)?.id; + splitAccessors = splitColumnId ? [splitColumnId] : []; accessors = y ? [y] : []; markSizeAccessor = table.columns.find( (column) => column.id === PointSeriesColumnNames.SIZE )?.id; } - return { splitAccessor, xAccessor, accessors, markSizeAccessor }; + return { splitAccessors, xAccessor, accessors, markSizeAccessor }; } diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index b27863efa4fe8..8da796c811a68 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -116,7 +116,7 @@ export interface DataLayerArgs { seriesType: SeriesType; xAccessor?: string | ExpressionValueVisDimension; hide?: boolean; - splitAccessor?: string | ExpressionValueVisDimension; + splitAccessors?: Array; markSizeAccessor?: string | ExpressionValueVisDimension; lineWidth?: number; showPoints?: boolean; @@ -142,7 +142,7 @@ export interface ExtendedDataLayerArgs { seriesType: SeriesType; xAccessor?: string; hide?: boolean; - splitAccessor?: string; + splitAccessors?: string[]; markSizeAccessor?: string; lineWidth?: number; showPoints?: boolean; diff --git a/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts index 44026b30ed493..f2a85241d1aec 100644 --- a/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts +++ b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts @@ -34,10 +34,10 @@ export const getLayerDimensions = ( layer: CommonXYDataLayerConfig | ReferenceLineLayerConfig ): Dimension[] => { let xAccessor; - let splitAccessor; + let splitAccessors; if (layer.layerType === LayerTypes.DATA) { xAccessor = layer.xAccessor; - splitAccessor = layer.splitAccessor; + splitAccessors = layer.splitAccessors; } const { accessors, layerType } = layer; @@ -47,6 +47,6 @@ export const getLayerDimensions = ( layerType === LayerTypes.DATA ? strings.getMetricHelp() : strings.getReferenceLineHelp(), ], [xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()], - [splitAccessor ? [splitAccessor] : undefined, strings.getBreakdownHelp()], + [splitAccessors ? splitAccessors : undefined, strings.getBreakdownHelp()], ]; }; diff --git a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx index a7c6494a33542..d4781db0ff915 100644 --- a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx @@ -171,7 +171,7 @@ export const dateHistogramLayer: DataLayerConfig = { isStacked: true, isPercentage: false, isHorizontal: false, - splitAccessor: 'splitAccessorId', + splitAccessors: ['splitAccessorId'], seriesType: 'bar', accessors: ['yAccessorId'], palette: mockPaletteOutput, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index ddd1dbd11525f..71e3b38b57072 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -413,6 +413,62 @@ exports[`XYChart component it renders area 1`] = ` = ({ @@ -73,8 +75,9 @@ export const DataLayers: FC = ({ formattedDatatables, chartHasMoreThanOneBarSeries, defaultXScaleType, + fieldFormats, }) => { - const colorAssignments = getColorAssignments(layers, formatFactory); + const colorAssignments = getColorAssignments(layers, titles, fieldFormats, formattedDatatables); return ( <> {layers.flatMap((layer) => @@ -114,6 +117,7 @@ export const DataLayers: FC = ({ emphasizeFitting, fillOpacity, defaultXScaleType, + fieldFormats, }); const index = `${layer.layerId}-${accessorIndex}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx index 147338853a808..f82428993ce46 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx @@ -17,6 +17,8 @@ import { LayerTypes } from '../../common/constants'; import { getLegendAction } from './legend_action'; import { LegendActionPopover } from './legend_action_popover'; import { mockPaletteOutput } from '../../common/__mocks__'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LayerFieldFormats } from '../helpers'; const table: Datatable = { type: 'datatable', @@ -163,7 +165,7 @@ const sampleLayer: DataLayerConfig = { showLines: true, xAccessor: 'c', accessors: ['a', 'b'], - splitAccessor: 'splitAccessorId', + splitAccessors: ['splitAccessorId'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, @@ -176,7 +178,26 @@ describe('getLegendAction', function () { const Component: ComponentType = getLegendAction( [sampleLayer], jest.fn(), - jest.fn(), + { + first: { + splitSeriesAccessors: { + splitAccessorId: { + format: { id: 'string' }, + formatter: { + convert(x: unknown) { + return x; + }, + } as FieldFormat, + }, + }, + } as unknown as LayerFieldFormats, + }, + { + first: { + table, + formattedColumns: {}, + }, + }, {} ); let wrapper: ReactWrapper; @@ -188,6 +209,7 @@ describe('getLegendAction', function () { series: [ { seriesKeys: ["Women's Accessories", 'test'], + splitAccessors: new Map().set('splitAccessorId', "Women's Accessories"), }, ] as unknown as SeriesIdentifier[], }; @@ -205,6 +227,7 @@ describe('getLegendAction', function () { series: [ { seriesKeys: ['test', 'b'], + splitAccessors: new Map().set('splitAccessorId', 'test'), }, ] as unknown as SeriesIdentifier[], }; @@ -219,12 +242,15 @@ describe('getLegendAction', function () { series: [ { seriesKeys: ["Women's Accessories", 'b'], + splitAccessors: new Map().set('splitAccessorId', "Women's Accessories"), }, ] as unknown as SeriesIdentifier[], }; wrapper = mountWithIntl(); expect(wrapper.find(EuiPopover).length).toBe(1); - expect(wrapper.find(EuiPopover).prop('title')).toEqual("Women's Accessories, filter options"); + expect(wrapper.find(EuiPopover).prop('title')).toEqual( + "Women's Accessories - Label B, filter options" + ); expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ data: [ { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx index 68e5b89559933..e27bb716c35c7 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx @@ -11,15 +11,20 @@ import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { FilterEvent } from '../types'; import type { CommonXYDataLayerConfig } from '../../common'; -import type { FormatFactory } from '../types'; import { LegendActionPopover } from './legend_action_popover'; -import { DatatablesWithFormatInfo, getFormat } from '../helpers'; +import { + DatatablesWithFormatInfo, + getSeriesName, + LayersAccessorsTitles, + LayersFieldFormats, +} from '../helpers'; export const getLegendAction = ( dataLayers: CommonXYDataLayerConfig[], onFilter: (data: FilterEvent['data']) => void, - formatFactory: FormatFactory, - formattedDatatables: DatatablesWithFormatInfo + fieldFormats: LayersFieldFormats, + formattedDatatables: DatatablesWithFormatInfo, + titles: LayersAccessorsTitles ): LegendAction => React.memo(({ series: [xySeries] }) => { const series = xySeries as XYChartSeriesIdentifier; @@ -36,36 +41,31 @@ export const getLegendAction = ( } const layer = dataLayers[layerIndex]; - if (!layer || !layer.splitAccessor) { + if (!layer || !layer.splitAccessors || !layer.splitAccessors.length) { return null; } - const splitLabel = series.seriesKeys[0] as string; - const { table } = layer; - const accessor = getAccessorByDimension(layer.splitAccessor, table.columns); - const formatter = formatFactory( - accessor ? getFormat(table.columns, layer.splitAccessor) : undefined - ); - const rowIndex = table.rows.findIndex((row) => { - if (formattedDatatables[layer.layerId]?.formattedColumns[accessor]) { - // stringify the value to compare with the chart value - return formatter.convert(row[accessor]) === splitLabel; + const data: FilterEvent['data']['data'] = []; + + series.splitAccessors.forEach((value, accessor) => { + const rowIndex = formattedDatatables[layer.layerId].table.rows.findIndex((row) => { + return row[accessor] === value; + }); + if (rowIndex !== -1) { + data.push({ + row: rowIndex, + column: table.columns.findIndex((column) => column.id === accessor), + value: table.rows[rowIndex][accessor], + table, + }); } - return row[accessor] === splitLabel; }); - if (rowIndex < 0) return null; - - const data = [ - { - row: rowIndex, - column: table.columns.findIndex((col) => col.id === accessor), - value: accessor ? table.rows[rowIndex][accessor] : splitLabel, - table, - }, - ]; + if (data.length === 0) { + return null; + } const context: FilterEvent['data'] = { data, @@ -74,9 +74,18 @@ export const getLegendAction = ( return ( ; + seriesSplitAccessors: Map; }): XYChartSeriesIdentifier => ({ - specId: generateSeriesId({ layerId, xAccessor, splitAccessor }, yAccessor), + specId: generateSeriesId({ layerId, xAccessor }, splitAccessors, yAccessor), yAccessor: yAccessor ?? 'a', - splitAccessors, + splitAccessors: seriesSplitAccessors, seriesKeys: [], key: '1', smVerticalAccessorValue: splitColumnAccessor, @@ -42,9 +42,11 @@ const getSeriesIdentifier = ({ describe('Tooltip', () => { const { data } = sampleArgs(); - const { layerId, xAccessor, splitAccessor, accessors } = sampleLayer; - const splitAccessors = new Map(); - splitAccessors.set(splitAccessor, '10'); + const { layerId, xAccessor, splitAccessors = [], accessors } = sampleLayer; + const seriesSplitAccessors = new Map(); + splitAccessors.forEach((splitAccessor) => { + seriesSplitAccessors.set(splitAccessor, '10'); + }); const accessor = accessors[0] as string; const splitRowAccessor = 'd'; @@ -54,8 +56,8 @@ describe('Tooltip', () => { layerId, yAccessor: accessor, xAccessor: xAccessor as string, - splitAccessor: splitAccessor as string, - splitAccessors, + splitAccessors: splitAccessors as string[], + seriesSplitAccessors, splitRowAccessor, splitColumnAccessor, }); @@ -74,7 +76,7 @@ describe('Tooltip', () => { [layerId]: { xTitles: { [xAccessor as string]: 'x-title' }, yTitles: { [accessor]: 'y-title' }, - splitSeriesTitles: { [splitAccessor as string]: 'split-series-title' }, + splitSeriesTitles: { [splitAccessors[0] as string]: 'split-series-title' }, splitRowTitles: { [splitRowAccessor]: 'split-row-title' }, splitColumnTitles: { [splitColumnAccessor]: 'split-column-title' }, }, @@ -84,7 +86,12 @@ describe('Tooltip', () => { [layerId]: { xAccessors: { [xAccessor as string]: { id: 'number' } }, yAccessors: { [accessor]: { id: 'string' } }, - splitSeriesAccessors: { [splitAccessor as string]: { id: 'date' } }, + splitSeriesAccessors: { + [splitAccessors[0] as string]: { + format: { id: 'date' }, + formatter: { convert: (value) => `formatted-date-${value}` } as FieldFormat, + }, + }, splitRowAccessors: { [splitRowAccessor]: { id: 'number' } }, splitColumnAccessors: { [splitColumnAccessor]: { id: 'number' } }, }, @@ -188,8 +195,8 @@ describe('Tooltip', () => { const seriesIdentifierWithoutX = getSeriesIdentifier({ layerId, yAccessor: accessor, - splitAccessor: splitAccessor as string, - splitAccessors, + splitAccessors: splitAccessors as string[], + seriesSplitAccessors, splitRowAccessor, splitColumnAccessor, }); @@ -215,8 +222,8 @@ describe('Tooltip', () => { const seriesIdentifierWithoutY = getSeriesIdentifier({ layerId, xAccessor: xAccessor as string, - splitAccessor: splitAccessor as string, - splitAccessors, + splitAccessors: splitAccessors as string[], + seriesSplitAccessors, splitRowAccessor, splitColumnAccessor, }); @@ -243,7 +250,8 @@ describe('Tooltip', () => { layerId, xAccessor: xAccessor as string, yAccessor: accessor, - splitAccessors: new Map(), + splitAccessors: splitAccessors as string[], + seriesSplitAccessors: new Map(), splitRowAccessor, splitColumnAccessor, }); @@ -270,8 +278,8 @@ describe('Tooltip', () => { layerId, xAccessor: xAccessor as string, yAccessor: accessor, - splitAccessor: splitAccessor as string, - splitAccessors, + splitAccessors: splitAccessors as string[], + seriesSplitAccessors, splitColumnAccessor, }); @@ -297,8 +305,8 @@ describe('Tooltip', () => { layerId, xAccessor: xAccessor as string, yAccessor: accessor, - splitAccessor: splitAccessor as string, - splitAccessors, + splitAccessors: splitAccessors as string[], + seriesSplitAccessors, splitRowAccessor, }); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.tsx b/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.tsx index 6c7a3e586e8e6..7ab7c9f549e24 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.tsx @@ -78,7 +78,7 @@ export const Tooltip: FC = ({ seriesIdentifier.splitAccessors.forEach((splitValue, key) => { const splitSeriesFormatter = formattedColumns[key] ? null - : formatFactory(layerFormats.splitSeriesAccessors[key]); + : layerFormats.splitSeriesAccessors[key].formatter; const label = layerTitles?.splitSeriesTitles?.[key]; const value = splitSeriesFormatter ? splitSeriesFormatter.convert(splitValue) : `${splitValue}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 8a61841077dfd..cc5805f7e97c1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -162,7 +162,7 @@ describe('XYChart component', () => { xAccessor: 'c', accessors: ['a', 'b'], showLines: true, - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'time', isHistogram: false, @@ -264,7 +264,7 @@ describe('XYChart component', () => { isPercentage: false, xAccessor: 'c', accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'time', isHistogram: true, @@ -408,7 +408,7 @@ describe('XYChart component', () => { seriesType: 'line', xScaleType: 'time', isHistogram: true, - splitAccessor: undefined, + splitAccessors: undefined, table: newData, } as DataLayerConfig, ], @@ -1108,7 +1108,7 @@ describe('XYChart component', () => { key: 'spec{d}yAccessor{d}splitAccessors{b-2}', specId: 'd', yAccessor: 'd', - splitAccessors: {}, + splitAccessors: new Map().set('b', 2), seriesKeys: [2, 'd'], }; @@ -1132,7 +1132,7 @@ describe('XYChart component', () => { showLines: true, xAccessor: 'b', xScaleType: 'time', - splitAccessor: 'b', + splitAccessors: ['b'], accessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', palette: mockPaletteOutput, @@ -1540,7 +1540,7 @@ describe('XYChart component', () => { { ...(args.layers[0] as DataLayerConfig), xAccessor: undefined, - splitAccessor: 'e', + splitAccessors: ['e'], seriesType: 'bar', isStacked: true, }, @@ -1570,7 +1570,7 @@ describe('XYChart component', () => { seriesType: 'bar', isHistogram: true, } as DataLayerConfig; - delete firstLayer.splitAccessor; + delete firstLayer.splitAccessors; const component = shallow( ); @@ -1586,7 +1586,7 @@ describe('XYChart component', () => { seriesType: 'bar', isHistogram: true, } as DataLayerConfig; - delete firstLayer.splitAccessor; + delete firstLayer.splitAccessors; const component = shallow( ); @@ -1603,13 +1603,13 @@ describe('XYChart component', () => { seriesType: 'line', isHistogram: true, } as DataLayerConfig; - delete firstLayer.splitAccessor; + delete firstLayer.splitAccessors; const secondLayer: DataLayerConfig = { ...args.layers[0], seriesType: 'line', isHistogram: true, } as DataLayerConfig; - delete secondLayer.splitAccessor; + delete secondLayer.splitAccessors; const component = shallow( ); @@ -1794,7 +1794,7 @@ describe('XYChart component', () => { ...layer, type: 'extendedDataLayer', accessors: ['a', 'b'], - splitAccessor: undefined, + splitAccessors: undefined, decorations: [ { type: 'dataDecorationConfig', @@ -1813,7 +1813,7 @@ describe('XYChart component', () => { ...layer, type: 'extendedDataLayer', accessors: ['c'], - splitAccessor: undefined, + splitAccessors: undefined, decorations: [ { type: 'dataDecorationConfig', @@ -1878,12 +1878,14 @@ describe('XYChart component', () => { (lineSeries.at(0).prop('color') as Function)!({ yAccessor: 'a', seriesKeys: ['a'], + splitAccessors: new Map(), }) ).toEqual('blue'); expect( (lineSeries.at(1).prop('color') as Function)!({ yAccessor: 'c', seriesKeys: ['c'], + splitAccessors: new Map(), }) ).toEqual('blue'); }); @@ -2012,7 +2014,7 @@ describe('XYChart component', () => { { ...args.layers[0], accessors: ['a'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A"}', table: dataWithoutFormats, }, @@ -2026,7 +2028,16 @@ describe('XYChart component', () => { .find(LineSeries) .prop('name') as SeriesNameFn; - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); + expect( + nameFn( + { + ...nameFnArgs, + seriesKeys: ['split1', 'a'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('split1'); }); test('split series with formatting and single y accessor', () => { @@ -2037,7 +2048,7 @@ describe('XYChart component', () => { { ...args.layers[0], accessors: ['a'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A"}', table: dataWithFormats, }, @@ -2052,7 +2063,16 @@ describe('XYChart component', () => { .prop('name') as SeriesNameFn; convertSpy.mockReturnValueOnce('formatted'); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); + expect( + nameFn( + { + ...nameFnArgs, + seriesKeys: ['split1', 'a'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('formatted'); expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' }); }); @@ -2064,7 +2084,7 @@ describe('XYChart component', () => { { ...args.layers[0], accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A","b": "Label B"}', table: dataWithoutFormats, }, @@ -2077,12 +2097,26 @@ describe('XYChart component', () => { const nameFn1 = lineSeries.at(0).prop('name') as SeriesNameFn; const nameFn2 = lineSeries.at(0).prop('name') as SeriesNameFn; - expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( - 'split1 - Label A' - ); - expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( - 'split1 - Label B' - ); + expect( + nameFn1( + { + ...nameFnArgs, + seriesKeys: ['split1', 'a'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('split1 - Label A'); + expect( + nameFn2( + { + ...nameFnArgs, + seriesKeys: ['split1', 'b'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('split1 - Label B'); }); test('split series with formatting with multiple y accessors', () => { @@ -2093,7 +2127,7 @@ describe('XYChart component', () => { { ...args.layers[0], accessors: ['a', 'b'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A","b": "Label B"}', table: dataWithFormats, }, @@ -2107,12 +2141,26 @@ describe('XYChart component', () => { const nameFn2 = lineSeries.at(1).prop('name') as SeriesNameFn; convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); - expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( - 'formatted1 - Label A' - ); - expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( - 'formatted2 - Label B' - ); + expect( + nameFn1( + { + ...nameFnArgs, + seriesKeys: ['split1', 'a'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('formatted1 - Label A'); + expect( + nameFn2( + { + ...nameFnArgs, + seriesKeys: ['split1', 'b'], + splitAccessors: nameFnArgs.splitAccessors.set('d', 'split1'), + }, + false + ) + ).toEqual('formatted2 - Label B'); }); }); @@ -2475,7 +2523,7 @@ describe('XYChart component', () => { showLines: true, xAccessor: 'a', accessors: ['c'], - splitAccessor: 'b', + splitAccessors: ['b'], columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, @@ -2491,7 +2539,7 @@ describe('XYChart component', () => { showLines: true, xAccessor: 'a', accessors: ['c'], - splitAccessor: 'b', + splitAccessors: ['b'], columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, @@ -2582,7 +2630,7 @@ describe('XYChart component', () => { seriesType: 'line', xAccessor: 'a', accessors: ['c'], - splitAccessor: 'b', + splitAccessors: ['b'], columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, @@ -2671,7 +2719,7 @@ describe('XYChart component', () => { showLines: true, xAccessor: 'a', accessors: ['c'], - splitAccessor: 'b', + splitAccessors: ['b'], columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, @@ -2701,7 +2749,7 @@ describe('XYChart component', () => { { ...(args.layers[0] as DataLayerConfig), accessors: ['a'], - splitAccessor: undefined, + splitAccessors: undefined, }, ], legend: { ...args.legend, isVisible: true, showSingleSeries: true }, @@ -2724,7 +2772,7 @@ describe('XYChart component', () => { { ...(args.layers[0] as DataLayerConfig), accessors: ['a'], - splitAccessor: undefined, + splitAccessors: undefined, }, ], legend: { ...args.legend, isVisible: true, isInside: true }, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 703504f68e2cc..a47f356c33dda 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -221,8 +221,8 @@ export function XYChart({ ); const fieldFormats = useMemo( - () => getLayersFormats(dataLayers, { splitColumnAccessor, splitRowAccessor }), - [dataLayers, splitColumnAccessor, splitRowAccessor] + () => getLayersFormats(dataLayers, { splitColumnAccessor, splitRowAccessor }, formatFactory), + [dataLayers, splitColumnAccessor, splitRowAccessor, formatFactory] ); if (dataLayers.length === 0) { @@ -250,7 +250,9 @@ export function XYChart({ const chartHasMoreThanOneSeries = filteredLayers.length > 1 || filteredLayers.some((layer) => layer.accessors.length > 1) || - filteredLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor); + filteredLayers.some( + (layer) => isDataLayer(layer) && layer.splitAccessors && layer.splitAccessors.length + ); const shouldRotate = isHorizontalChart(dataLayers); const yAxesConfiguration = getAxesConfiguration( @@ -291,7 +293,9 @@ export function XYChart({ const chartHasMoreThanOneBarSeries = filteredBarLayers.length > 1 || filteredBarLayers.some((layer) => layer.accessors.length > 1) || - filteredBarLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor); + filteredBarLayers.some( + (layer) => isDataLayer(layer) && layer.splitAccessors && layer.splitAccessors.length + ); const isTimeViz = isTimeChart(dataLayers); @@ -501,35 +505,31 @@ export function XYChart({ row: rowIndex, column: table.columns.findIndex((col) => col.id === xAccessor), value: xAccessor ? table.rows[rowIndex][xAccessor] : xyGeometry.x, + table, }, ]; + const splitPoints: FilterEvent['data']['data'] = []; + if (xySeries.seriesKeys.length > 1) { - const pointValue = xySeries.seriesKeys[0]; - const splitAccessor = layer.splitAccessor - ? getAccessorByDimension(layer.splitAccessor, table.columns) - : undefined; - - const splitFormat = splitAccessor - ? fieldFormats[layer.layerId].splitSeriesAccessors[splitAccessor] - : undefined; - const splitFormatter = formatFactory(splitFormat); - - points.push({ - row: table.rows.findIndex((row) => { - if (splitAccessor) { - if (formattedDatatables[layer.layerId]?.formattedColumns[splitAccessor]) { - return splitFormatter.convert(row[splitAccessor]) === pointValue; - } - return row[splitAccessor] === pointValue; + xySeries.splitAccessors.forEach((value, accessor) => { + const splitPointRowIndex = formattedDatatables[layer.layerId].table.rows.findIndex( + (row) => { + return row[accessor] === value; } - }), - column: table.columns.findIndex((col) => col.id === splitAccessor), - value: pointValue, + ); + if (splitPointRowIndex !== -1) { + splitPoints.push({ + row: splitPointRowIndex, + column: table.columns.findIndex((column) => column.id === accessor), + value: table.rows[splitPointRowIndex][accessor], + table, + }); + } }); } const context: FilterEvent['data'] = { - data: points.map(({ row, column, value }) => ({ row, column, value, table })), + data: [...points, ...splitPoints], }; onClickValue(context); }; @@ -703,7 +703,7 @@ export function XYChart({ onElementClick={interactive ? clickHandler : undefined} legendAction={ interactive - ? getLegendAction(dataLayers, onClickValue, formatFactory, formattedDatatables) + ? getLegendAction(dataLayers, onClickValue, fieldFormats, formattedDatatables, titles) : undefined } showLegendExtra={isHistogramViz && valuesInLegend} @@ -790,7 +790,7 @@ export function XYChart({ histogramMode={dataLayers.every( (layer) => layer.isHistogram && - (layer.isStacked || !layer.splitAccessor) && + (layer.isStacked || !layer.splitAccessors || !layer.splitAccessors.length) && (layer.isStacked || layer.seriesType !== SeriesTypes.BAR || !chartHasMoreThanOneBarSeries) @@ -817,6 +817,7 @@ export function XYChart({ formattedDatatables={formattedDatatables} chartHasMoreThanOneBarSeries={chartHasMoreThanOneBarSeries} defaultXScaleType={defaultXScaleType} + fieldFormats={fieldFormats} /> )} {referenceLineLayers.length ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts index eb5e2a29c2cdc..35019d75e0554 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { FieldFormat } from '@kbn/field-formats-plugin/common'; import { DataLayerConfig, YAxisConfigResult } from '../../common'; import { LayerTypes } from '../../common/constants'; import { Datatable } from '@kbn/expressions-plugin/public'; @@ -237,7 +238,7 @@ describe('axes_configuration', () => { seriesType: 'line', xAccessor: 'c', accessors: ['yAccessorId'], - splitAccessor: 'd', + splitAccessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, @@ -256,7 +257,9 @@ describe('axes_configuration', () => { yAccessorId3: { id: 'currency', params: {} }, yAccessorId4: { id: 'currency', params: {} }, }, - splitSeriesAccessors: { d: { id: 'number', params: {} } }, + splitSeriesAccessors: { + d: { format: { id: 'number', params: {} }, formatter: {} as FieldFormat }, + }, splitColumnAccessors: {}, splitRowAccessors: {}, }, diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts index c1405a2639f5e..4a4c0ac5b7af1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts @@ -8,9 +8,10 @@ import { getColorAssignments } from './color_assignment'; import type { DataLayerConfig } from '../../common'; -import type { FormatFactory } from '../types'; import { LayerTypes } from '../../common/constants'; import { Datatable } from '@kbn/expressions-plugin'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LayersFieldFormats } from './layers'; describe('color_assignment', () => { const tables: Record = { @@ -61,7 +62,7 @@ describe('color_assignment', () => { isHorizontal: false, palette: { type: 'palette', name: 'palette1' }, layerType: LayerTypes.DATA, - splitAccessor: 'split1', + splitAccessors: ['split1'], accessors: ['y1', 'y2'], table: tables['1'], }, @@ -77,22 +78,61 @@ describe('color_assignment', () => { isHorizontal: false, palette: { type: 'palette', name: 'palette2' }, layerType: LayerTypes.DATA, - splitAccessor: 'split2', + splitAccessors: ['split2'], accessors: ['y3', 'y4'], table: tables['2'], }, ]; - const formatFactory = (() => - ({ - convert(x: unknown) { - return x; + const fieldFormats = { + first: { + splitSeriesAccessors: { + split1: { + format: { id: 'string' }, + formatter: { + convert: (x) => x, + } as FieldFormat, + }, + }, + }, + second: { + splitSeriesAccessors: { + split2: { + format: { id: 'string' }, + formatter: { + convert: (x) => x, + } as FieldFormat, + }, }, - } as unknown)) as FormatFactory; + }, + } as unknown as LayersFieldFormats; + + const formattedDatatables = { + first: { + table: tables['1'], + formattedColumns: {}, + }, + second: { + table: tables['2'], + formattedColumns: {}, + }, + }; describe('totalSeriesCount', () => { it('should calculate total number of series per palette', () => { - const assignments = getColorAssignments(layers, formatFactory); + const assignments = getColorAssignments( + layers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables + ); // two y accessors, with 3 splitted series expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3); expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); @@ -101,7 +141,16 @@ describe('color_assignment', () => { it('should calculate total number of series spanning multible layers', () => { const assignments = getColorAssignments( [layers[0], { ...layers[1], palette: layers[0].palette }], - formatFactory + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables ); // two y accessors, with 3 splitted series, two times expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3 + 2 * 3); @@ -110,8 +159,17 @@ describe('color_assignment', () => { it('should calculate total number of series for non split series', () => { const assignments = getColorAssignments( - [layers[0], { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }], - formatFactory + [layers[0], { ...layers[1], palette: layers[0].palette, splitAccessors: undefined }], + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables ); // two y accessors, with 3 splitted series for the first layer, 2 non splitted y accessors for the second layer expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3 + 2); @@ -120,7 +178,7 @@ describe('color_assignment', () => { it('should format non-primitive values and count them correctly', () => { const complexObject = { aProp: 123 }; - const formatMock = jest.fn((x) => 'formatted'); + const formatMock = jest.fn((value) => (typeof value === 'object' ? 'formatted' : value)); const newLayers = [ { ...layers[0], @@ -128,31 +186,61 @@ describe('color_assignment', () => { }, layers[1], ]; + fieldFormats.first.splitSeriesAccessors.split1.formatter.convert = formatMock; + const newFormattedDatatables = { + first: { + formattedColumns: formattedDatatables.first.formattedColumns, + table: { + ...formattedDatatables.first.table, + rows: [{ split1: complexObject }, { split1: 'abc' }], + }, + }, + second: formattedDatatables.second, + }; const assignments = getColorAssignments( newLayers, - (() => - ({ - convert: formatMock, - } as unknown)) as FormatFactory + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + newFormattedDatatables ); + + fieldFormats.first.splitSeriesAccessors.split1.formatter.convert = (x) => x as string; + expect(formatMock).toHaveBeenCalledWith(complexObject); expect(assignments.palette1.totalSeriesCount).toEqual(2 * 2); expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); expect(formatMock).toHaveBeenCalledWith(complexObject); }); - it('should handle missing tables', () => { - const assignments = getColorAssignments( - layers.map((l) => ({ ...l, table: {} as any })), - formatFactory - ); - // if there is no data, just assume a single split - expect(assignments.palette1.totalSeriesCount).toEqual(2); - }); - it('should handle missing columns', () => { const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]]; - const assignments = getColorAssignments(newLayers, formatFactory); + const newFormattedDatatables = { + first: { + formattedColumns: formattedDatatables.first.formattedColumns, + table: { ...formattedDatatables.first.table, columns: [] }, + }, + second: formattedDatatables.second, + }; + const assignments = getColorAssignments( + newLayers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + newFormattedDatatables + ); // if the split column is missing, just assume a single split expect(assignments.palette1.totalSeriesCount).toEqual(2); @@ -161,32 +249,68 @@ describe('color_assignment', () => { describe('getRank', () => { it('should return the correct rank for a series key', () => { - const assignments = getColorAssignments(layers, formatFactory); + const assignments = getColorAssignments( + layers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables + ); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(3); + expect(assignments.palette1.getRank(layers[0], '2 - test2')).toEqual(3); // 1 series in front of 1/y4 - 1/y3 - expect(assignments.palette2.getRank(layers[1], '1', 'y4')).toEqual(1); + expect(assignments.palette2.getRank(layers[1], '1 - test4')).toEqual(1); }); it('should return the correct rank for a series key spanning multiple layers', () => { const newLayers = [layers[0], { ...layers[1], palette: layers[0].palette }]; - const assignments = getColorAssignments(newLayers, formatFactory); + const assignments = getColorAssignments( + newLayers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables + ); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3); + expect(assignments.palette1.getRank(newLayers[0], '2 - test2')).toEqual(3); // 2 series in front for the current layer (1/y3, 1/y4), plus all 6 series from the first layer - expect(assignments.palette1.getRank(newLayers[1], '2', 'y3')).toEqual(8); + expect(assignments.palette1.getRank(newLayers[1], '2 - test3')).toEqual(8); }); it('should return the correct rank for a series without a split', () => { const newLayers = [ layers[0], - { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }, + { ...layers[1], palette: layers[0].palette, splitAccessors: undefined }, ]; - const assignments = getColorAssignments(newLayers, formatFactory); + const assignments = getColorAssignments( + newLayers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + formattedDatatables + ); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3); + expect(assignments.palette1.getRank(newLayers[0], '2 - test2')).toEqual(3); // 1 series in front for the current layer (y3), plus all 6 series from the first layer - expect(assignments.palette1.getRank(newLayers[1], 'Metric y4', 'y4')).toEqual(7); + expect(assignments.palette1.getRank(newLayers[1], 'test4')).toEqual(7); }); it('should return the correct rank for a series with a non-primitive value', () => { @@ -197,34 +321,64 @@ describe('color_assignment', () => { }, layers[1], ]; + fieldFormats.first.splitSeriesAccessors.split1.formatter.convert = (value: unknown) => + (typeof value === 'object' ? 'formatted' : value) as string; + const newFormattedDatatables = { + first: { + formattedColumns: formattedDatatables.first.formattedColumns, + table: { + ...formattedDatatables.first.table, + rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }], + }, + }, + second: formattedDatatables.second, + }; const assignments = getColorAssignments( newLayers, - (() => - ({ - convert: () => 'formatted', - } as unknown)) as FormatFactory + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + newFormattedDatatables ); - // 3 series in front of (complex object)/y1 - abc/y1, abc/y2 - expect(assignments.palette1.getRank(layers[0], 'formatted', 'y1')).toEqual(2); - }); - it('should handle missing tables', () => { - const assignments = getColorAssignments( - layers.map((l) => ({ ...l, table: {} as any })), - formatFactory - ); - // if there is no data, assume it is the first splitted series. One series in front - 0/y1 - expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + fieldFormats.first.splitSeriesAccessors.split1.formatter.convert = (x) => x as string; + // 3 series in front of (complex object)/y1 - abc/y1, abc/y2 + expect(assignments.palette1.getRank(layers[0], 'formatted - test1')).toEqual(2); }); it('should handle missing columns', () => { const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]]; + const newFormattedDatatables = { + first: { + formattedColumns: formattedDatatables.first.formattedColumns, + table: { ...formattedDatatables.first.table, columns: [] }, + }, + second: formattedDatatables.second, + }; - const assignments = getColorAssignments(newLayers, formatFactory); + const assignments = getColorAssignments( + newLayers, + { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + fieldFormats, + newFormattedDatatables + ); // if the split column is missing, assume it is the first splitted series. One series in front - 0/y1 - expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + expect(assignments.palette1.getRank(layers[0], 'test2')).toEqual(1); }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index 0b7f8d8b08f22..150c27434e1be 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { uniq, mapValues } from 'lodash'; +import { mapValues } from 'lodash'; +import { DatatableRow } from '@kbn/expressions-plugin'; import { euiLightVars } from '@kbn/ui-theme'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; -import { FormatFactory } from '../types'; +import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { isDataLayer } from './visualization'; import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common'; -import { getFormat } from './format'; - -const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; +import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers'; +import { DatatablesWithFormatInfo, DatatableWithFormatInfo } from './data_layers'; export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; @@ -22,13 +22,72 @@ export type ColorAssignments = Record< string, { totalSeriesCount: number; - getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string): number; + getRank(sortedLayer: CommonXYDataLayerConfig, seriesName: string): number; } >; +function getSplitName( + splitAccessors: Array = [], + formattedDatatable: DatatableWithFormatInfo, + row: DatatableRow, + fieldFormats: LayerFieldFormats +) { + return splitAccessors.reduce((splitName, accessor) => { + if (!formattedDatatable.table.columns.length) return; + const splitAccessor = getAccessorByDimension(accessor, formattedDatatable.table.columns); + const splitFormatterObj = fieldFormats.splitSeriesAccessors[splitAccessor]; + const name = formattedDatatable.formattedColumns[splitAccessor] + ? row[splitAccessor] + : splitFormatterObj.formatter.convert(row[splitAccessor]); + if (splitName) { + return `${splitName} - ${name}`; + } else { + return name; + } + }, ''); +} + +export const getAllSeries = ( + formattedDatatable: DatatableWithFormatInfo, + splitAccessors: CommonXYDataLayerConfig['splitAccessors'] = [], + accessors: Array, + columnToLabel: CommonXYDataLayerConfig['columnToLabel'], + titles: LayerAccessorsTitles, + fieldFormats: LayerFieldFormats +) => { + if (!formattedDatatable.table) { + return []; + } + + const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; + + const allSeries: string[] = []; + + formattedDatatable.table.rows.forEach((row) => { + const splitName = getSplitName(splitAccessors, formattedDatatable, row, fieldFormats); + + accessors.forEach((accessor) => { + const yAccessor = getAccessorByDimension(accessor, formattedDatatable.table.columns); + const yTitle = columnToLabelMap[yAccessor] ?? titles?.yTitles?.[yAccessor] ?? null; + let name = yTitle; + if (splitName) { + name = accessors.length > 1 ? `${splitName} - ${yTitle}` : splitName; + } + + if (!allSeries.includes(name)) { + allSeries.push(name); + } + }); + }); + + return allSeries; +}; + export function getColorAssignments( layers: CommonXYLayerConfig[], - formatFactory: FormatFactory + titles: LayerAccessorsTitles, + fieldFormats: LayersFieldFormats, + formattedDatatables: DatatablesWithFormatInfo ): ColorAssignments { const layersPerPalette: Record = {}; @@ -46,28 +105,17 @@ export function getColorAssignments( return mapValues(layersPerPalette, (paletteLayers) => { const seriesPerLayer = paletteLayers.map((layer) => { - if (!layer.splitAccessor) { - return { numberOfSeries: layer.accessors.length, splits: [] }; - } - const splitAccessor = getAccessorByDimension(layer.splitAccessor, layer.table.columns); - const column = layer.table.columns?.find(({ id }) => id === splitAccessor); - const columnFormatter = - column && formatFactory(getFormat(layer.table.columns, layer.splitAccessor)); - const splits = - !column || !layer.table - ? [] - : uniq( - layer.table.rows.map((row) => { - let value = row[splitAccessor]; - if (value && !isPrimitive(value)) { - value = columnFormatter?.convert(value) ?? value; - } else { - value = String(value); - } - return value; - }) - ); - return { numberOfSeries: (splits.length || 1) * layer.accessors.length, splits }; + const allSeries = + getAllSeries( + formattedDatatables[layer.layerId], + layer.splitAccessors, + layer.accessors, + layer.columnToLabel, + titles, + fieldFormats[layer.layerId] + ) || []; + + return { numberOfSeries: allSeries.length, allSeries }; }); const totalSeriesCount = seriesPerLayer.reduce( (sum, perLayer) => sum + perLayer.numberOfSeries, @@ -75,24 +123,19 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string) { + getRank(sortedLayer: CommonXYDataLayerConfig, seriesName: string) { const layerIndex = paletteLayers.findIndex( (layer) => sortedLayer.layerId === layer.layerId ); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; - const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); + const rank = currentSeriesPerLayer.allSeries.indexOf(seriesName); return ( (layerIndex === 0 ? 0 : seriesPerLayer .slice(0, layerIndex) .reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) + - (sortedLayer.splitAccessor && splitRank !== -1 - ? splitRank * sortedLayer.accessors.length - : 0) + - sortedLayer.accessors.findIndex( - (accessor) => getAccessorByDimension(accessor, sortedLayer.table.columns) === yAccessor - ) + (rank !== -1 ? rank : 0) ); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 151d56ed63060..bb1bdbe113dbc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -18,12 +18,9 @@ import { XYChartSeriesIdentifier, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { FieldFormat, IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { IFieldFormat } from '@kbn/field-formats-plugin/common'; import { Datatable } from '@kbn/expressions-plugin'; -import { - getFormatByAccessor, - getAccessorByDimension, -} from '@kbn/visualizations-plugin/common/utils'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; import { CommonXYDataLayerConfig, XScaleType } from '../../common'; @@ -32,7 +29,7 @@ import { FormatFactory } from '../types'; import { getSeriesColor } from './state'; import { ColorAssignments } from './color_assignment'; import { GroupsConfiguration } from './axes_configuration'; -import { LayerAccessorsTitles } from './layers'; +import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers'; import { getFormat } from './format'; type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps; @@ -53,15 +50,16 @@ type GetSeriesPropsFn = (config: { fillOpacity?: number; formattedDatatableInfo: DatatableWithFormatInfo; defaultXScaleType: XScaleType; + fieldFormats: LayersFieldFormats; }) => SeriesSpec; type GetSeriesNameFn = ( data: XYChartSeriesIdentifier, config: { - splitColumnId?: string; + splitAccessors: Array; accessorsCount: number; - splitHint: SerializedFieldFormat | undefined; - splitFormatter: FieldFormat; + columns: Datatable['columns']; + splitAccessorsFormats: LayerFieldFormats['splitSeriesAccessors']; alreadyFormattedColumns: Record; columnToLabelMap: Record; }, @@ -74,11 +72,10 @@ type GetColorFn = ( layer: CommonXYDataLayerConfig; accessor: string; colorAssignments: ColorAssignments; - columnToLabelMap: Record; paletteService: PaletteRegistry; + getSeriesNameFn: (d: XYChartSeriesIdentifier) => SeriesName; syncColors?: boolean; - }, - titles: LayerAccessorsTitles + } ) => string | null; type GetPointConfigFn = (config: { @@ -184,13 +181,16 @@ export const getFormattedTablesByLayers = ( formatFactory: FormatFactory ): DatatablesWithFormatInfo => layers.reduce( - (formattedDatatables, { layerId, table, xAccessor, splitAccessor, accessors, xScaleType }) => ({ + ( + formattedDatatables, + { layerId, table, xAccessor, splitAccessors = [], accessors, xScaleType } + ) => ({ ...formattedDatatables, [layerId]: getFormattedTable( table, formatFactory, xAccessor, - [xAccessor, splitAccessor, ...accessors].filter( + [xAccessor, ...splitAccessors, ...accessors].filter( (a): a is string | ExpressionValueVisDimension => a !== undefined ), xScaleType @@ -199,13 +199,41 @@ export const getFormattedTablesByLayers = ( {} ); -const getSeriesName: GetSeriesNameFn = ( +function getSplitValues( + splitAccessorsMap: XYChartSeriesIdentifier['splitAccessors'], + splitAccessors: Array, + alreadyFormattedColumns: Record, + columns: Datatable['columns'], + splitAccessorsFormats: LayerFieldFormats['splitSeriesAccessors'] +) { + if (splitAccessorsMap.size < 0) { + return []; + } + + return [...splitAccessorsMap].reduce>((acc, [splitAccessor, value]) => { + const split = splitAccessors.find( + (accessor) => getAccessorByDimension(accessor, columns) === splitAccessor + ); + if (split) { + const splitColumnId = getAccessorByDimension(split, columns); + const splitFormatter = splitAccessorsFormats[splitColumnId].formatter; + return [ + ...acc, + alreadyFormattedColumns[splitColumnId] ? value : splitFormatter.convert(value), + ]; + } + + return acc; + }, []); +} + +export const getSeriesName: GetSeriesNameFn = ( data, { - splitColumnId, + splitAccessors, accessorsCount, - splitHint, - splitFormatter, + columns, + splitAccessorsFormats, alreadyFormattedColumns, columnToLabelMap, }, @@ -214,40 +242,26 @@ const getSeriesName: GetSeriesNameFn = ( // For multiple y series, the name of the operation is used on each, either: // * Key - Y name // * Formatted value - Y name - if (splitColumnId && accessorsCount > 1) { - const formatted = alreadyFormattedColumns[splitColumnId]; - const result = data.seriesKeys - .map((key: string | number, i) => { - if (i === 0 && splitHint && splitColumnId && !formatted) { - return splitFormatter.convert(key); - } - return splitColumnId && i === 0 - ? key - : columnToLabelMap[key] ?? - titles?.yTitles?.[key] ?? - titles?.splitSeriesTitles?.[key] ?? - null; - }) - .join(' - '); - - return result; - } - // For formatted split series, format the key - // This handles splitting by dates, for example - if (splitHint) { - if (splitColumnId && alreadyFormattedColumns[splitColumnId]) { - return data.seriesKeys[0]; + const splitValues = getSplitValues( + data.splitAccessors, + splitAccessors, + alreadyFormattedColumns, + columns, + splitAccessorsFormats + ); + + const key = data.seriesKeys[data.seriesKeys.length - 1]; + const yAccessorTitle = columnToLabelMap[key] ?? titles?.yTitles?.[key] ?? null; + + if (accessorsCount > 1) { + if (splitValues.length === 0) { + return yAccessorTitle; } - return splitFormatter.convert(data.seriesKeys[0]); + return `${splitValues.join(' - ')}${yAccessorTitle ? ' - ' + yAccessorTitle : ''}`; } - // This handles both split and single-y cases: - // * If split series without formatting, show the value literally - // * If single Y, the seriesKey will be the accessor, so we show the human-readable name - return splitColumnId - ? data.seriesKeys[0] - : columnToLabelMap[data.seriesKeys[0]] ?? titles?.yTitles?.[data.seriesKeys[0]] ?? null; + return splitValues.length > 0 ? splitValues.join(' - ') : yAccessorTitle; }; const getPointConfig: GetPointConfigFn = ({ @@ -275,9 +289,8 @@ const getLineConfig: GetLineConfigFn = ({ showLines, lineWidth }) => ({ }); const getColor: GetColorFn = ( - { yAccessor, seriesKeys }, - { layer, accessor, colorAssignments, columnToLabelMap, paletteService, syncColors }, - titles + series, + { layer, accessor, colorAssignments, paletteService, syncColors, getSeriesNameFn } ) => { const overwriteColor = getSeriesColor(layer, accessor); if (overwriteColor !== null) { @@ -285,13 +298,13 @@ const getColor: GetColorFn = ( } const colorAssignment = colorAssignments[layer.palette.name]; + const name = getSeriesNameFn(series)?.toString() || ''; + const seriesLayers: SeriesLayer[] = [ { - name: layer.splitAccessor - ? String(seriesKeys[0]) - : columnToLabelMap[seriesKeys[0]] ?? titles?.yTitles?.[seriesKeys[0]] ?? null, + name, totalSeriesAtDepth: colorAssignment.totalSeriesCount, - rankAtDepth: colorAssignment.getRank(layer, String(seriesKeys[0]), String(yAccessor)), + rankAtDepth: colorAssignment.getRank(layer, name), }, ]; return paletteService.get(layer.palette.name).getCategoricalColor( @@ -310,27 +323,21 @@ const EMPTY_ACCESSOR = '-'; const SPLIT_CHAR = '.'; export const generateSeriesId = ( - { - layerId, - xAccessor, - splitAccessor, - }: Pick, + { layerId, xAccessor }: Pick, + splitColumnIds: string[], accessor?: string ) => - [ - layerId, - xAccessor ?? EMPTY_ACCESSOR, - accessor ?? EMPTY_ACCESSOR, - splitAccessor ?? EMPTY_ACCESSOR, - ].join(SPLIT_CHAR); + [layerId, xAccessor ?? EMPTY_ACCESSOR, accessor ?? EMPTY_ACCESSOR, ...splitColumnIds].join( + SPLIT_CHAR + ); export const getMetaFromSeriesId = (seriesId: string) => { - const [layerId, xAccessor, yAccessor, splitAccessor] = seriesId.split(SPLIT_CHAR); + const [layerId, xAccessor, yAccessor, ...splitAccessors] = seriesId.split(SPLIT_CHAR); return { layerId, xAccessor: xAccessor === EMPTY_ACCESSOR ? undefined : xAccessor, yAccessor, - splitAccessor: splitAccessor === EMPTY_ACCESSOR ? undefined : splitAccessor, + splitAccessor: splitAccessors[0] === EMPTY_ACCESSOR ? undefined : splitAccessors, }; }; @@ -350,6 +357,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ fillOpacity, formattedDatatableInfo, defaultXScaleType, + fieldFormats, }): SeriesSpec => { const { table, isStacked, markSizeAccessor } = layer; const isPercentage = layer.isPercentage; @@ -360,18 +368,16 @@ export const getSeriesProps: GetSeriesPropsFn = ({ const scaleType = yAxis?.scaleType || ScaleType.Linear; const isBarChart = layer.seriesType === SeriesTypes.BAR; const xColumnId = layer.xAccessor && getAccessorByDimension(layer.xAccessor, table.columns); - const splitColumnId = - layer.splitAccessor && getAccessorByDimension(layer.splitAccessor, table.columns); + const splitColumnIds = + layer.splitAccessors?.map((splitAccessor) => { + return getAccessorByDimension(splitAccessor, table.columns); + }) || []; const enableHistogramMode = layer.isHistogram && - (isStacked || !layer.splitAccessor) && + (isStacked || !splitColumnIds.length) && (isStacked || !isBarChart || !chartHasMoreThanOneBarSeries); const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params; - const splitHint = layer.splitAccessor - ? getFormatByAccessor(layer.splitAccessor, table.columns) - : undefined; - const splitFormatter = formatFactory(splitHint); const markSizeColumnId = markSizeAccessor ? getAccessorByDimension(markSizeAccessor, table.columns) @@ -390,11 +396,10 @@ export const getSeriesProps: GetSeriesPropsFn = ({ // To not display them in the legend, they need to be filtered out. let rows = formattedTable.rows.filter( (row) => - !(xColumnId && typeof row[xColumnId] === 'undefined') && + !(xColumnId && row[xColumnId] === undefined) && !( - splitColumnId && - typeof row[splitColumnId] === 'undefined' && - typeof row[accessor] === 'undefined' + splitColumnIds.some((splitColumnId) => row[splitColumnId] === undefined) && + row[accessor] === undefined ) ); @@ -407,10 +412,29 @@ export const getSeriesProps: GetSeriesPropsFn = ({ })); } + const getSeriesNameFn = (d: XYChartSeriesIdentifier) => { + return getSeriesName( + d, + { + splitAccessors: layer.splitAccessors || [], + accessorsCount: layer.accessors.length, + alreadyFormattedColumns: formattedColumns, + columns: formattedTable.columns, + splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, + columnToLabelMap, + }, + titles + ); + }; + return { - splitSeriesAccessors: splitColumnId ? [splitColumnId] : [], + splitSeriesAccessors: splitColumnIds.length ? splitColumnIds : [], stackAccessors: isStacked ? [layer.xAccessor as string] : [], - id: generateSeriesId(layer, accessor), + id: generateSeriesId( + layer, + splitColumnIds.length ? splitColumnIds : [EMPTY_ACCESSOR], + accessor + ), xAccessor: xColumnId || 'unifiedX', yAccessors: [accessor], markSizeAccessor: markSizeColumnId, @@ -422,18 +446,14 @@ export const getSeriesProps: GetSeriesPropsFn = ({ ? ScaleType.LinearBinary : scaleType, color: (series) => - getColor( - series, - { - layer, - accessor, - colorAssignments, - columnToLabelMap, - paletteService, - syncColors, - }, - titles - ), + getColor(series, { + layer, + accessor, + colorAssignments, + paletteService, + getSeriesNameFn, + syncColors, + }), groupId: yAxis?.groupId, enableHistogramMode, stackMode, @@ -467,18 +487,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ line: getLineConfig({ lineWidth: layer.lineWidth, showLines: layer.showLines }), }, name(d) { - return getSeriesName( - d, - { - splitColumnId, - accessorsCount: layer.accessors.length, - splitHint, - splitFormatter, - alreadyFormattedColumns: formattedColumns, - columnToLabelMap, - }, - titles - ); + return getSeriesNameFn(d); }, }; }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts index eba7cf7c8a9f3..359f1c27879a9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts @@ -7,7 +7,11 @@ */ import { Datatable } from '@kbn/expressions-plugin/common'; -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { + FieldFormat, + FormatFactory, + SerializedFieldFormat, +} from '@kbn/field-formats-plugin/common'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { getAccessorByDimension, @@ -34,11 +38,15 @@ interface SplitAccessors { } export type AccessorsFieldFormats = Record; +export type SplitAccessorsFieldFormats = Record< + string, + { format: SerializedFieldFormat | undefined; formatter: FieldFormat } +>; export interface LayerFieldFormats { xAccessors: AccessorsFieldFormats; yAccessors: AccessorsFieldFormats; - splitSeriesAccessors: AccessorsFieldFormats; + splitSeriesAccessors: SplitAccessorsFieldFormats; splitColumnAccessors: AccessorsFieldFormats; splitRowAccessors: AccessorsFieldFormats; } @@ -63,7 +71,7 @@ export function getFilteredLayers(layers: CommonXYLayerConfig[]) { let table: Datatable | undefined; let accessors: Array = []; let xAccessor: undefined | string | number; - let splitAccessor: undefined | string | number; + let splitAccessors: string[] = []; if (isDataLayer(layer) || isReferenceLayer(layer)) { table = layer.table; @@ -73,22 +81,24 @@ export function getFilteredLayers(layers: CommonXYLayerConfig[]) { if (isDataLayer(layer)) { xAccessor = layer.xAccessor && table && getAccessorByDimension(layer.xAccessor, table.columns); - splitAccessor = - layer.splitAccessor && - table && - getAccessorByDimension(layer.splitAccessor, table.columns); + splitAccessors = table + ? layer.splitAccessors?.map((splitAccessor) => + getAccessorByDimension(splitAccessor, table!.columns) + ) || [] + : []; } return !( !accessors.length || !table || table.rows.length === 0 || - (xAccessor && - table.rows.every((row) => xAccessor && typeof row[xAccessor] === 'undefined')) || + (xAccessor && table.rows.every((row) => xAccessor && row[xAccessor] === undefined)) || // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty (!xAccessor && - splitAccessor && - table.rows.every((row) => splitAccessor && typeof row[splitAccessor] === 'undefined')) + splitAccessors.length && + table.rows.every((row) => + splitAccessors.every((splitAccessor) => row[splitAccessor] === undefined) + )) ); } ); @@ -125,10 +135,12 @@ const getYAccessorWithFieldFormat = ( }; export const getLayerFormats = ( - { xAccessor, accessors, splitAccessor, table, isPercentage }: CommonXYDataLayerConfig, - { splitColumnAccessor, splitRowAccessor }: SplitAccessors + { xAccessor, accessors, splitAccessors = [], table, isPercentage }: CommonXYDataLayerConfig, + { splitColumnAccessor, splitRowAccessor }: SplitAccessors, + formatFactory: FormatFactory ): LayerFieldFormats => { const yAccessors: Array = accessors; + const splitColumnAccessors: Array = splitAccessors; return { xAccessors: getAccessorWithFieldFormat(xAccessor, table.columns), yAccessors: yAccessors.reduce( @@ -138,7 +150,17 @@ export const getLayerFormats = ( }), {} ), - splitSeriesAccessors: getAccessorWithFieldFormat(splitAccessor, table.columns), + splitSeriesAccessors: splitColumnAccessors?.reduce((formatters, splitAccessor) => { + const formatObj = getAccessorWithFieldFormat(splitAccessor, table.columns); + const accessor = Object.keys(formatObj)[0]; + return { + ...formatters, + [accessor]: { + format: formatObj[accessor], + formatter: formatFactory(formatObj[accessor]), + }, + }; + }, {}), splitColumnAccessors: getAccessorWithFieldFormat(splitColumnAccessor, table.columns), splitRowAccessors: getAccessorWithFieldFormat(splitRowAccessor, table.columns), }; @@ -146,12 +168,13 @@ export const getLayerFormats = ( export const getLayersFormats = ( layers: CommonXYDataLayerConfig[], - splitAccessors: SplitAccessors + splitAccessors: SplitAccessors, + formatFactory: FormatFactory ): LayersFieldFormats => layers.reduce( (formatters, layer) => ({ ...formatters, - [layer.layerId]: getLayerFormats(layer, splitAccessors), + [layer.layerId]: getLayerFormats(layer, splitAccessors, formatFactory), }), {} ); @@ -171,7 +194,7 @@ const getTitleForYAccessor = ( }; export const getLayerTitles = ( - { xAccessor, accessors, splitAccessor, table, layerId }: CommonXYDataLayerConfig, + { xAccessor, accessors, splitAccessors = [], table, layerId }: CommonXYDataLayerConfig, { splitColumnAccessor, splitRowAccessor }: SplitAccessors, { xTitle }: CustomTitles, groups: GroupsConfiguration @@ -191,6 +214,7 @@ export const getLayerTitles = ( const xColumnId = xAccessor && getAccessorByDimension(xAccessor, table.columns); const yColumnIds = accessors.map((a) => a && getAccessorByDimension(a, table.columns)); + const splitColumnAccessors: Array = splitAccessors; return { xTitles: xTitle && xColumnId ? { [xColumnId]: xTitle } : mapTitle(xColumnId), @@ -198,7 +222,13 @@ export const getLayerTitles = ( (titles, yAccessor) => ({ ...titles, ...(yAccessor ? getYTitle(yAccessor) : {}) }), {} ), - splitSeriesTitles: mapTitle(splitAccessor), + splitSeriesTitles: splitColumnAccessors.reduce( + (titles, splitAccessor) => ({ + ...titles, + ...(splitAccessor ? mapTitle(splitAccessor) : {}), + }), + {} + ), splitColumnTitles: mapTitle(splitColumnAccessor), splitRowTitles: mapTitle(splitRowAccessor), }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts index a705dab47dc43..f1b8295ff005c 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -19,7 +19,7 @@ export function isHorizontalChart(layers: CommonXYLayerConfig[]) { export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => { if ( - (isDataLayer(layer) && layer.splitAccessor) || + (isDataLayer(layer) && layer.splitAccessors) || isAnnotationsLayer(layer) || isReferenceLine(layer) ) { diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 548274f463323..43f4082d00dbe 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -67,7 +67,7 @@ Object { "seriesType": Array [ "area", ], - "splitAccessor": Array [ + "splitAccessors": Array [ "d", ], "table": Array [ diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 1b9ea44fac496..36c00ed55e743 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -462,7 +462,7 @@ const dataLayerToExpression = ( isPercentage: isPercentage ? [isPercentage] : [], isStacked: isStacked ? [isStacked] : [], isHorizontal: isHorizontal ? [isHorizontal] : [], - splitAccessor: layer.collapseFn || !layer.splitAccessor ? [] : [layer.splitAccessor], + splitAccessors: layer.collapseFn || !layer.splitAccessor ? [] : [layer.splitAccessor], decorations: layer.yConfig ? layer.yConfig.map((yConfig) => yConfigToDataDecorationConfigExpression(yConfig, yAxisConfigs)