diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts index 5d9d7fb70d478..8116f0146f19a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/constants.ts +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -35,6 +35,8 @@ export const FittingFunctions = { LINEAR: 'Linear', CARRY: 'Carry', LOOKAHEAD: 'Lookahead', + AVERAGE: 'Average', + NEAREST: 'Nearest', } as const; export const EndValues = { @@ -60,6 +62,7 @@ export const LineStyles = { SOLID: 'solid', DASHED: 'dashed', DOTTED: 'dotted', + DOT_DASHED: 'dot-dashed', } as const; export const FillStyles = { @@ -98,6 +101,7 @@ export const XScaleTypes = { export const XYCurveTypes = { LINEAR: 'LINEAR', CURVE_MONOTONE_X: 'CURVE_MONOTONE_X', + CURVE_STEP_AFTER: 'CURVE_STEP_AFTER', } as const; export const ValueLabelModes = { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts index c1a4070225a64..613b631d8c9c4 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts @@ -53,6 +53,12 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition< defaultMessage: 'Upper bound', }), }, + enforce: { + types: ['boolean'], + help: i18n.translate('expressionXY.axisExtentConfig.enforce.help', { + defaultMessage: 'Enforce extent params.', + }), + }, }, fn(input, args) { if (args.mode === AxisExtentModes.CUSTOM) { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 0c173ea0754f8..10f6d5d748b23 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -7,7 +7,7 @@ */ import { ArgumentType } from '@kbn/expressions-plugin/common'; -import { SeriesTypes, XScaleTypes, DATA_DECORATION_CONFIG } from '../constants'; +import { SeriesTypes, XScaleTypes, DATA_DECORATION_CONFIG, XYCurveTypes } from '../constants'; import { strings } from '../i18n'; import { DataLayerArgs, ExtendedDataLayerArgs } from '../types'; @@ -58,6 +58,12 @@ export const commonDataLayerArgs: Omit< default: false, help: strings.getIsHorizontalHelp(), }, + curveType: { + types: ['string'], + options: [...Object.values(XYCurveTypes)], + help: strings.getCurveTypeHelp(), + strict: true, + }, lineWidth: { types: ['number'], help: strings.getLineWidthHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts index df9c4abdfe22e..799dc12b1ea5b 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -11,7 +11,6 @@ import { FittingFunctions, LEGEND_CONFIG, ValueLabelModes, - XYCurveTypes, X_AXIS_CONFIG, Y_AXIS_CONFIG, } from '../constants'; @@ -50,12 +49,6 @@ export const commonXYArgs: CommonXYFn['args'] = { strict: true, default: ValueLabelModes.HIDE, }, - curveType: { - types: ['string'], - options: [...Object.values(XYCurveTypes)], - help: strings.getCurveTypeHelp(), - strict: true, - }, fillOpacity: { types: ['number'], help: strings.getFillOpacityHelp(), 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 17c6485c711da..d0d65cc72732d 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 @@ -20,27 +20,23 @@ export const extendedDataLayerFunction: ExtendedDataLayerFn = { args: { ...commonDataLayerArgs, xAccessor: { - types: ['string'], + types: ['vis_dimension', 'string'], help: strings.getXAccessorHelp(), }, splitAccessors: { - types: ['string'], + types: ['vis_dimension', 'string'], help: strings.getSplitAccessorHelp(), multi: true, }, accessors: { - types: ['string'], + types: ['vis_dimension', 'string'], help: strings.getAccessorsHelp(), multi: true, }, markSizeAccessor: { - types: ['string'], + types: ['vis_dimension', 'string'], help: strings.getMarkSizeAccessorHelp(), }, - table: { - types: ['datatable'], - help: strings.getTableHelp(), - }, layerId: { types: ['string'], help: strings.getLayerIdHelp(), 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 16905f96f9c2f..79612971e841b 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 @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; import { ExtendedDataLayerArgs, ExtendedDataLayerFn } from '../types'; import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants'; @@ -19,8 +20,11 @@ import { } from './validate'; export const extendedDataLayerFn: ExtendedDataLayerFn['fn'] = async (data, args, context) => { - const table = args.table ?? data; - const accessors = getAccessors(args, table); + const table = data; + const accessors = getAccessors( + args, + table + ); validateAccessor(accessors.xAccessor, table.columns); accessors.splitAccessors?.forEach((accessor) => validateAccessor(accessor, table.columns)); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts index f419891e079ea..392c9a0d5830a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts @@ -13,6 +13,7 @@ import { REFERENCE_LINE_LAYER, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, + REFERENCE_LINE, } from '../constants'; import { commonXYArgs } from './common_xy_args'; import { strings } from '../i18n'; @@ -25,12 +26,27 @@ export const layeredXyVisFunction: LayeredXyVisFn = { args: { ...commonXYArgs, layers: { - types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER], + types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER, REFERENCE_LINE], help: i18n.translate('expressionXY.layeredXyVis.layers.help', { defaultMessage: 'Layers of visual series', }), multi: true, }, + splitColumnAccessor: { + types: ['vis_dimension', 'string'], + help: strings.getSplitColumnAccessorHelp(), + }, + splitRowAccessor: { + types: ['vis_dimension', 'string'], + help: strings.getSplitRowAccessorHelp(), + }, + singleTable: { + types: ['boolean'], + help: i18n.translate('expressionXY.layeredXyVis.singleTable.help', { + defaultMessage: 'All layers use the one datatable', + }), + default: false, + }, }, async fn(data, args, handlers) { const { layeredXyVisFn } = await import('./layered_xy_vis_fn'); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts index 9e1ef1cc0cb9f..50549d5f8ac1c 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -8,7 +8,7 @@ import { XY_VIS_RENDERER } from '../constants'; import { LayeredXyVisFn } from '../types'; -import { logDatatables } from '../utils'; +import { logDatatables, logDatatable } from '../utils'; import { validateMarkSizeRatioLimits, validateAddTimeMarker, @@ -21,10 +21,14 @@ import { appendLayerIds, getDataLayers } from '../helpers'; export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => { const layers = appendLayerIds(args.layers ?? [], 'layers'); + const dataLayers = getDataLayers(layers); - logDatatables(layers, handlers); + if (args.singleTable) { + logDatatable(data, layers, handlers, args.splitColumnAccessor, args.splitRowAccessor); + } else { + logDatatables(layers, handlers, args.splitColumnAccessor, args.splitRowAccessor); + } - const dataLayers = getDataLayers(layers); const hasBar = hasBarLayer(dataLayers); validateAddTimeMarker(dataLayers, args.addTimeMarker); validateMarkSizeRatioLimits(args.markSizeRatio); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index 9b8183abfa205..6c88d0ff6a62c 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -171,7 +171,7 @@ export const validateExtents = ( xAxisConfig?: XAxisConfigResult ) => { yAxisConfigs?.forEach((axis) => { - if (!axis.extent) { + if (!axis.extent || axis.extent.enforce) { return; } if ( 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 789b7fb9ceb63..2808b861c6df8 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 @@ -6,17 +6,12 @@ * Side Public License, v 1. */ -import { - Dimension, - prepareLogTable, - validateAccessor, -} from '@kbn/visualizations-plugin/common/utils'; +import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; import type { Datatable } from '@kbn/expressions-plugin/common'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; -import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER, REFERENCE_LINE } from '../constants'; +import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants'; import { appendLayerIds, getAccessors, getShowLines, normalizeTable } from '../helpers'; import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types'; -import { getLayerDimensions } from '../utils'; import { hasAreaLayer, hasBarLayer, @@ -35,6 +30,7 @@ import { validateLinesVisibilityForChartType, validateAxes, } from './validate'; +import { logDatatable } from '../utils'; const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => { const accessors = getAccessors(args, table); @@ -108,21 +104,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { ...appendLayerIds(annotationLayers, 'annotationLayers'), ]; - if (handlers.inspectorAdapters.tables) { - handlers.inspectorAdapters.tables.reset(); - handlers.inspectorAdapters.tables.allowCsvExport = true; - - const layerDimensions = layers.reduce((dimensions, layer) => { - if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { - return dimensions; - } - - return [...dimensions, ...getLayerDimensions(layer)]; - }, []); - - const logTable = prepareLogTable(data, layerDimensions, true); - handlers.inspectorAdapters.tables.logDatatable('default', logTable); - } + logDatatable(data, layers, handlers, args.splitColumnAccessor, args.splitRowAccessor); const hasBar = hasBarLayer(dataLayers); const hasArea = hasAreaLayer(dataLayers); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/table.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/table.ts index 65cae152c0caf..09171f8f72ff5 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/table.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/table.ts @@ -15,7 +15,10 @@ export function normalizeTable(data: Datatable, xAccessor?: string | ExpressionV const xColumn = xAccessor && getColumnByAccessor(xAccessor, data.columns); if (xColumn && xColumn?.meta.type === 'date') { const xColumnId = xColumn.id; - if (!data.rows.some((row) => typeof row[xColumnId] === 'string')) return data; + if ( + !data.rows.some((row) => typeof row[xColumnId] === 'string' && row[xColumnId] !== '__other__') + ) + return data; const rows = data.rows.map((row) => { return typeof row[xColumnId] !== 'string' ? row diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index 75a523ab41d92..1e9df3c3ffe64 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -25,6 +25,18 @@ export const strings = { i18n.translate('expressionXY.xyVis.logDatatable.breakDown', { defaultMessage: 'Break down by', }), + getSplitRowHelp: () => + i18n.translate('expressionXY.xyVis.logDatatable.splitRow', { + defaultMessage: 'Split rows by', + }), + getSplitColumnHelp: () => + i18n.translate('expressionXY.xyVis.logDatatable.splitColumn', { + defaultMessage: 'Split columns by', + }), + getMarkSizeHelp: () => + i18n.translate('expressionXY.xyVis.logDatatable.markSize', { + defaultMessage: 'Mark size', + }), getReferenceLineHelp: () => i18n.translate('expressionXY.xyVis.logDatatable.breakDown', { defaultMessage: 'Break down by', 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 12727973ea4ed..0970cec985d30 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 @@ -71,6 +71,7 @@ export interface AxisExtentConfig { mode: AxisExtentMode; lowerBound?: number; upperBound?: number; + enforce?: boolean; } export interface AxisConfig { @@ -130,6 +131,7 @@ export interface DataLayerArgs { isHorizontal: boolean; palette: PaletteOutput; decorations?: DataDecorationConfigResult[]; + curveType?: XYCurveType; } export interface ValidLayer extends DataLayerConfigResult { @@ -138,12 +140,12 @@ export interface ValidLayer extends DataLayerConfigResult { export interface ExtendedDataLayerArgs { layerId?: string; - accessors: string[]; + accessors: Array; seriesType: SeriesType; - xAccessor?: string; + xAccessor?: string | ExpressionValueVisDimension; simpleView?: boolean; - splitAccessors?: string[]; - markSizeAccessor?: string; + splitAccessors?: Array; + markSizeAccessor?: string | ExpressionValueVisDimension; lineWidth?: number; showPoints?: boolean; showLines?: boolean; @@ -157,7 +159,7 @@ export interface ExtendedDataLayerArgs { palette: PaletteOutput; // palette will always be set on the expression decorations?: DataDecorationConfigResult[]; - table?: Datatable; + curveType?: XYCurveType; } export interface LegendConfig { @@ -215,7 +217,6 @@ export interface XYArgs extends DataLayerArgs { referenceLines: ReferenceLineConfigResult[]; annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; - curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; @@ -239,7 +240,6 @@ export interface LayeredXYArgs { valueLabels: ValueLabelMode; layers?: XYExtendedLayerConfigResult[]; fittingFunction?: FittingFunction; - curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; @@ -252,6 +252,9 @@ export interface LayeredXYArgs { minTimeBarInterval?: string; orderBucketsBySum?: boolean; showTooltip: boolean; + splitRowAccessor?: ExpressionValueVisDimension | string; + splitColumnAccessor?: ExpressionValueVisDimension | string; + singleTable?: boolean; } export interface XYProps { @@ -261,7 +264,6 @@ export interface XYProps { valueLabels: ValueLabelMode; layers: CommonXYLayerConfig[]; fittingFunction?: FittingFunction; - curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; @@ -276,6 +278,7 @@ export interface XYProps { detailedTooltip?: boolean; orderBucketsBySum?: boolean; showTooltip: boolean; + singleTable?: boolean; } export interface AnnotationLayerArgs { @@ -317,12 +320,14 @@ export type XYLayerConfig = DataLayerConfig | ReferenceLineConfig | AnnotationLa export type XYExtendedLayerConfig = | ExtendedDataLayerConfig | ReferenceLineLayerConfig - | ExtendedAnnotationLayerConfig; + | ExtendedAnnotationLayerConfig + | ReferenceLineConfig; export type XYExtendedLayerConfigResult = | ExtendedDataLayerConfigResult | ReferenceLineLayerConfigResult - | ExtendedAnnotationLayerConfigResult; + | ExtendedAnnotationLayerConfigResult + | ReferenceLineConfigResult; export interface ExtendedReferenceLineDecorationConfig extends ReferenceLineArgs { type: typeof EXTENDED_REFERENCE_LINE_DECORATION_CONFIG; diff --git a/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx b/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx index ba40d5768f50c..b367b14b0e117 100644 --- a/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { logDatatables, getLayerDimensions } from './log_datatables'; +export { logDatatables, logDatatable, getLayerDimensions } from './log_datatables'; 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 d9bbd56f6f323..5b04b1bdf0d42 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 @@ -6,13 +6,19 @@ * Side Public License, v 1. */ -import { ExecutionContext } from '@kbn/expressions-plugin/common'; +import { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; import { LayerTypes, REFERENCE_LINE } from '../constants'; import { strings } from '../i18n'; import { CommonXYDataLayerConfig, CommonXYLayerConfig, ReferenceLineLayerConfig } from '../types'; -export const logDatatables = (layers: CommonXYLayerConfig[], handlers: ExecutionContext) => { +export const logDatatables = ( + layers: CommonXYLayerConfig[], + handlers: ExecutionContext, + splitColumnAccessor?: string | ExpressionValueVisDimension, + splitRowAccessor?: string | ExpressionValueVisDimension +) => { if (!handlers?.inspectorAdapters?.tables) { return; } @@ -25,19 +31,65 @@ export const logDatatables = (layers: CommonXYLayerConfig[], handlers: Execution return; } - const logTable = prepareLogTable(layer.table, getLayerDimensions(layer), true); + const layerDimensions = getLayerDimensions(layer); + + layerDimensions.push([ + splitColumnAccessor ? [splitColumnAccessor] : undefined, + strings.getSplitColumnHelp(), + ]); + layerDimensions.push([ + splitRowAccessor ? [splitRowAccessor] : undefined, + strings.getSplitRowHelp(), + ]); + + const logTable = prepareLogTable(layer.table, layerDimensions, true); handlers.inspectorAdapters.tables.logDatatable(layer.layerId, logTable); }); }; +export const logDatatable = ( + data: Datatable, + layers: CommonXYLayerConfig[], + handlers: ExecutionContext, + splitColumnAccessor?: string | ExpressionValueVisDimension, + splitRowAccessor?: string | ExpressionValueVisDimension +) => { + if (handlers.inspectorAdapters.tables) { + handlers.inspectorAdapters.tables.reset(); + handlers.inspectorAdapters.tables.allowCsvExport = true; + + const layerDimensions = layers.reduce((dimensions, layer) => { + if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { + return dimensions; + } + + return [...dimensions, ...getLayerDimensions(layer)]; + }, []); + + layerDimensions.push([ + splitColumnAccessor ? [splitColumnAccessor] : undefined, + strings.getSplitColumnHelp(), + ]); + layerDimensions.push([ + splitRowAccessor ? [splitRowAccessor] : undefined, + strings.getSplitRowHelp(), + ]); + + const logTable = prepareLogTable(data, layerDimensions, true); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } +}; + export const getLayerDimensions = ( layer: CommonXYDataLayerConfig | ReferenceLineLayerConfig ): Dimension[] => { let xAccessor; let splitAccessors; + let markSizeAccessor; if (layer.layerType === LayerTypes.DATA) { xAccessor = layer.xAccessor; splitAccessors = layer.splitAccessors; + markSizeAccessor = layer.markSizeAccessor; } const { accessors, layerType } = layer; @@ -48,5 +100,6 @@ export const getLayerDimensions = ( ], [xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()], [splitAccessors ? splitAccessors : undefined, strings.getBreakdownHelp()], + [markSizeAccessor ? [markSizeAccessor] : undefined, strings.getMarkSizeHelp()], ]; }; 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 24c7d68cd6fda..e92644d7d2fb5 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 @@ -281,3014 +281,937 @@ Array [ `; exports[`XYChart component it renders area 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - - -`; - -exports[`XYChart component it renders bar 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - - -`; - -exports[`XYChart component it renders horizontal bar 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={90} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - - -`; - -exports[`XYChart component it renders line 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ Object { - "type": "return", - "value": Object { - "convert": [MockFunction] { - "calls": Array [ - Array [ - 1652034840000, - ], - Array [ - 1652122440000, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": 1652034840000, - }, - Object { - "type": "return", - "value": 1652122440000, - }, - ], - }, + "background": Object { + "color": undefined, }, - }, - Object { - "type": "return", - "value": Object { - "convert": [MockFunction] { - "calls": Array [ - Array [ - 1652034840000, - ], - Array [ - 1652122440000, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": 1652034840000, - }, - Object { - "type": "return", - "value": 1652122440000, - }, - ], + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - }, + "markSizeRatio": undefined, + } + } + tooltip={ Object { - "type": "return", - "value": Object { - "convert": [MockFunction] { - "calls": Array [ - Array [ - 1652034840000, - ], - Array [ - 1652122440000, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": 1652034840000, - }, - Object { - "type": "return", - "value": 1652122440000, - }, - ], - }, + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + - -`; - -exports[`XYChart component it renders stacked area 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - + + + +`; + +exports[`XYChart component it renders bar 1`] = ` +
+ + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - }, + "markSizeRatio": undefined, + } + } + tooltip={ Object { - "type": "return", - "value": Object { - "convert": [MockFunction] { - "calls": Array [ - Array [ - 1652034840000, - ], - Array [ - 1652122440000, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": 1652034840000, - }, - Object { - "type": "return", - "value": 1652122440000, - }, - ], - }, + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + - -`; - -exports[`XYChart component it renders stacked bar 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - + + +
+`; + +exports[`XYChart component it renders horizontal bar 1`] = ` +
+ + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={90} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + + +
+`; + +exports[`XYChart component it renders line 1`] = ` +
+ - -`; - -exports[`XYChart component it renders stacked horizontal bar 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={90} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - - - + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + + +
+`; + +exports[`XYChart component it renders stacked area 1`] = ` +
+ - -`; - -exports[`XYChart component split chart should render split chart if both, splitRowAccessor and splitColumnAccessor are specified 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + - - - + + +
+`; + +exports[`XYChart component it renders stacked bar 1`] = ` +
+ + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + + +
+`; + +exports[`XYChart component it renders stacked horizontal bar 1`] = ` +
+ - -`; - -exports[`XYChart component split chart should render split chart if splitColumnAccessor is specified 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={90} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + - - - + + +
+`; + +exports[`XYChart component split chart should render split chart if both, splitRowAccessor and splitColumnAccessor are specified 1`] = ` +
+ + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + + + +
+`; + +exports[`XYChart component split chart should render split chart if splitColumnAccessor is specified 1`] = ` +
+ - -`; - -exports[`XYChart component split chart should render split chart if splitRowAccessor is specified 1`] = ` - - - } - onBrushEnd={[Function]} - onElementClick={[Function]} - onPointerUpdate={[Function]} - onRenderChange={[Function]} - rotation={0} - showLegend={false} - showLegendExtra={false} - theme={ - Object { - "background": Object { - "color": undefined, - }, - "barSeriesStyle": Object {}, - "chartMargins": Object {}, - "legend": Object { - "labelOptions": Object { - "maxLines": 0, - }, - }, - "markSizeRatio": undefined, - } - } - tooltip={ - Object { - "boundary": undefined, - "customTooltip": undefined, - "headerFormatter": [Function], - "type": "vertical", - } - } - /> - - - + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - ], - Array [ - Object { - "id": "string", + "markSizeRatio": undefined, + } + } + tooltip={ + Object { + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + - - - + + +
+`; + +exports[`XYChart component split chart should render split chart if splitRowAccessor is specified 1`] = ` +
+ + + + } + onBrushEnd={[Function]} + onElementClick={[Function]} + onPointerUpdate={[Function]} + onRenderChange={[Function]} + rotation={0} + showLegend={false} + showLegendExtra={false} + theme={ + Object { + "background": Object { + "color": undefined, + }, + "barSeriesStyle": Object {}, + "chartMargins": Object {}, + "legend": Object { + "labelOptions": Object { + "maxLines": 0, }, }, - }, + "markSizeRatio": undefined, + } + } + tooltip={ Object { - "type": "return", - "value": Object { - "convert": [MockFunction] { - "calls": Array [ - Array [ - 1652034840000, - ], - Array [ - 1652122440000, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": 1652034840000, - }, - Object { - "type": "return", - "value": 1652122440000, + "boundary": undefined, + "customTooltip": undefined, + "headerFormatter": [Function], + "type": "vertical", + } + } + /> + + + + + + - + ] + } + /> + + +
`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index 35e4c0c7f1abb..035faf8d22b00 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -16,13 +16,12 @@ import React, { FC } from 'react'; import { PaletteRegistry } from '@kbn/coloring'; import { FormatFactory } from '@kbn/field-formats-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; - +import { PersistedState } from '@kbn/visualizations-plugin/public'; import { CommonXYDataLayerConfig, EndValue, FittingFunction, ValueLabelMode, - XYCurveType, XScaleType, } from '../../common'; import { SeriesTypes, ValueLabelModes, AxisModes } from '../../common/constants'; @@ -42,7 +41,6 @@ interface Props { formatFactory: FormatFactory; chartHasMoreThanOneBarSeries?: boolean; yAxesConfiguration: GroupsConfiguration; - curveType?: XYCurveType; fittingFunction?: FittingFunction; endValue?: EndValue | undefined; paletteService: PaletteRegistry; @@ -55,6 +53,8 @@ interface Props { valueLabels: ValueLabelMode; defaultXScaleType: XScaleType; fieldFormats: LayersFieldFormats; + uiState?: PersistedState; + singleTable?: boolean; } export const DataLayers: FC = ({ @@ -62,7 +62,6 @@ export const DataLayers: FC = ({ layers, endValue, timeZone, - curveType, syncColors, valueLabels, fillOpacity, @@ -76,14 +75,57 @@ export const DataLayers: FC = ({ chartHasMoreThanOneBarSeries, defaultXScaleType, fieldFormats, + uiState, + singleTable, }) => { - const colorAssignments = getColorAssignments(layers, titles, fieldFormats, formattedDatatables); + // for singleTable mode we should use y accessors from all layers for creating correct series name and getting color + const allYAccessors = layers.flatMap((layer) => layer.accessors); + const allColumnsToLabel = layers.reduce((acc, layer) => { + if (layer.columnToLabel) { + return { ...acc, ...JSON.parse(layer.columnToLabel) }; + } + + return acc; + }, {}); + const allYTitles = Object.keys(titles).reduce((acc, key) => { + if (titles[key].yTitles) { + return { ...acc, ...titles[key].yTitles }; + } + return acc; + }, {}); + const colorAssignments = singleTable + ? getColorAssignments( + [ + { + ...layers[0], + layerId: 'commonLayerId', + accessors: allYAccessors, + columnToLabel: JSON.stringify(allColumnsToLabel), + }, + ], + { commonLayerId: { ...titles, yTitles: allYTitles } }, + { commonLayerId: fieldFormats[layers[0].layerId] }, + { commonLayerId: formattedDatatables[layers[0].layerId] } + ) + : getColorAssignments(layers, titles, fieldFormats, formattedDatatables); return ( <> - {layers.flatMap((layer) => - layer.accessors.map((accessor, accessorIndex) => { - const { seriesType, columnToLabel, layerId, table } = layer; - const yColumnId = getAccessorByDimension(accessor, table.columns); + {layers.flatMap((layer) => { + const yPercentileAccessors: string[] = []; + const yAccessors: string[] = []; + layer.accessors.forEach((accessor) => { + const columnId = getAccessorByDimension(accessor, layer.table.columns); + if (columnId.includes('.')) { + yPercentileAccessors.push(columnId); + } else { + yAccessors.push(columnId); + } + }); + return ( + yPercentileAccessors.length ? [...yAccessors, yPercentileAccessors] : [...yAccessors] + ).map((accessor, accessorIndex) => { + const { seriesType, columnToLabel, layerId } = layer; + const yColumnId = Array.isArray(accessor) ? accessor[0] : accessor; const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; @@ -104,7 +146,7 @@ export const DataLayers: FC = ({ const seriesProps = getSeriesProps({ layer, titles: titles[layer.layerId], - accessor: yColumnId, + accessor, chartHasMoreThanOneBarSeries, colorAssignments, formatFactory, @@ -118,11 +160,14 @@ export const DataLayers: FC = ({ fillOpacity, defaultXScaleType, fieldFormats, + uiState, + allYAccessors, + singleTable, }); const index = `${layer.layerId}-${accessorIndex}`; - const curve = curveType ? CurveType[curveType] : undefined; + const curve = layer.curveType ? CurveType[layer.curveType] : undefined; switch (seriesType) { case SeriesTypes.LINE: @@ -161,8 +206,8 @@ export const DataLayers: FC = ({ /> ); } - }) - )} + }); + })} ); }; 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 e27bb716c35c7..0d1d3b5b59256 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 @@ -24,7 +24,8 @@ export const getLegendAction = ( onFilter: (data: FilterEvent['data']) => void, fieldFormats: LayersFieldFormats, formattedDatatables: DatatablesWithFormatInfo, - titles: LayersAccessorsTitles + titles: LayersAccessorsTitles, + singleTable?: boolean ): LegendAction => React.memo(({ series: [xySeries] }) => { const series = xySeries as XYChartSeriesIdentifier; @@ -35,6 +36,7 @@ export const getLegendAction = ( ) ) ); + const allYAccessors = dataLayers.flatMap((dataLayer) => dataLayer.accessors); if (layerIndex === -1) { return null; @@ -78,7 +80,7 @@ export const getLegendAction = ( series, { splitAccessors: layer.splitAccessors, - accessorsCount: layer.accessors.length, + accessorsCount: singleTable ? allYAccessors.length : layer.accessors.length, columns: table.columns, splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, alreadyFormattedColumns: formattedDatatables[layer.layerId].formattedColumns, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx new file mode 100644 index 0000000000000..3573fc65e5288 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { createContext, useCallback, useContext } from 'react'; +import { LegendColorPicker, Position, XYChartSeriesIdentifier } from '@elastic/charts'; +import { PopoverAnchorPosition, EuiWrappingPopover, EuiOutsideClickDetector } from '@elastic/eui'; +import type { PersistedState } from '@kbn/visualizations-plugin/public'; +import { ColorPicker } from '@kbn/charts-plugin/public'; +import { + DatatablesWithFormatInfo, + getMetaFromSeriesId, + getSeriesName, + LayersAccessorsTitles, + LayersFieldFormats, +} from '../helpers'; +import type { CommonXYDataLayerConfig } from '../../common/types'; + +const KEY_CODE_ENTER = 13; + +function getAnchorPosition(legendPosition: Position): PopoverAnchorPosition { + switch (legendPosition) { + case Position.Bottom: + return 'upCenter'; + case Position.Top: + return 'downCenter'; + case Position.Left: + return 'rightCenter'; + default: + return 'leftCenter'; + } +} + +export interface LegendColorPickerWrapperContextType { + legendPosition: Position; + setColor: (newColor: string | null, seriesKey: string | number) => void; + uiState?: PersistedState; + dataLayers: CommonXYDataLayerConfig[]; + formattedDatatables: DatatablesWithFormatInfo; + titles: LayersAccessorsTitles; + fieldFormats: LayersFieldFormats; + singleTable?: boolean; +} + +export const LegendColorPickerWrapperContext = createContext< + LegendColorPickerWrapperContextType | undefined +>(undefined); + +export const LegendColorPickerWrapper: LegendColorPicker = ({ + anchor, + color, + onClose, + onChange, + seriesIdentifiers: [seriesIdentifier], +}) => { + const colorPickerWrappingContext = useContext(LegendColorPickerWrapperContext); + const handleOutsideClick = useCallback(() => { + onClose?.(); + }, [onClose]); + + if (!colorPickerWrappingContext) { + return null; + } + + const { + legendPosition, + setColor, + uiState, + dataLayers, + titles, + formattedDatatables, + fieldFormats, + singleTable, + } = colorPickerWrappingContext; + const { layerId } = getMetaFromSeriesId(seriesIdentifier.specId); + + const layer = dataLayers.find((dataLayer) => dataLayer.layerId === layerId); + const allYAccessors = dataLayers.flatMap((dataLayer) => dataLayer.accessors); + const seriesName = layer + ? getSeriesName( + seriesIdentifier as XYChartSeriesIdentifier, + { + splitAccessors: layer.splitAccessors ?? [], + accessorsCount: singleTable ? allYAccessors.length : layer.accessors.length, + columns: formattedDatatables[layer.layerId].table.columns, + splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, + alreadyFormattedColumns: formattedDatatables[layer.layerId].formattedColumns, + columnToLabelMap: layer.columnToLabel ? JSON.parse(layer.columnToLabel) : {}, + }, + titles[layer.layerId] + )?.toString() || '' + : ''; + + const overwriteColors: Record = uiState?.get('vis.colors', {}) ?? {}; + const colorIsOverwritten = seriesName.toString() in overwriteColors; + let keyDownEventOn = false; + + const handleChange = (newColor: string | null) => { + if (newColor) { + onChange(newColor); + } + setColor(newColor, seriesName); + // close the popover if no color is applied or the user has clicked a color + if (!newColor || !keyDownEventOn) { + onClose(); + } + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.keyCode === KEY_CODE_ENTER) { + onClose?.(); + } + keyDownEventOn = true; + }; + + return ( + + + + + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx index 2ccf2890efd49..5a4b145a6b1f6 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx @@ -72,6 +72,13 @@ export const getSharedStyle = (config: ReferenceLineAnnotationConfig) => ({ ? [(config.lineWidth || 1) * 3, config.lineWidth || 1] : config.lineStyle === 'dotted' ? [config.lineWidth || 1, config.lineWidth || 1] + : config.lineStyle === 'dot-dashed' + ? [ + (config.lineWidth || 1) * 5, + config.lineWidth || 1, + config.lineWidth || 1, + config.lineWidth || 1, + ] : undefined, }); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/split_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/split_chart.tsx index 3f7d59e0473d5..3487b89fff290 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/split_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/split_chart.tsx @@ -9,41 +9,19 @@ import React, { useCallback } from 'react'; import { GroupBy, SmallMultiples, Predicate } from '@elastic/charts'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; -import { - getAccessorByDimension, - getColumnByAccessor, -} from '@kbn/visualizations-plugin/common/utils'; +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { Datatable } from '@kbn/expressions-plugin/public'; -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; -import { FormatFactory } from '../types'; interface SplitChartProps { splitColumnAccessor?: ExpressionValueVisDimension | string; splitRowAccessor?: ExpressionValueVisDimension | string; columns: Datatable['columns']; - formatFactory: FormatFactory; - fieldFormats: Record; } const SPLIT_COLUMN = '__split_column__'; const SPLIT_ROW = '__split_row__'; -export const SplitChart = ({ - splitColumnAccessor, - splitRowAccessor, - columns, - fieldFormats, - formatFactory, -}: SplitChartProps) => { - const format = useCallback( - (value: unknown, accessor: ExpressionValueVisDimension | string) => { - const formatParams = fieldFormats[getAccessorByDimension(accessor, columns)]; - const formatter = formatFactory(formatParams); - return formatter.convert(value); - }, - [columns, formatFactory, fieldFormats] - ); - +export const SplitChart = ({ splitColumnAccessor, splitRowAccessor, columns }: SplitChartProps) => { const getData = useCallback( (datum: Record, accessor: ExpressionValueVisDimension | string) => { const splitColumn = getColumnByAccessor(accessor, columns); @@ -59,7 +37,6 @@ export const SplitChart = ({ id={SPLIT_COLUMN} by={(spec, datum) => getData(datum, splitColumnAccessor)} sort={Predicate.DataIndex} - format={(value) => format(value, splitColumnAccessor)} /> )} {splitRowAccessor && ( @@ -67,12 +44,11 @@ export const SplitChart = ({ id={SPLIT_ROW} by={(spec, datum) => getData(datum, splitRowAccessor)} sort={Predicate.DataIndex} - format={(value) => format(value, splitRowAccessor)} /> )} ) : null; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.test.tsx index 26c1c7ff785a2..fa8638afc68d1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/tooltip/tooltip.test.tsx @@ -31,14 +31,14 @@ const getSeriesIdentifier = ({ splitColumnAccessor?: string; seriesSplitAccessors: Map; }): XYChartSeriesIdentifier => ({ - specId: generateSeriesId({ layerId, xAccessor }, splitAccessors, yAccessor), + specId: generateSeriesId({ layerId }, splitAccessors, yAccessor, xAccessor), xAccessor: xAccessor ?? 'x', yAccessor: yAccessor ?? 'a', splitAccessors: seriesSplitAccessors, seriesKeys: [], key: '1', - smVerticalAccessorValue: splitColumnAccessor, - smHorizontalAccessorValue: splitRowAccessor, + smVerticalAccessorValue: splitRowAccessor, + smHorizontalAccessorValue: splitColumnAccessor, }); describe('Tooltip', () => { @@ -113,6 +113,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} + layers={[sampleLayer]} /> ); @@ -133,6 +134,7 @@ describe('Tooltip', () => { formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} xDomain={xDomain} + layers={[sampleLayer]} /> ); @@ -153,6 +155,7 @@ describe('Tooltip', () => { formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} xDomain={xDomain} + layers={[sampleLayer]} /> ); @@ -170,6 +173,7 @@ describe('Tooltip', () => { formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} xDomain={xDomain2} + layers={[sampleLayer]} /> ); @@ -188,6 +192,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} + layers={[sampleLayer]} /> ); @@ -213,6 +218,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} + layers={[sampleLayer]} /> ); @@ -240,6 +246,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} + layers={[sampleLayer]} /> ); @@ -268,6 +275,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor, splitRowAccessor }} + layers={[sampleLayer]} /> ); @@ -295,6 +303,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitColumnAccessor }} + layers={[sampleLayer]} /> ); @@ -322,6 +331,7 @@ describe('Tooltip', () => { formatFactory={formatFactory} formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }} splitAccessors={{ splitRowAccessor }} + layers={[sampleLayer]} /> ); 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 7ab7c9f549e24..6d4ba715dae0d 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 @@ -8,7 +8,9 @@ import { TooltipInfo, XYChartSeriesIdentifier } from '@elastic/charts'; import { FormatFactory } from '@kbn/field-formats-plugin/common'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import React, { FC } from 'react'; +import { CommonXYDataLayerConfig } from '../../../common'; import { DatatablesWithFormatInfo, getMetaFromSeriesId, @@ -32,6 +34,7 @@ type Props = TooltipInfo & { splitRowAccessor?: string; splitColumnAccessor?: string; }; + layers: CommonXYDataLayerConfig[]; }; export const Tooltip: FC = ({ @@ -43,6 +46,7 @@ export const Tooltip: FC = ({ formattedDatatables, splitAccessors, xDomain, + layers, }) => { const pickedValue = values.find(({ isHighlighted }) => isHighlighted); @@ -52,10 +56,14 @@ export const Tooltip: FC = ({ const data: TooltipData[] = []; const seriesIdentifier = pickedValue.seriesIdentifier as XYChartSeriesIdentifier; - const { layerId, xAccessor, yAccessor } = getMetaFromSeriesId(seriesIdentifier.specId); - const { formattedColumns } = formattedDatatables[layerId]; + const { layerId, xAccessor, yAccessors } = getMetaFromSeriesId(seriesIdentifier.specId); + const { formattedColumns, table } = formattedDatatables[layerId]; const layerTitles = titles[layerId]; const layerFormats = fieldFormats[layerId]; + const markSizeAccessor = layers.find((layer) => layer.layerId === layerId)?.markSizeAccessor; + const markSizeColumnId = markSizeAccessor + ? getAccessorByDimension(markSizeAccessor, table.columns) + : undefined; let headerFormatter; if (header && xAccessor) { headerFormatter = formattedColumns[xAccessor] @@ -67,7 +75,9 @@ export const Tooltip: FC = ({ }); } - const tooltipYAccessor = yAccessor === seriesIdentifier.yAccessor ? yAccessor : null; + const tooltipYAccessor = yAccessors.includes(seriesIdentifier.yAccessor as string) + ? (seriesIdentifier.yAccessor as string) + : null; if (tooltipYAccessor) { const yFormatter = formatFactory(layerFormats.yAccessors[tooltipYAccessor]); data.push({ @@ -75,6 +85,12 @@ export const Tooltip: FC = ({ value: yFormatter ? yFormatter.convert(pickedValue.value) : `${pickedValue.value}`, }); } + if (markSizeColumnId && pickedValue.formattedMarkValue) { + data.push({ + label: layerTitles?.markSizeTitles?.[markSizeColumnId], + value: pickedValue.formattedMarkValue, + }); + } seriesIdentifier.splitAccessors.forEach((splitValue, key) => { const splitSeriesFormatter = formattedColumns[key] ? null @@ -87,21 +103,18 @@ export const Tooltip: FC = ({ if ( splitAccessors?.splitColumnAccessor && - seriesIdentifier.smVerticalAccessorValue !== undefined + seriesIdentifier.smHorizontalAccessorValue !== undefined ) { data.push({ label: layerTitles?.splitColumnTitles?.[splitAccessors?.splitColumnAccessor], - value: `${seriesIdentifier.smVerticalAccessorValue}`, + value: `${seriesIdentifier.smHorizontalAccessorValue}`, }); } - if ( - splitAccessors?.splitRowAccessor && - seriesIdentifier.smHorizontalAccessorValue !== undefined - ) { + if (splitAccessors?.splitRowAccessor && seriesIdentifier.smVerticalAccessorValue !== undefined) { data.push({ label: layerTitles?.splitRowTitles?.[splitAccessors?.splitRowAccessor], - value: `${seriesIdentifier.smHorizontalAccessorValue}`, + value: `${seriesIdentifier.smVerticalAccessorValue}`, }); } diff --git a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx index e193eb9a91d3f..d11bcb0bfe071 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -import { isUndefined, uniq } from 'lodash'; +import { isUndefined, uniq, find } from 'lodash'; import React from 'react'; import moment from 'moment'; -import { Endzones } from '@kbn/charts-plugin/public'; +import dateMath, { Unit } from '@kbn/datemath'; +import { Endzones, getAdjustedInterval } from '@kbn/charts-plugin/public'; import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { getAccessorByDimension, @@ -47,6 +48,8 @@ export const getXDomain = ( minInterval: number | undefined, isTimeViz: boolean, isHistogram: boolean, + hasBars: boolean, + timeZone: string, xExtent?: AxisExtentConfigResult ) => { const appliedTimeRange = getAppliedTimeRange(datatableUtilitites, layers)?.timeRange; @@ -62,7 +65,7 @@ export const getXDomain = ( ? { minInterval, min: NaN, max: NaN } : undefined; - if (isHistogram && isFullyQualified(baseDomain)) { + if ((isHistogram || isTimeViz) && isFullyQualified(baseDomain)) { if (xExtent && !isTimeViz) { return { extendedDomain: { @@ -76,7 +79,8 @@ export const getXDomain = ( const xValues = uniq( layers .flatMap(({ table, xAccessor }) => { - const accessor = xAccessor && getAccessorByDimension(xAccessor, table.columns); + const accessor = + xAccessor !== undefined ? getAccessorByDimension(xAccessor, table.columns) : undefined; return table.rows.map((row) => accessor && row[accessor] && row[accessor].valueOf()); }) .filter((v) => !isUndefined(v)) @@ -86,14 +90,25 @@ export const getXDomain = ( const lastXValue = xValues[xValues.length - 1]; const domainMin = Math.min(firstXValue, baseDomain.min); - const domainMaxValue = baseDomain.max - baseDomain.minInterval; - const domainMax = Math.max(domainMaxValue, lastXValue); + const domainMaxValue = Math.max(baseDomain.max - baseDomain.minInterval, lastXValue); + const domainMax = hasBars ? domainMaxValue : domainMaxValue + baseDomain.minInterval; + + const duration = moment.duration(baseDomain.minInterval); + const selectedUnit = find(dateMath.units, (u) => { + const value = duration.as(u); + return Number.isInteger(value); + }) as Unit; return { extendedDomain: { min: domainMin, max: domainMax, - minInterval: baseDomain.minInterval, + minInterval: getAdjustedInterval( + xValues, + duration.as(selectedUnit), + selectedUnit, + timeZone + ), }, baseDomain, }; 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 4de0f274697b0..aa05c90d052f2 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 @@ -421,7 +421,7 @@ describe('XYChart component', () => { expect(component.find(Settings).prop('xDomain')).toEqual({ // shortened to 24th midnight (elastic-charts automatically adds one min interval) - max: new Date('2021-04-24').valueOf(), + max: new Date('2021-04-25').valueOf(), // extended to 22nd midnight because of first bucket min: new Date('2021-04-22').valueOf(), minInterval: 24 * 60 * 60 * 1000, @@ -446,7 +446,7 @@ describe('XYChart component', () => { domainStart: new Date('2021-04-22T12:00:00.000Z').valueOf(), domainEnd: new Date('2021-04-24T12:00:00.000Z').valueOf(), domainMin: new Date('2021-04-22').valueOf(), - domainMax: new Date('2021-04-24').valueOf(), + domainMax: new Date('2021-04-25').valueOf(), }) ); }); @@ -3209,7 +3209,7 @@ describe('XYChart component', () => { const smallMultiples = splitChart.dive().find(SmallMultiples); expect(groupBy.at(0).prop('id')).toEqual(SPLIT_ROW); - expect(smallMultiples.prop('splitHorizontally')).toEqual(SPLIT_ROW); + expect(smallMultiples.prop('splitVertically')).toEqual(SPLIT_ROW); }); it('should render split chart if splitColumnAccessor is specified', () => { @@ -3235,7 +3235,7 @@ describe('XYChart component', () => { const smallMultiples = splitChart.dive().find(SmallMultiples); expect(groupBy.at(0).prop('id')).toEqual(SPLIT_COLUMN); - expect(smallMultiples.prop('splitVertically')).toEqual(SPLIT_COLUMN); + expect(smallMultiples.prop('splitHorizontally')).toEqual(SPLIT_COLUMN); }); it('should render split chart if both, splitRowAccessor and splitColumnAccessor are specified', () => { @@ -3267,8 +3267,8 @@ describe('XYChart component', () => { expect(groupBy.at(0).prop('id')).toEqual(SPLIT_COLUMN); expect(groupBy.at(1).prop('id')).toEqual(SPLIT_ROW); - expect(smallMultiples.prop('splitVertically')).toEqual(SPLIT_COLUMN); - expect(smallMultiples.prop('splitHorizontally')).toEqual(SPLIT_ROW); + expect(smallMultiples.prop('splitVertically')).toEqual(SPLIT_ROW); + expect(smallMultiples.prop('splitHorizontally')).toEqual(SPLIT_COLUMN); }); }); 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 5bb7f5d6f8c47..c94eccaeed0ba 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 @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { css } from '@emotion/react'; import { Chart, Settings, @@ -29,9 +30,9 @@ import { } from '@elastic/charts'; import { IconType } from '@elastic/eui'; import { PaletteRegistry } from '@kbn/coloring'; -import { RenderMode } from '@kbn/expressions-plugin/common'; +import { Datatable, RenderMode } from '@kbn/expressions-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; +import { EmptyPlaceholder, LegendToggle } from '@kbn/charts-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public'; import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; @@ -43,6 +44,7 @@ import { DEFAULT_LEGEND_SIZE, LegendSizeToPixels, } from '@kbn/visualizations-plugin/common/constants'; +import { PersistedState } from '@kbn/visualizations-plugin/public'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; import { isTimeChart } from '../../common/helpers'; import type { @@ -96,6 +98,7 @@ import { XYCurrentTime } from './xy_current_time'; import './xy_chart.scss'; import { TooltipHeader } from './tooltip'; +import { LegendColorPickerWrapperContext, LegendColorPickerWrapper } from './legend_color_picker'; declare global { interface Window { @@ -123,6 +126,7 @@ export type XYChartRenderProps = XYChartProps & { syncTooltips: boolean; eventAnnotationService: EventAnnotationServiceType; renderComplete: () => void; + uiState?: PersistedState; }; function getValueLabelsStyling(isHorizontal: boolean): { @@ -154,10 +158,29 @@ function getIconForSeriesType(layer: CommonXYDataLayerConfig): IconType { `${layer.seriesType}${layer.isHorizontal ? '_horizontal' : ''}${ layer.isPercentage ? '_percentage' : '' }${layer.isStacked ? '_stacked' : ''}` - )!.icon || 'empty' + )?.icon || 'empty' ); } +function createSplitPoint( + accessor: string | number, + value: string | number, + rows: Datatable['rows'], + table: Datatable +) { + const splitPointRowIndex = rows.findIndex((row) => { + return row[accessor] === value; + }); + if (splitPointRowIndex !== -1) { + return { + row: splitPointRowIndex, + column: table.columns.findIndex((column) => column.id === accessor), + value: table.rows[splitPointRowIndex][accessor], + table, + }; + } +} + export const XYChartReportable = React.memo(XYChart); export function XYChart({ @@ -176,6 +199,7 @@ export function XYChart({ syncTooltips, useLegacyTimeAxis, renderComplete, + uiState, }: XYChartRenderProps) { const { legend, @@ -190,6 +214,7 @@ export function XYChart({ xAxisConfig, splitColumnAccessor, splitRowAccessor, + singleTable, } = args; const chartRef = useRef(null); const chartTheme = chartsThemeService.useChartsTheme(); @@ -200,6 +225,49 @@ export function XYChart({ (hashMap, layer) => ({ ...hashMap, [layer.layerId]: layer }), {} ); + const chartHasMoreThanOneSeries = + filteredLayers.length > 1 || + filteredLayers.some((layer) => layer.accessors.length > 1) || + filteredLayers.some( + (layer) => isDataLayer(layer) && layer.splitAccessors && layer.splitAccessors.length + ); + + const getShowLegendDefault = useCallback(() => { + const legendStateDefault = + legend.isVisible && !legend.showSingleSeries ? chartHasMoreThanOneSeries : legend.isVisible; + return uiState?.get('vis.legendOpen', legendStateDefault) ?? legendStateDefault; + }, [chartHasMoreThanOneSeries, legend.isVisible, legend.showSingleSeries, uiState]); + + const [showLegend, setShowLegend] = useState(() => getShowLegendDefault()); + + useEffect(() => { + const legendShow = getShowLegendDefault(); + setShowLegend(legendShow); + }, [getShowLegendDefault]); + + const toggleLegend = useCallback(() => { + setShowLegend((value) => { + const newValue = !value; + uiState?.set?.('vis.legendOpen', newValue); + return newValue; + }); + }, [uiState]); + + const setColor = useCallback( + (newColor: string | null, seriesLabel: string | number) => { + const colors = uiState?.get('vis.colors') || {}; + if (colors[seriesLabel] === newColor || !newColor) { + delete colors[seriesLabel]; + } else { + colors[seriesLabel] = newColor; + } + uiState?.setSilent('vis.colors', null); + uiState?.set('vis.colors', colors); + uiState?.emit('reload'); + uiState?.emit('colorChanged'); + }, + [uiState] + ); const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, { datatables: filteredLayers.map(({ table }) => table), @@ -216,8 +284,9 @@ export function XYChart({ const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer); const formattedDatatables = useMemo( - () => getFormattedTablesByLayers(dataLayers, formatFactory), - [dataLayers, formatFactory] + () => + getFormattedTablesByLayers(dataLayers, formatFactory, splitColumnAccessor, splitRowAccessor), + [dataLayers, formatFactory, splitColumnAccessor, splitRowAccessor] ); const fieldFormats = useMemo( @@ -248,12 +317,6 @@ export function XYChart({ ? String(value) : String(xAxisFormatter.convert(value)); - const chartHasMoreThanOneSeries = - filteredLayers.length > 1 || - filteredLayers.some((layer) => layer.accessors.length > 1) || - filteredLayers.some( - (layer) => isDataLayer(layer) && layer.splitAccessors && layer.splitAccessors.length - ); const shouldRotate = isHorizontalChart(dataLayers); const yAxesConfiguration = getAxesConfiguration( @@ -272,7 +335,7 @@ export function XYChart({ [...(yAxisConfigs ?? []), ...(xAxisConfig ? [xAxisConfig] : [])] ); - const xTitle = xAxisConfig?.title || (xAxisColumn && xAxisColumn.name); + const xTitle = xAxisConfig?.title || (xAxisColumn && xAxisColumn.name) || undefined; const yAxesMap = { left: yAxesConfiguration.find( ({ position }) => position === getAxisPosition(Position.Left, shouldRotate) @@ -303,6 +366,7 @@ export function XYChart({ const defaultXScaleType = isTimeViz ? XScaleTypes.TIME : XScaleTypes.ORDINAL; const isHistogramViz = dataLayers.every((l) => l.isHistogram); + const hasBars = dataLayers.some((l) => l.seriesType === SeriesTypes.BAR); const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( data.datatableUtilities, @@ -310,6 +374,8 @@ export function XYChart({ minInterval, isTimeViz, isHistogramViz, + hasBars, + timeZone, xAxisConfig?.extent ); @@ -411,14 +477,16 @@ export function XYChart({ }) ); - const fit = !hasBarOrArea && extent.mode === AxisExtentModes.DATA_BOUNDS; + const fit = Boolean( + (!hasBarOrArea || axis.extent?.enforce) && extent.mode === AxisExtentModes.DATA_BOUNDS + ); const padding = axis.boundsMargin || undefined; let min: number = NaN; let max: number = NaN; if (extent.mode === 'custom') { const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent); - if (!inclusiveZeroError && !boundaryError) { + if ((!inclusiveZeroError && !boundaryError) || extent.enforce) { min = extent.lowerBound ?? NaN; max = extent.upperBound ?? NaN; } @@ -450,11 +518,12 @@ export function XYChart({ }; }; - const shouldShowValueLabels = - // No stacked bar charts - dataLayers.every((layer) => !layer.isStacked) && - // No histogram charts - !isHistogramViz; + const shouldShowValueLabels = uiState + ? valueLabels !== ValueLabelModes.HIDE + : // No stacked bar charts + dataLayers.every((layer) => !layer.isStacked) && + // No histogram charts + !isHistogramViz; const valueLabelsStyling = shouldShowValueLabels && @@ -514,21 +583,44 @@ export function XYChart({ if (xySeries.seriesKeys.length > 1) { xySeries.splitAccessors.forEach((value, accessor) => { - const splitPointRowIndex = formattedDatatables[layer.layerId].table.rows.findIndex( - (row) => { - return row[accessor] === value; - } + const point = createSplitPoint( + accessor, + value, + formattedDatatables[layer.layerId].table.rows, + table ); - if (splitPointRowIndex !== -1) { - splitPoints.push({ - row: splitPointRowIndex, - column: table.columns.findIndex((column) => column.id === accessor), - value: table.rows[splitPointRowIndex][accessor], - table, - }); + if (point) { + splitPoints.push(point); } }); } + + if (xySeries.smHorizontalAccessorValue && splitColumnAccessor) { + const accessor = getAccessorByDimension(splitColumnAccessor, table.columns); + const point = createSplitPoint( + accessor, + xySeries.smHorizontalAccessorValue, + formattedDatatables[layer.layerId].table.rows, + table + ); + if (point) { + splitPoints.push(point); + } + } + + if (xySeries.smVerticalAccessorValue && splitRowAccessor) { + const accessor = getAccessorByDimension(splitRowAccessor, table.columns); + const point = createSplitPoint( + accessor, + xySeries.smVerticalAccessorValue, + formattedDatatables[layer.layerId].table.rows, + table + ); + if (point) { + splitPoints.push(point); + } + } + const context: FilterEvent['data'] = { data: [...points, ...splitPoints], }; @@ -546,7 +638,9 @@ export function XYChart({ const { table } = dataLayers[0]; const xAccessor = - dataLayers[0].xAccessor && getAccessorByDimension(dataLayers[0].xAccessor, table.columns); + dataLayers[0].xAccessor !== undefined + ? getAccessorByDimension(dataLayers[0].xAccessor, table.columns) + : undefined; const xAxisColumnIndex = table.columns.findIndex((el) => el.id === xAccessor); const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex }; @@ -617,248 +711,273 @@ export function XYChart({ splitRowAccessor && splitTable ? getAccessorByDimension(splitRowAccessor, splitTable?.columns) : undefined; - const splitLayerFieldFormats = fieldFormats[dataLayers[0].layerId]; - const splitFieldFormats = { - ...(splitColumnId - ? { [splitColumnId]: splitLayerFieldFormats.splitColumnAccessors[splitColumnId] } - : {}), - ...(splitRowId ? { [splitRowId]: splitLayerFieldFormats.splitRowAccessors[splitRowId] } : {}), - }; + + const chartContainerStyle = css({ + width: '100%', + height: '100%', + overflowX: 'hidden', + position: uiState ? 'absolute' : 'relative', + }); return ( - - - } - onRenderChange={onRenderChange} - onPointerUpdate={handleCursorUpdate} - externalPointerEvents={{ - tooltip: { visible: syncTooltips, placement: Placement.Right }, - }} - debugState={window._echDebugStateFlag ?? false} - showLegend={ - legend.isVisible && !legend.showSingleSeries - ? chartHasMoreThanOneSeries - : legend.isVisible - } - legendPosition={legend?.isInside ? legendInsideParams : legend.position} - legendSize={LegendSizeToPixels[legend.legendSize ?? DEFAULT_LEGEND_SIZE]} - theme={{ - ...chartTheme, - barSeriesStyle: { - ...chartTheme.barSeriesStyle, - ...valueLabelsStyling, - }, - background: { - color: undefined, // removes background for embeddables - }, - legend: { - labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 }, - }, - // if not title or labels are shown for axes, add some padding if required by reference line markers - chartMargins: { - ...chartTheme.chartPaddings, - ...computeChartMargins( - linesPaddings, - { ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels }, - { ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle }, - yAxesMap, - shouldRotate - ), - }, - markSizeRatio: args.markSizeRatio, - }} - baseTheme={chartBaseTheme} - tooltip={{ - boundary: document.getElementById('app-fixed-viewport') ?? undefined, - headerFormatter: !args.detailedTooltip - ? ({ value }) => ( - - ) - : undefined, - customTooltip: args.detailedTooltip - ? ({ header, values }) => ( - - ) - : undefined, - type: args.showTooltip ? TooltipType.VerticalCursor : TooltipType.None, - }} - allowBrushingLastHistogramBin={isTimeViz} - rotation={shouldRotate ? 90 : 0} - xDomain={xDomain} - onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} - onElementClick={interactive ? clickHandler : undefined} - legendAction={ - interactive - ? getLegendAction(dataLayers, onClickValue, fieldFormats, formattedDatatables, titles) - : undefined - } - showLegendExtra={isHistogramViz && valuesInLegend} - ariaLabel={args.ariaLabel} - ariaUseDefaultSummary={!args.ariaLabel} - orderOrdinalBinsBy={ - args.orderBucketsBySum - ? { - direction: Direction.Descending, - } - : undefined - } - /> - - - { - let value = safeXAccessorLabelRenderer(d) || ''; - if (xAxisConfig?.truncate && value.length > xAxisConfig.truncate) { - value = `${value.slice(0, xAxisConfig.truncate)}...`; - } - return value; - }} - style={xAxisStyle} - showOverlappingLabels={xAxisConfig?.showOverlappingLabels} - showDuplicatedTicks={xAxisConfig?.showDuplicates} - timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0} - /> - {isSplitChart && splitTable && ( - + {showLegend !== undefined && uiState && ( + )} - {yAxesConfiguration.map((axis) => { - return ( - + + + } + onRenderChange={onRenderChange} + onPointerUpdate={handleCursorUpdate} + externalPointerEvents={{ + tooltip: { visible: syncTooltips, placement: Placement.Right }, + }} + legendColorPicker={uiState ? LegendColorPickerWrapper : undefined} + debugState={window._echDebugStateFlag ?? false} + showLegend={showLegend} + legendPosition={legend?.isInside ? legendInsideParams : legend.position} + legendSize={LegendSizeToPixels[legend.legendSize ?? DEFAULT_LEGEND_SIZE]} + theme={{ + ...chartTheme, + barSeriesStyle: { + ...chartTheme.barSeriesStyle, + ...valueLabelsStyling, + }, + background: { + color: undefined, // removes background for embeddables + }, + legend: { + labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 }, + }, + // if not title or labels are shown for axes, add some padding if required by reference line markers + chartMargins: { + ...chartTheme.chartPaddings, + ...computeChartMargins( + linesPaddings, + { ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels }, + { ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle }, + yAxesMap, + shouldRotate + ), + }, + markSizeRatio: args.markSizeRatio, + }} + baseTheme={chartBaseTheme} + tooltip={{ + boundary: document.getElementById('app-fixed-viewport') ?? undefined, + headerFormatter: !args.detailedTooltip + ? ({ value }) => ( + + ) + : undefined, + customTooltip: args.detailedTooltip + ? ({ header, values }) => ( + + ) + : undefined, + type: args.showTooltip ? TooltipType.VerticalCursor : TooltipType.None, }} - hide={axis.hide || dataLayers[0]?.simpleView} + allowBrushingLastHistogramBin={isTimeViz} + rotation={shouldRotate ? 90 : 0} + xDomain={xDomain} + onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} + onElementClick={interactive ? clickHandler : undefined} + legendAction={ + interactive + ? getLegendAction( + dataLayers, + onClickValue, + fieldFormats, + formattedDatatables, + titles, + singleTable + ) + : undefined + } + showLegendExtra={isHistogramViz && valuesInLegend} + ariaLabel={args.ariaLabel} + ariaUseDefaultSummary={!args.ariaLabel} + orderOrdinalBinsBy={ + args.orderBucketsBySum + ? { + direction: Direction.Descending, + } + : undefined + } + /> + + + { - let value = axis.formatter?.convert(d) || ''; - if (axis.truncate && value.length > axis.truncate) { - value = `${value.slice(0, axis.truncate)}...`; + let value = safeXAccessorLabelRenderer(d) || ''; + if (xAxisConfig?.truncate && value.length > xAxisConfig.truncate) { + value = `${value.slice(0, xAxisConfig.truncate)}...`; } return value; }} - style={getYAxesStyle(axis)} - domain={getYAxisDomain(axis)} - showOverlappingLabels={axis.showOverlappingLabels} - showDuplicatedTicks={axis.showDuplicates} - ticks={5} + style={xAxisStyle} + showOverlappingLabels={xAxisConfig?.showOverlappingLabels} + showDuplicatedTicks={xAxisConfig?.showDuplicates} + timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0} /> - ); - })} - - {!hideEndzones && ( - - layer.isHistogram && - (layer.isStacked || !layer.splitAccessors || !layer.splitAccessors.length) && - (layer.isStacked || - layer.seriesType !== SeriesTypes.BAR || - !chartHasMoreThanOneBarSeries) + {isSplitChart && splitTable && ( + + )} + {yAxesConfiguration.map((axis) => { + return ( + { + let value = axis.formatter?.convert(d) || ''; + if (axis.truncate && value.length > axis.truncate) { + value = `${value.slice(0, axis.truncate)}...`; + } + return value; + }} + style={getYAxesStyle(axis)} + domain={getYAxisDomain(axis)} + showOverlappingLabels={axis.showOverlappingLabels} + showDuplicatedTicks={axis.showDuplicates} + /> + ); + })} + + {!hideEndzones && ( + + layer.isHistogram && + (layer.isStacked || !layer.splitAccessors || !layer.splitAccessors.length) && + (layer.isStacked || + layer.seriesType !== SeriesTypes.BAR || + !chartHasMoreThanOneBarSeries) + )} + /> )} - /> - )} - {dataLayers.length && ( - - )} - {referenceLineLayers.length ? ( - - ) : null} - {rangeAnnotations.length || groupedLineAnnotations.length ? ( - 0} - minInterval={minInterval} - simpleView={annotationsLayers?.[0].simpleView} - outsideDimension={ - rangeAnnotations.length && shouldHideDetails - ? OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION - : shouldUseNewTimeAxis - ? Number(MULTILAYER_TIME_AXIS_STYLE.tickLine?.padding || 0) + - Number(chartTheme.axes?.tickLabel?.fontSize || 0) - : Number(chartTheme.axes?.tickLine?.size) || OUTSIDE_RECT_ANNOTATION_WIDTH - } - /> - ) : null} - + {dataLayers.length && ( + + )} + {referenceLineLayers.length ? ( + + ) : null} + {rangeAnnotations.length || groupedLineAnnotations.length ? ( + 0} + minInterval={minInterval} + simpleView={annotationsLayers?.[0].simpleView} + outsideDimension={ + rangeAnnotations.length && shouldHideDetails + ? OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION + : shouldUseNewTimeAxis + ? Number(MULTILAYER_TIME_AXIS_STYLE.tickLine?.padding || 0) + + Number(chartTheme.axes?.tickLabel?.fontSize || 0) + : Number(chartTheme.axes?.tickLine?.size) || OUTSIDE_RECT_ANNOTATION_WIDTH + } + /> + ) : null} + + + ); } diff --git a/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts index d16d8204846fc..bb4acd7fa50de 100644 --- a/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts +++ b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts @@ -28,6 +28,7 @@ export const visualizationDefinitions = [ { id: `${SeriesTypes.BAR}_horizontal_stacked`, icon: BarHorizontalStackedIcon }, { id: `${SeriesTypes.BAR}_horizontal_percentage_stacked`, icon: BarHorizontalPercentageIcon }, { id: SeriesTypes.LINE, icon: LineIcon }, + { id: `${SeriesTypes.LINE}_stacked`, icon: LineIcon }, { id: SeriesTypes.AREA, icon: AreaIcon }, { id: `${SeriesTypes.AREA}_stacked`, icon: AreaStackedIcon }, { id: `${SeriesTypes.AREA}_percentage_stacked`, icon: AreaPercentageIcon }, diff --git a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx index e1b5512dbebff..3bab1c2a1accd 100644 --- a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -9,10 +9,12 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { ThemeServiceStart } from '@kbn/core/public'; +import { css } from '@emotion/react'; import React from 'react'; import ReactDOM from 'react-dom'; import { METRIC_TYPE } from '@kbn/analytics'; import type { PaletteRegistry } from '@kbn/coloring'; +import { PersistedState } from '@kbn/visualizations-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; @@ -133,13 +135,16 @@ export const getXyChartRenderer = ({ import('../helpers/interval'), ]); + const chartContainerStyle = css({ + position: 'relative', + width: '100%', + height: '100%', + }); + ReactDOM.render( -
+
{' '} 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 f1a11bbe06185..6442845b0ecf3 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 @@ -118,21 +118,28 @@ describe('color_assignment', () => { }, }; + const titles = { + [layers[0].layerId]: { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + [layers[1].layerId]: { + yTitles: { + y1: 'test1', + y2: 'test2', + y3: 'test3', + y4: 'test4', + }, + }, + }; + describe('totalSeriesCount', () => { it('should calculate total number of series per palette', () => { - const assignments = getColorAssignments( - layers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, - fieldFormats, - formattedDatatables - ); + const assignments = getColorAssignments(layers, titles, fieldFormats, formattedDatatables); // two y accessors, with 3 splitted series expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3); expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); @@ -141,14 +148,7 @@ describe('color_assignment', () => { it('should calculate total number of series spanning multible layers', () => { const assignments = getColorAssignments( [layers[0], { ...layers[1], palette: layers[0].palette }], - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, fieldFormats, formattedDatatables ); @@ -160,14 +160,7 @@ 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, splitAccessors: undefined }], - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, fieldFormats, formattedDatatables ); @@ -200,14 +193,7 @@ describe('color_assignment', () => { const assignments = getColorAssignments( newLayers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, fieldFormats, newFormattedDatatables ); @@ -230,14 +216,7 @@ describe('color_assignment', () => { }; const assignments = getColorAssignments( newLayers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, fieldFormats, newFormattedDatatables ); @@ -249,44 +228,20 @@ describe('color_assignment', () => { describe('getRank', () => { it('should return the correct rank for a series key', () => { - const assignments = getColorAssignments( - layers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, - fieldFormats, - formattedDatatables - ); + const assignments = getColorAssignments(layers, titles, fieldFormats, formattedDatatables); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(layers[0], '2 - test2')).toEqual(3); + expect(assignments.palette1.getRank(layers[0].layerId, '2 - test2')).toEqual(3); // 1 series in front of 1/y4 - 1/y3 - expect(assignments.palette2.getRank(layers[1], '1 - test4')).toEqual(1); + expect(assignments.palette2.getRank(layers[1].layerId, '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, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, - fieldFormats, - formattedDatatables - ); + const assignments = getColorAssignments(newLayers, titles, fieldFormats, formattedDatatables); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(newLayers[0], '2 - test2')).toEqual(3); + expect(assignments.palette1.getRank(newLayers[0].layerId, '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 - test3')).toEqual(8); + expect(assignments.palette1.getRank(newLayers[1].layerId, '2 - test3')).toEqual(8); }); it('should return the correct rank for a series without a split', () => { @@ -294,23 +249,11 @@ describe('color_assignment', () => { layers[0], { ...layers[1], palette: layers[0].palette, splitAccessors: undefined }, ]; - const assignments = getColorAssignments( - newLayers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, - fieldFormats, - formattedDatatables - ); + const assignments = getColorAssignments(newLayers, titles, fieldFormats, formattedDatatables); // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 - expect(assignments.palette1.getRank(newLayers[0], '2 - test2')).toEqual(3); + expect(assignments.palette1.getRank(newLayers[0].layerId, '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], 'test4')).toEqual(7); + expect(assignments.palette1.getRank(newLayers[1].layerId, 'test4')).toEqual(7); }); it('should return the correct rank for a series with a non-primitive value', () => { @@ -336,21 +279,14 @@ describe('color_assignment', () => { const assignments = getColorAssignments( newLayers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, fieldFormats, newFormattedDatatables ); 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); + expect(assignments.palette1.getRank(layers[0].layerId, 'formatted - test1')).toEqual(2); }); it('should handle missing columns', () => { @@ -365,20 +301,13 @@ describe('color_assignment', () => { const assignments = getColorAssignments( newLayers, - { - yTitles: { - y1: 'test1', - y2: 'test2', - y3: 'test3', - y4: 'test4', - }, - }, + titles, 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], 'test2')).toEqual(1); + expect(assignments.palette1.getRank(layers[0].layerId, '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 c7139cf036fa2..2cce918d4b798 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 @@ -13,7 +13,12 @@ import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils' import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { isDataLayer } from './visualization'; import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common'; -import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers'; +import { + LayerAccessorsTitles, + LayerFieldFormats, + LayersAccessorsTitles, + LayersFieldFormats, +} from './layers'; import { DatatablesWithFormatInfo, DatatableWithFormatInfo } from './data_layers'; export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; @@ -22,7 +27,7 @@ export type ColorAssignments = Record< string, { totalSeriesCount: number; - getRank(sortedLayer: CommonXYDataLayerConfig, seriesName: string): number; + getRank(layerId: string, seriesName: string): number; } >; @@ -53,7 +58,8 @@ export const getAllSeries = ( accessors: Array, columnToLabel: CommonXYDataLayerConfig['columnToLabel'], titles: LayerAccessorsTitles, - fieldFormats: LayerFieldFormats + fieldFormats: LayerFieldFormats, + accessorsCount: number ) => { if (!formattedDatatable.table) { return []; @@ -71,7 +77,7 @@ export const getAllSeries = ( const yTitle = columnToLabelMap[yAccessor] ?? titles?.yTitles?.[yAccessor] ?? null; let name = yTitle; if (splitName) { - name = accessors.length > 1 ? `${splitName} - ${yTitle}` : splitName; + name = accessorsCount > 1 ? `${splitName} - ${yTitle}` : splitName; } if (!allSeries.includes(name)) { @@ -85,7 +91,7 @@ export const getAllSeries = ( export function getColorAssignments( layers: CommonXYLayerConfig[], - titles: LayerAccessorsTitles, + titles: LayersAccessorsTitles, fieldFormats: LayersFieldFormats, formattedDatatables: DatatablesWithFormatInfo ): ColorAssignments { @@ -111,22 +117,22 @@ export function getColorAssignments( layer.splitAccessors, layer.accessors, layer.columnToLabel, - titles, - fieldFormats[layer.layerId] + titles[layer.layerId], + fieldFormats[layer.layerId], + layer.accessors.length ) || []; return { numberOfSeries: allSeries.length, allSeries }; }); + const totalSeriesCount = seriesPerLayer.reduce( (sum, perLayer) => sum + perLayer.numberOfSeries, 0 ); return { totalSeriesCount, - getRank(sortedLayer: CommonXYDataLayerConfig, seriesName: string) { - const layerIndex = paletteLayers.findIndex( - (layer) => sortedLayer.layerId === layer.layerId - ); + getRank(layerId: string, seriesName: string) { + const layerIndex = paletteLayers.findIndex((layer) => layerId === layer.layerId); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; const rank = currentSeriesPerLayer.allSeries.indexOf(seriesName); return ( 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 4f830c08b234d..8098bb0efe02b 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 @@ -19,6 +19,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { IFieldFormat } from '@kbn/field-formats-plugin/common'; +import type { PersistedState } from '@kbn/visualizations-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; @@ -37,7 +38,7 @@ type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps; type GetSeriesPropsFn = (config: { layer: CommonXYDataLayerConfig; titles?: LayerAccessorsTitles; - accessor: string; + accessor: string | string[]; chartHasMoreThanOneBarSeries?: boolean; formatFactory: FormatFactory; colorAssignments: ColorAssignments; @@ -51,6 +52,9 @@ type GetSeriesPropsFn = (config: { formattedDatatableInfo: DatatableWithFormatInfo; defaultXScaleType: XScaleType; fieldFormats: LayersFieldFormats; + uiState?: PersistedState; + allYAccessors: Array; + singleTable?: boolean; }) => SeriesSpec; type GetSeriesNameFn = ( @@ -70,12 +74,13 @@ type GetColorFn = ( seriesIdentifier: XYChartSeriesIdentifier, config: { layer: CommonXYDataLayerConfig; - accessor: string; colorAssignments: ColorAssignments; paletteService: PaletteRegistry; getSeriesNameFn: (d: XYChartSeriesIdentifier) => SeriesName; syncColors?: boolean; - } + }, + uiState?: PersistedState, + singleTable?: boolean ) => string | null; type GetPointConfigFn = (config: { @@ -107,6 +112,8 @@ export const getFormattedRow = ( columns: Datatable['columns'], columnsFormatters: Record, xAccessor: string | undefined, + splitColumnAccessor: string | undefined, + splitRowAccessor: string | undefined, xScaleType: XScaleType ): { row: Datatable['rows'][number]; formattedColumns: Record } => columns.reduce( @@ -115,7 +122,10 @@ export const getFormattedRow = ( if ( record != null && // pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level - (!isPrimitive(record) || (id === xAccessor && xScaleType === 'ordinal')) + (!isPrimitive(record) || + (id === xAccessor && xScaleType === 'ordinal') || + id === splitColumnAccessor || + id === splitRowAccessor) ) { return { row: { ...formattedInfo.row, [id]: columnsFormatters[id]!.convert(record) }, @@ -131,6 +141,8 @@ export const getFormattedTable = ( table: Datatable, formatFactory: FormatFactory, xAccessor: string | ExpressionValueVisDimension | undefined, + splitColumnAccessor: string | ExpressionValueVisDimension | undefined, + splitRowAccessor: string | ExpressionValueVisDimension | undefined, accessors: Array, xScaleType: XScaleType ): { table: Datatable; formattedColumns: Record } => { @@ -161,6 +173,8 @@ export const getFormattedTable = ( table.columns, columnsFormatters, xAccessor ? getAccessorByDimension(xAccessor, table.columns) : undefined, + splitColumnAccessor ? getAccessorByDimension(splitColumnAccessor, table.columns) : undefined, + splitRowAccessor ? getAccessorByDimension(splitRowAccessor, table.columns) : undefined, xScaleType ); formattedTableInfo.rows.push(formattedRowInfo.row); @@ -178,7 +192,9 @@ export const getFormattedTable = ( export const getFormattedTablesByLayers = ( layers: CommonXYDataLayerConfig[], - formatFactory: FormatFactory + formatFactory: FormatFactory, + splitColumnAccessor?: string | ExpressionValueVisDimension, + splitRowAccessor?: string | ExpressionValueVisDimension ): DatatablesWithFormatInfo => layers.reduce( ( @@ -190,9 +206,11 @@ export const getFormattedTablesByLayers = ( table, formatFactory, xAccessor, - [xAccessor, ...splitAccessors, ...accessors].filter( - (a): a is string | ExpressionValueVisDimension => a !== undefined - ), + splitColumnAccessor, + splitRowAccessor, + [xAccessor, ...splitAccessors, ...accessors, splitColumnAccessor, splitRowAccessor].filter< + string | ExpressionValueVisDimension + >((a): a is string | ExpressionValueVisDimension => a !== undefined), xScaleType ), }), @@ -290,21 +308,29 @@ const getLineConfig: GetLineConfigFn = ({ showLines, lineWidth }) => ({ const getColor: GetColorFn = ( series, - { layer, accessor, colorAssignments, paletteService, syncColors, getSeriesNameFn } + { layer, colorAssignments, paletteService, syncColors, getSeriesNameFn }, + uiState, + singleTable ) => { - const overwriteColor = getSeriesColor(layer, accessor); + const overwriteColor = getSeriesColor(layer, series.yAccessor as string); if (overwriteColor !== null) { return overwriteColor; } - const colorAssignment = colorAssignments[layer.palette.name]; const name = getSeriesNameFn(series)?.toString() || ''; + const overwriteColors: Record = uiState?.get ? uiState.get('vis.colors', {}) : {}; + + if (Object.keys(overwriteColors).includes(name)) { + return overwriteColors[name]; + } + const colorAssignment = colorAssignments[layer.palette.name]; + const seriesLayers: SeriesLayer[] = [ { name, totalSeriesAtDepth: colorAssignment.totalSeriesCount, - rankAtDepth: colorAssignment.getRank(layer, name), + rankAtDepth: colorAssignment.getRank(singleTable ? 'commonLayerId' : layer.layerId, name), }, ]; return paletteService.get(layer.palette.name).getCategoricalColor( @@ -320,23 +346,25 @@ const getColor: GetColorFn = ( }; const EMPTY_ACCESSOR = '-'; -const SPLIT_CHAR = '.'; +const SPLIT_CHAR = ':'; +const SPLIT_Y_ACCESSORS = '|'; export const generateSeriesId = ( - { layerId, xAccessor }: Pick, + { layerId }: Pick, splitColumnIds: string[], - accessor?: string + accessor?: string, + xColumnId?: string ) => - [layerId, xAccessor ?? EMPTY_ACCESSOR, accessor ?? EMPTY_ACCESSOR, ...splitColumnIds].join( + [layerId, xColumnId ?? EMPTY_ACCESSOR, accessor ?? EMPTY_ACCESSOR, ...splitColumnIds].join( SPLIT_CHAR ); export const getMetaFromSeriesId = (seriesId: string) => { - const [layerId, xAccessor, yAccessor, ...splitAccessors] = seriesId.split(SPLIT_CHAR); + const [layerId, xAccessor, yAccessors, ...splitAccessors] = seriesId.split(SPLIT_CHAR); return { layerId, xAccessor: xAccessor === EMPTY_ACCESSOR ? undefined : xAccessor, - yAccessor, + yAccessors: yAccessors.split(SPLIT_Y_ACCESSORS), splitAccessor: splitAccessors[0] === EMPTY_ACCESSOR ? undefined : splitAccessors, }; }; @@ -358,6 +386,9 @@ export const getSeriesProps: GetSeriesPropsFn = ({ formattedDatatableInfo, defaultXScaleType, fieldFormats, + uiState, + allYAccessors, + singleTable, }): SeriesSpec => { const { table, isStacked, markSizeAccessor } = layer; const isPercentage = layer.isPercentage; @@ -367,7 +398,10 @@ 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 xColumnId = + layer.xAccessor !== undefined + ? getAccessorByDimension(layer.xAccessor, table.columns) + : undefined; const splitColumnIds = layer.splitAccessors?.map((splitAccessor) => { return getAccessorByDimension(splitAccessor, table.columns); @@ -377,7 +411,9 @@ export const getSeriesProps: GetSeriesPropsFn = ({ (isStacked || !splitColumnIds.length) && (isStacked || !isBarChart || !chartHasMoreThanOneBarSeries); - const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params; + const formatter = table?.columns.find( + (column) => column.id === (Array.isArray(accessor) ? accessor[0] : accessor) + )?.meta?.params; const markSizeColumnId = markSizeAccessor ? getAccessorByDimension(markSizeAccessor, table.columns) @@ -399,16 +435,22 @@ export const getSeriesProps: GetSeriesPropsFn = ({ !(xColumnId && row[xColumnId] === undefined) && !( splitColumnIds.some((splitColumnId) => row[splitColumnId] === undefined) && - row[accessor] === undefined + (Array.isArray(accessor) + ? accessor.some((a) => row[a] === undefined) + : row[accessor] === undefined) ) ); + const emptyX: Record = { + unifiedX: i18n.translate('expressionXY.xyChart.emptyXLabel', { + defaultMessage: '(empty)', + }), + }; + if (!xColumnId) { rows = rows.map((row) => ({ ...row, - unifiedX: i18n.translate('expressionXY.xyChart.emptyXLabel', { - defaultMessage: '(empty)', - }), + ...emptyX, })); } @@ -417,7 +459,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ d, { splitAccessors: layer.splitAccessors || [], - accessorsCount: layer.accessors.length, + accessorsCount: singleTable ? allYAccessors.length : layer.accessors.length, alreadyFormattedColumns: formattedColumns, columns: formattedTable.columns, splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, @@ -429,14 +471,15 @@ export const getSeriesProps: GetSeriesPropsFn = ({ return { splitSeriesAccessors: splitColumnIds.length ? splitColumnIds : [], - stackAccessors: isStacked ? [layer.xAccessor as string] : [], + stackAccessors: isStacked && xColumnId ? [xColumnId] : [], id: generateSeriesId( layer, splitColumnIds.length ? splitColumnIds : [EMPTY_ACCESSOR], - accessor + Array.isArray(accessor) ? accessor.join(SPLIT_Y_ACCESSORS) : accessor, + xColumnId ), xAccessor: xColumnId || 'unifiedX', - yAccessors: [accessor], + yAccessors: Array.isArray(accessor) ? accessor : [accessor], markSizeAccessor: markSizeColumnId, markFormat: (value) => markFormatter.convert(value), data: rows, @@ -446,14 +489,18 @@ export const getSeriesProps: GetSeriesPropsFn = ({ ? ScaleType.LinearBinary : scaleType, color: (series) => - getColor(series, { - layer, - accessor, - colorAssignments, - paletteService, - getSeriesNameFn, - syncColors, - }), + getColor( + series, + { + layer, + colorAssignments, + paletteService, + getSeriesNameFn, + syncColors, + }, + uiState, + singleTable + ), groupId: yAxis?.groupId, enableHistogramMode, stackMode, 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 359f1c27879a9..7dc26e4c76cf1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts @@ -61,6 +61,7 @@ export interface LayerAccessorsTitles { splitSeriesTitles?: AccessorsTitles; splitColumnTitles?: AccessorsTitles; splitRowTitles?: AccessorsTitles; + markSizeTitles?: AccessorsTitles; } export type LayersAccessorsTitles = Record; @@ -80,7 +81,9 @@ export function getFilteredLayers(layers: CommonXYLayerConfig[]) { if (isDataLayer(layer)) { xAccessor = - layer.xAccessor && table && getAccessorByDimension(layer.xAccessor, table.columns); + layer.xAccessor !== undefined && table + ? getAccessorByDimension(layer.xAccessor, table.columns) + : undefined; splitAccessors = table ? layer.splitAccessors?.map((splitAccessor) => getAccessorByDimension(splitAccessor, table!.columns) @@ -190,11 +193,18 @@ const getTitleForYAccessor = ( group.series.some(({ accessor, layer }) => accessor === yAccessor && layer === layerId) ); - return axisGroup?.title || column!.name; + return column?.name ?? axisGroup?.title; }; export const getLayerTitles = ( - { xAccessor, accessors, splitAccessors = [], table, layerId }: CommonXYDataLayerConfig, + { + xAccessor, + accessors, + splitAccessors = [], + table, + layerId, + markSizeAccessor, + }: CommonXYDataLayerConfig, { splitColumnAccessor, splitRowAccessor }: SplitAccessors, { xTitle }: CustomTitles, groups: GroupsConfiguration @@ -212,8 +222,8 @@ export const getLayerTitles = ( [accessor]: getTitleForYAccessor(layerId, accessor, groups, table.columns), }); - const xColumnId = xAccessor && getAccessorByDimension(xAccessor, table.columns); - const yColumnIds = accessors.map((a) => a && getAccessorByDimension(a, table.columns)); + const xColumnId = xAccessor ? getAccessorByDimension(xAccessor, table.columns) : undefined; + const yColumnIds = accessors.map((a) => getAccessorByDimension(a, table.columns)); const splitColumnAccessors: Array = splitAccessors; return { @@ -229,6 +239,7 @@ export const getLayerTitles = ( }), {} ), + markSizeTitles: mapTitle(markSizeAccessor), splitColumnTitles: mapTitle(splitColumnAccessor), splitRowTitles: mapTitle(splitRowAccessor), }; diff --git a/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap index aa2a68204108a..37b8daab6a7a4 100644 --- a/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap @@ -4,83 +4,41 @@ exports[`xy vis toExpressionAst function should match basic snapshot 1`] = ` Object { "addArgument": [Function], "arguments": Object { - "addLegend": Array [ - true, - ], "addTimeMarker": Array [ false, ], - "addTooltip": Array [ - true, - ], - "categoryAxes": Array [ + "layers": Array [ Object { "toAst": [Function], }, ], - "chartType": Array [ - "area", - ], - "gridCategoryLines": Array [ - false, - ], - "labels": Array [ + "legend": Array [ Object { "toAst": [Function], }, ], - "legendPosition": Array [ - "top", - ], - "legendSize": Array [ - "auto", - ], - "maxLegendLines": Array [ - 1, - ], - "palette": Array [ - "default", - ], - "seriesDimension": Array [ - Object { - "toAst": [Function], - }, - ], - "seriesParams": Array [ - Object { - "toAst": [Function], - }, - ], - "thresholdLine": Array [ - Object { - "toAst": [Function], - }, - ], - "times": Array [], - "truncateLegend": Array [ + "showTooltip": Array [ true, ], - "type": Array [ - "area", + "singleTable": Array [ + true, ], - "valueAxes": Array [ - Object { - "toAst": [Function], - }, + "valueLabels": Array [ + "hide", ], - "xDimension": Array [ + "xAxisConfig": Array [ Object { "toAst": [Function], }, ], - "yDimension": Array [ + "yAxisConfigs": Array [ Object { "toAst": [Function], }, ], }, "getArgument": [Function], - "name": "xy_vis", + "name": "layeredXyVis", "removeArgument": [Function], "replaceArgument": [Function], "toAst": [Function], diff --git a/src/plugins/vis_types/xy/public/to_ast.ts b/src/plugins/vis_types/xy/public/to_ast.ts index e92ca8cda82d2..4041075b98c4d 100644 --- a/src/plugins/vis_types/xy/public/to_ast.ts +++ b/src/plugins/vis_types/xy/public/to_ast.ts @@ -7,7 +7,8 @@ */ import moment from 'moment'; -import { Position } from '@elastic/charts'; +import { Position, ScaleType as ECScaleType } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; import { VisToExpressionAst, getVisSchemas, @@ -17,9 +18,8 @@ import { } from '@kbn/visualizations-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { BUCKET_TYPES } from '@kbn/data-plugin/public'; -import { Labels } from '@kbn/charts-plugin/public'; - import { TimeRangeBounds } from '@kbn/data-plugin/common'; +import { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types'; import { Dimensions, Dimension, @@ -29,89 +29,274 @@ import { ThresholdLine, ValueAxis, Scale, - TimeMarker, + ChartMode, + InterpolationMode, + ScaleType, } from './types'; -import { visName, VisTypeXyExpressionFunctionDefinition } from './expression_functions/xy_vis_fn'; -import { XyVisType } from '../common'; +import { ChartType } from '../common'; import { getSeriesParams } from './utils/get_series_params'; import { getSafeId } from './utils/accessors'; -const prepareLabel = (data: Labels) => { - const label = buildExpressionFunction('label', { - ...data, +interface Bounds { + min?: string | number; + max?: string | number; +} + +type YDimension = Omit & { accessor: string }; + +const getCurveType = (type?: InterpolationMode) => { + switch (type) { + case 'cardinal': + return 'CURVE_MONOTONE_X'; + case 'step-after': + return 'CURVE_STEP_AFTER'; + case 'linear': + default: + return 'LINEAR'; + } +}; + +const prepareLengend = (params: VisParams, legendSize?: LegendSize) => { + const legend = buildExpressionFunction('legendConfig', { + isVisible: params.addLegend, + maxLines: params.maxLegendLines, + position: params.legendPosition, + shouldTruncate: params.truncateLegend, + showSingleSeries: true, + legendSize, }); - return buildExpression([label]); + return buildExpression([legend]); +}; + +const getCorrectAccessor = (yAccessor: Dimension | YDimension, aggId: string) => { + return typeof yAccessor.accessor === 'number' + ? `col-${yAccessor.accessor}-${aggId}` + : yAccessor.accessor; }; -const prepareScale = (data: Scale) => { - const scale = buildExpressionFunction('visscale', { - ...data, +const prepareDecoration = (axisId: string, yAccessor: YDimension, aggId: string) => { + const dataDecorationConfig = buildExpressionFunction('dataDecorationConfig', { + forAccessor: getCorrectAccessor(yAccessor, aggId), + axisId, }); - return buildExpression([scale]); + return buildExpression([dataDecorationConfig]); }; -const prepareThresholdLine = (data: ThresholdLine) => { - const thresholdLine = buildExpressionFunction('thresholdline', { - ...data, +const preparePalette = (palette: PaletteOutput) => { + const paletteExp = buildExpressionFunction( + palette.name === 'custom' ? 'palette' : 'system_palette', + palette.name === 'custom' + ? { ...palette.params } + : { + name: palette.name, + } + ); + + return buildExpression([paletteExp]); +}; + +const prepareLayers = ( + seriesParam: SeriesParam, + isHistogram: boolean, + valueAxes: ValueAxis[], + yAccessors: YDimension[], + xAccessor: Dimension | null, + splitAccessors?: Dimension[], + markSizeAccessor?: Dimension, + palette?: PaletteOutput, + xScale?: Scale +) => { + // valueAxis.position !== Position.Left + const isHorizontal = valueAxes.some((valueAxis) => { + return ( + seriesParam.valueAxis === valueAxis.id && + valueAxis.position !== Position.Left && + valueAxis.position !== Position.Right + ); + }); + const isBar = seriesParam.type === ChartType.Histogram; + const dataLayer = buildExpressionFunction('extendedDataLayer', { + seriesType: isBar ? 'bar' : seriesParam.type, + isHistogram, + isHorizontal, + isStacked: seriesParam.mode === ChartMode.Stacked, + lineWidth: !isBar ? seriesParam.lineWidth : undefined, + showPoints: !isBar ? seriesParam.showCircles : undefined, + pointsRadius: !isBar ? seriesParam.circlesRadius ?? 3 : undefined, + showLines: !isBar ? seriesParam.drawLinesBetweenPoints : undefined, + curveType: getCurveType(seriesParam.interpolate), + decorations: yAccessors.map((accessor) => + prepareDecoration(seriesParam.valueAxis, accessor, seriesParam.data.id) + ), + accessors: yAccessors.map((accessor) => prepareVisDimension(accessor)), + xAccessor: xAccessor ? prepareVisDimension(xAccessor) : 'all', + xScaleType: getScaleType( + xScale, + xAccessor?.format?.id === 'number' || + (xAccessor?.format?.params?.id === 'number' && + xAccessor?.format?.id !== BUCKET_TYPES.RANGE && + xAccessor?.format?.id !== BUCKET_TYPES.TERMS), + 'date' in (xAccessor?.params || {}), + 'interval' in (xAccessor?.params || {}) + ), + splitAccessors: splitAccessors ? splitAccessors.map(prepareVisDimension) : undefined, + markSizeAccessor: + markSizeAccessor && !isBar ? prepareVisDimension(markSizeAccessor) : undefined, + palette: palette ? preparePalette(palette) : undefined, + columnToLabel: JSON.stringify( + [...yAccessors, xAccessor, ...(splitAccessors ?? [])].reduce>( + (acc, dimension) => { + if (dimension) { + acc[getCorrectAccessor(dimension, seriesParam.data.id)] = dimension.label; + } + + return acc; + }, + {} + ) + ), }); - return buildExpression([thresholdLine]); + return buildExpression([dataLayer]); +}; + +const getMode = (scale: Scale, bounds?: Bounds) => { + if (scale.defaultYExtents) { + return 'dataBounds'; + } + + if (scale.setYExtents || bounds) { + return 'custom'; + } +}; + +const getLabelArgs = (data: CategoryAxis, isTimeChart?: boolean) => { + return { + truncate: data.labels.truncate, + labelsOrientation: -(data.labels.rotate ?? (isTimeChart ? 0 : 90)), + showOverlappingLabels: data.labels.filter === false, + showDuplicates: data.labels.filter === false, + labelColor: data.labels.color, + showLabels: data.labels.show, + }; }; -const prepareTimeMarker = (data: TimeMarker) => { - const timeMarker = buildExpressionFunction('timemarker', { - ...data, +const prepareAxisExtentConfig = (scale: Scale, bounds?: Bounds) => { + const axisExtentConfig = buildExpressionFunction('axisExtentConfig', { + mode: getMode(scale, bounds), + lowerBound: bounds?.min || scale.min, + upperBound: bounds?.max || scale.max, + enforce: true, }); - return buildExpression([timeMarker]); + return buildExpression([axisExtentConfig]); }; -const prepareCategoryAxis = (data: CategoryAxis) => { - const categoryAxis = buildExpressionFunction('categoryaxis', { - id: data.id, - show: data.show, - position: data.position, - type: data.type, +function getScaleType( + scale?: Scale, + isNumber?: boolean, + isTime = false, + isHistogram = false +): ECScaleType | undefined { + if (isTime) return ECScaleType.Time; + if (isHistogram) return ECScaleType.Linear; + + if (!isNumber) { + return ECScaleType.Ordinal; + } + + const type = scale?.type; + if (type === ScaleType.SquareRoot) { + return ECScaleType.Sqrt; + } + + return type; +} + +function getYAxisPosition(position: Position) { + if (position === Position.Top) { + return Position.Right; + } + + if (position === Position.Bottom) { + return Position.Left; + } + + return position; +} + +function getXAxisPosition(position: Position) { + if (position === Position.Left) { + return Position.Bottom; + } + + if (position === Position.Right) { + return Position.Top; + } + + return position; +} + +const prepareXAxis = ( + data: CategoryAxis, + showGridLines?: boolean, + bounds?: Bounds, + isTimeChart?: boolean +) => { + const xAxisConfig = buildExpressionFunction('xAxisConfig', { + hide: !data.show, + position: getXAxisPosition(data.position), title: data.title.text, - scale: prepareScale(data.scale), - labels: prepareLabel(data.labels), + extent: prepareAxisExtentConfig(data.scale, bounds), + showGridLines, + ...getLabelArgs(data, isTimeChart), }); - return buildExpression([categoryAxis]); + return buildExpression([xAxisConfig]); }; -const prepareValueAxis = (data: ValueAxis) => { - const categoryAxis = buildExpressionFunction('valueaxis', { - name: data.name, - axisParams: prepareCategoryAxis({ - ...data, - }), +const prepareYAxis = (data: ValueAxis, showGridLines?: boolean) => { + const yAxisConfig = buildExpressionFunction('yAxisConfig', { + id: data.id, + hide: !data.show, + position: getYAxisPosition(data.position), + title: data.title.text, + extent: prepareAxisExtentConfig(data.scale), + boundsMargin: data.scale.boundsMargin, + scaleType: getScaleType(data.scale, true), + mode: data.scale.mode, + showGridLines, + ...getLabelArgs(data), }); - return buildExpression([categoryAxis]); + return buildExpression([yAxisConfig]); }; -const prepareSeriesParam = (data: SeriesParam) => { - const seriesParam = buildExpressionFunction('seriesparam', { - label: data.data.label, - id: data.data.id, - drawLinesBetweenPoints: data.drawLinesBetweenPoints, - interpolate: data.interpolate, - lineWidth: data.lineWidth, - mode: data.mode, - show: data.show, - showCircles: data.showCircles, - circlesRadius: data.circlesRadius, - type: data.type, - valueAxis: data.valueAxis, +const getLineStyle = (style: ThresholdLine['style']) => { + switch (style) { + case 'full': + return 'solid'; + case 'dashed': + case 'dot-dashed': + return style; + } +}; + +const prepareReferenceLine = (thresholdLine: ThresholdLine, axisId: string) => { + const referenceLine = buildExpressionFunction('referenceLine', { + value: thresholdLine.value, + color: thresholdLine.color, + lineWidth: thresholdLine.width, + lineStyle: getLineStyle(thresholdLine.style), + axisId, }); - return buildExpression([seriesParam]); + return buildExpression([referenceLine]); }; -const prepareVisDimension = (data: Dimension) => { +const prepareVisDimension = (data: Dimension | YDimension) => { const visDimension = buildExpressionFunction('visdimension', { accessor: data.accessor }); if (data.format) { @@ -122,16 +307,8 @@ const prepareVisDimension = (data: Dimension) => { return buildExpression([visDimension]); }; -const prepareXYDimension = (data: Dimension) => { - const xyDimension = buildExpressionFunction('xydimension', { - params: JSON.stringify(data.params), - aggType: data.aggType, - label: data.label, - visDimension: prepareVisDimension(data), - }); - - return buildExpression([xyDimension]); -}; +export const isDateHistogramParams = (params: Dimension['params']): params is DateHistogramParams => + (params as DateHistogramParams).date; export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const schemas = getVisSchemas(vis, params); @@ -158,9 +335,12 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params const finalSeriesParams = updatedSeries ?? vis.params.seriesParams; + let isHistogram = false; + if (dimensions.x) { const xAgg = responseAggs[dimensions.x.accessor] as any; if (xAgg.type.name === BUCKET_TYPES.DATE_HISTOGRAM) { + isHistogram = true; (dimensions.x.params as DateHistogramParams).date = true; const { esUnit, esValue } = xAgg.buckets.getInterval(); (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; @@ -178,6 +358,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params }; } } else if (xAgg.type.name === BUCKET_TYPES.HISTOGRAM) { + isHistogram = true; const intervalParam = xAgg.type.paramByName('interval'); const output = { params: {} as any }; await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { @@ -203,47 +384,109 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params } } }); + let legendSize = vis.params.legendSize; if (vis.params.legendPosition === Position.Top || vis.params.legendPosition === Position.Bottom) { legendSize = LegendSize.AUTO; } - const visTypeXy = buildExpressionFunction(visName, { - type: vis.type.name as XyVisType, - chartType: vis.params.type, - addTimeMarker: vis.params.addTimeMarker, - truncateLegend: vis.params.truncateLegend, - maxLegendLines: vis.params.maxLegendLines, - legendSize, - addLegend: vis.params.addLegend, - addTooltip: vis.params.addTooltip, - legendPosition: vis.params.legendPosition, + const yAccessors = (dimensions.y || []).reduce>( + (acc, yDimension) => { + const yAgg = responseAggs[yDimension.accessor]; + const aggId = getSafeId(yAgg.id); + const dimension: YDimension = { + ...yDimension, + accessor: getCorrectAccessor(yDimension, yAgg.id), + }; + if (acc[aggId]) { + acc[aggId].push(dimension); + } else { + acc[aggId] = [dimension]; + } + return acc; + }, + {} + ); + + const xScale = vis.params.categoryAxes[0].scale; + + let mapColumn; + + if (!dimensions.x) { + mapColumn = buildExpressionFunction('mapColumn', { + id: 'all', + expression: '_all', + name: i18n.translate('visTypeXy.allDocsTitle', { + defaultMessage: 'All docs', + }), + }); + } + + const visibleSeries = finalSeriesParams.filter( + (param) => param.show && yAccessors[param.data.id] + ); + + const visTypeXy = buildExpressionFunction('layeredXyVis', { + layers: [ + ...visibleSeries.map((seriesParams) => + prepareLayers( + seriesParams, + isHistogram, + vis.params.valueAxes, + yAccessors[seriesParams.data.id], + dimensions.x, + dimensions.series, + dimensions.z ? dimensions.z[0] : undefined, + vis.params.palette, + xScale + ) + ), + ...(vis.params.thresholdLine.show + ? [prepareReferenceLine(vis.params.thresholdLine, vis.params.valueAxes[0].id)] + : []), + ], + addTimeMarker: vis.params.addTimeMarker && (dimensions.x?.params as DateHistogramParams)?.date, orderBucketsBySum: vis.params.orderBucketsBySum, - categoryAxes: vis.params.categoryAxes.map(prepareCategoryAxis), - valueAxes: vis.params.valueAxes.map(prepareValueAxis), - seriesParams: finalSeriesParams.map(prepareSeriesParam), - labels: prepareLabel(vis.params.labels), - thresholdLine: prepareThresholdLine(vis.params.thresholdLine), - gridCategoryLines: vis.params.grid.categoryLines, - gridValueAxis: vis.params.grid.valueAxis, - radiusRatio: vis.params.radiusRatio, - isVislibVis: vis.params.isVislibVis, + fittingFunction: vis.params.fittingFunction + ? vis.params.fittingFunction.charAt(0).toUpperCase() + vis.params.fittingFunction.slice(1) + : undefined, detailedTooltip: vis.params.detailedTooltip, - fittingFunction: vis.params.fittingFunction, - times: vis.params.times.map(prepareTimeMarker), - palette: vis.params.palette.name, fillOpacity: vis.params.fillOpacity, - xDimension: dimensions.x ? prepareXYDimension(dimensions.x) : null, - yDimension: dimensions.y.map(prepareXYDimension), - zDimension: dimensions.z?.map(prepareXYDimension), - widthDimension: dimensions.width?.map(prepareXYDimension), - seriesDimension: dimensions.series?.map(prepareXYDimension), - splitRowDimension: dimensions.splitRow?.map(prepareXYDimension), - splitColumnDimension: dimensions.splitColumn?.map(prepareXYDimension), + showTooltip: vis.params.addTooltip, + markSizeRatio: + dimensions.z && + visibleSeries.some((param) => param.type === ChartType.Area || param.type === ChartType.Line) + ? vis.params.radiusRatio * 0.6 // NOTE: downscale ratio to match current vislib implementation + : undefined, + legend: prepareLengend(vis.params, legendSize), + xAxisConfig: prepareXAxis( + vis.params.categoryAxes[0], + vis.params.grid.categoryLines, + dimensions.x?.params && isDateHistogramParams(dimensions.x?.params) + ? dimensions.x?.params.bounds + : undefined, + dimensions.x?.params && isDateHistogramParams(dimensions.x?.params) + ? dimensions.x?.params.date + : undefined + ), // as we have only one x axis + yAxisConfigs: vis.params.valueAxes + .filter((axis) => visibleSeries.some((seriesParam) => seriesParam.valueAxis === axis.id)) + .map((valueAxis) => prepareYAxis(valueAxis, vis.params.grid.valueAxis === valueAxis.id)), + minTimeBarInterval: + dimensions.x?.params && + isDateHistogramParams(dimensions.x?.params) && + dimensions.x?.params.date && + visibleSeries.some((param) => param.type === ChartType.Histogram) + ? dimensions.x?.params.intervalESValue + dimensions.x?.params.intervalESUnit + : undefined, + splitColumnAccessor: dimensions.splitColumn?.map(prepareVisDimension), + splitRowAccessor: dimensions.splitRow?.map(prepareVisDimension), + valueLabels: vis.params.labels.show ? 'show' : 'hide', + singleTable: true, }); - const ast = buildExpression([visTypeXy]); + const ast = buildExpression(mapColumn ? [mapColumn, visTypeXy] : [visTypeXy]); return ast.toAst(); }; diff --git a/test/functional/apps/dashboard/group1/embeddable_rendering.ts b/test/functional/apps/dashboard/group1/embeddable_rendering.ts index f57b1f1fda83a..c0d5a47dbeb27 100644 --- a/test/functional/apps/dashboard/group1/embeddable_rendering.ts +++ b/test/functional/apps/dashboard/group1/embeddable_rendering.ts @@ -60,9 +60,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // TODO add test for 'animal sound pie' viz // This tests line charts that do not use timeseries data - const dogData = await elasticChart.getChartDebugData('visTypeXyChart', 2); - const pointCount = dogData?.areas?.reduce((acc, a) => { - return acc + a.lines.y1.points.length; + const dogData = await elasticChart.getChartDebugData('xyVisChart', 2); + const pointCount = dogData?.lines?.reduce((acc, a) => { + return acc + a.points.length; }, 0); expect(pointCount).to.equal(6); @@ -83,9 +83,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Three instead of 0 because there is a visualization based off a non time based index that // should still show data. - const dogData = await elasticChart.getChartDebugData('visTypeXyChart'); - const pointCount = dogData?.areas?.reduce((acc, a) => { - return acc + a.lines.y1.points.length; + const dogData = await elasticChart.getChartDebugData('xyVisChart', 2); + const pointCount = dogData?.lines?.reduce((acc, a) => { + return acc + a.points.length; }, 0); expect(pointCount).to.equal(6); diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts index 33d5acceb00d8..8bff6355988f2 100644 --- a/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); const dashboardAddPanel = getService('dashboardAddPanel'); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; const enableNewChartLibraryDebug = async (force = false) => { if ((await PageObjects.visChart.isNewChartsLibraryEnabled()) || force) { diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index 900121f1bcf0f..942986bdb27d6 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', ]); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html diff --git a/test/functional/apps/visualize/group2/_inspector.ts b/test/functional/apps/visualize/group2/_inspector.ts index 7b306f7817f5c..e996c2140718e 100644 --- a/test/functional/apps/visualize/group2/_inspector.ts +++ b/test/functional/apps/visualize/group2/_inspector.ts @@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('inspector table', function indexPatternCreation() { it('should update table header when columns change', async function () { await inspector.open(); - await inspector.expectTableHeaders(['Count']); + await inspector.expectTableHeaders(['Count', 'All docs']); await inspector.close(); log.debug('Add Average Metric on machine.ram field'); @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectField('machine.ram', 'metrics'); await PageObjects.visEditor.clickGo(); await inspector.open(); - await inspector.expectTableHeaders(['Count', 'Average machine.ram']); + await inspector.expectTableHeaders(['Count', 'Average machine.ram', 'All docs']); await inspector.close(); }); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts index 5fbb264910dca..309d1abb9961b 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'header', 'timePicker', ]); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; const vizName = 'Visualization AreaChart Name Test - Charts library'; diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts index 77ddc3bbac1a4..4403c2157465a 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'timePicker', ]); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; describe('line charts - split chart', function () { const initLineChart = async function () { @@ -116,11 +116,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct data, ordered by Term', async function () { const expectedChartData = [ - ['png', '1,373'], - ['php', '445'], - ['jpg', '9,109'], - ['gif', '918'], - ['css', '2,159'], + ['png', '1,373', '_all'], + ['php', '445', '_all'], + ['jpg', '9,109', '_all'], + ['gif', '918', '_all'], + ['css', '2,159', '_all'], ]; await inspector.open(); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts index a46c46fda48ad..c1df1570008a3 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'timePicker', ]); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; describe('line charts - split series', function () { const initLineChart = async function () { @@ -114,11 +114,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct data, ordered by Term', async function () { const expectedChartData = [ - ['png', '1,373'], - ['php', '445'], - ['jpg', '9,109'], - ['gif', '918'], - ['css', '2,159'], + ['png', '1,373', '_all'], + ['php', '445', '_all'], + ['jpg', '9,109', '_all'], + ['gif', '918', '_all'], + ['css', '2,159', '_all'], ]; await inspector.open(); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts index 1a11d19064ce7..d093959f45405 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'common', ]); const inspector = getService('inspector'); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; async function initChart() { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts index b8d5cd64bbc1f..896f2ed238067 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts @@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; describe('vertical bar chart', function () { before(async () => { diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts index 4f00bac7792c4..7d8ffe17566fe 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'visEditor', 'visChart']); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; describe('vertical bar chart with index without time filter', function () { const vizName1 = 'Visualization VerticalBarChart without time filter'; diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 1caae58ad5af6..5521762f865e6 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -135,9 +135,7 @@ export class VisualizeChartPageObject extends FtrService { * @param axis axis value, 'ValueAxis-1' by default */ public async getLineChartData(selector: string, dataLabel = 'Count') { - // For now lines are rendered as areas to enable stacking - const areas = (await this.getEsChartDebugState(selector))?.areas ?? []; - const lines = areas.map(({ lines: { y1 }, name, color }) => ({ ...y1, name, color })); + const lines = (await this.getEsChartDebugState(selector))?.lines ?? []; const points = lines.find(({ name }) => name === dataLabel)?.points ?? []; return points.map(({ y }) => y); } diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index 7bbaa256f0360..7a0d5be047712 100644 Binary files a/test/functional/screenshots/baseline/area_chart.png and b/test/functional/screenshots/baseline/area_chart.png differ diff --git a/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap index 60afa4fe4ea88..2c58f75779456 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap @@ -5,9 +5,6 @@ Object { "chain": Array [ Object { "arguments": Object { - "curveType": Array [ - "LINEAR", - ], "emphasizeFitting": Array [ true, ], @@ -35,6 +32,9 @@ Object { "columnToLabel": Array [ "{\\"b\\":\\"col_b\\",\\"c\\":\\"col_c\\",\\"d\\":\\"col_d\\"}", ], + "curveType": Array [ + "LINEAR", + ], "decorations": Array [], "isHistogram": Array [ false, @@ -73,12 +73,6 @@ Object { "splitAccessors": Array [ "d", ], - "table": Array [ - Object { - "chain": Array [], - "type": "expression", - }, - ], "xAccessor": Array [ "a", ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index 16fd8576b1989..167811a7f8691 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -10,6 +10,7 @@ import { Position, ScaleType } from '@elastic/charts'; import type { PaletteRegistry } from '@kbn/coloring'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { LegendSize } from '@kbn/visualizations-plugin/public'; +import { XYCurveType } from '@kbn/expression-xy-plugin/common'; import { State, YConfig, @@ -296,7 +297,6 @@ export const buildExpression = ( fittingFunction: [state.fittingFunction || 'None'], endValue: [state.endValue || 'None'], emphasizeFitting: [state.emphasizeFitting || false], - curveType: [state.curveType || 'LINEAR'], fillOpacity: [state.fillOpacity || 0.3], valueLabels: [state?.valueLabels || 'hide'], hideEndzones: [state?.hideEndzones || false], @@ -331,7 +331,8 @@ export const buildExpression = ( datasourceLayers[layer.layerId], metadata, paletteService, - datasourceExpressionsByLayers[layer.layerId] + datasourceExpressionsByLayers[layer.layerId], + state.curveType || 'LINEAR' ) ), ...validReferenceLayers.map((layer) => @@ -429,7 +430,8 @@ const dataLayerToExpression = ( datasourceLayer: DatasourcePublicAPI | undefined, metadata: Record>, paletteService: PaletteRegistry, - datasourceExpression: Ast + datasourceExpression: Ast, + curveType: XYCurveType ): Ast => { const columnToLabel = getColumnToLabelMap(layer, datasourceLayer); @@ -451,6 +453,24 @@ const dataLayerToExpression = ( return { type: 'expression', chain: [ + ...(datasourceExpression + ? [ + ...datasourceExpression.chain, + ...(layer.collapseFn + ? [ + { + type: 'function', + function: 'lens_collapse', + arguments: { + by: layer.xAccessor ? [layer.xAccessor] : [], + metric: layer.accessors, + fn: [layer.collapseFn!], + }, + } as AstFunction, + ] + : []), + ] + : []), { type: 'function', function: 'extendedDataLayer', @@ -469,35 +489,11 @@ const dataLayerToExpression = ( yConfigToDataDecorationConfigExpression(yConfig, yAxisConfigs) ) : [], + curveType: [curveType], seriesType: [seriesType], showLines: seriesType === 'line' || seriesType === 'area' ? [true] : [false], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], - ...(datasourceExpression - ? { - table: [ - { - ...datasourceExpression, - chain: [ - ...datasourceExpression.chain, - ...(layer.collapseFn - ? [ - { - type: 'function', - function: 'lens_collapse', - arguments: { - by: layer.xAccessor ? [layer.xAccessor] : [], - metric: layer.accessors, - fn: [layer.collapseFn!], - }, - } as AstFunction, - ] - : []), - ], - }, - ], - } - : {}), palette: [ { type: 'expression', diff --git a/x-pack/test/functional/fixtures/kbn_archiver/dashboard_async/async_search.json b/x-pack/test/functional/fixtures/kbn_archiver/dashboard_async/async_search.json index 8dbd2dedeaf72..08a64e34168f1 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/dashboard_async/async_search.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/dashboard_async/async_search.json @@ -50,7 +50,7 @@ "title": "Sum of Bytes by Extension (Delayed 5s)", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Sum of Bytes by Extension (Delayed 5s)\",\"type\":\"histogram\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"params\":{\"field\":\"bytes\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"extension.raw\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"},{\"id\":\"3\",\"enabled\":true,\"type\":\"shard_delay\",\"params\":{\"delay\":\"5s\"},\"schema\":\"group\"}],\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Sum of bytes\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Sum of bytes\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"circlesRadius\":1}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"labels\":{\"show\":false},\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"row\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"legendSize\":\"auto\"}}" + "visState": "{\"title\":\"Sum of Bytes by Extension (Delayed 5s)\",\"type\":\"histogram\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"params\":{\"field\":\"bytes\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"extension.raw\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"},{\"id\":\"3\",\"enabled\":true,\"type\":\"shard_delay\",\"params\":{\"delay\":\"5s\"},\"schema\":\"split\"}],\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Sum of bytes\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Sum of bytes\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"circlesRadius\":1}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"labels\":{\"show\":false},\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"row\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"legendSize\":\"auto\"}}" }, "coreMigrationVersion": "8.4.0", "id": "6c9f3830-01e3-11eb-9b63-176d7b28a352", diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts index 95f2a1f926ad7..d37d8a937601a 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const queryBar = getService('queryBar'); const elasticChart = getService('elasticChart'); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; const enableNewChartLibraryDebug = async () => { await elasticChart.setNewChartUiDebugFlag(); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts index 41ecf3b3015d4..ded0f097abdc7 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts @@ -104,7 +104,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.switchToEditMode(); await searchSessions.expectState('restored'); - const xyChartSelector = 'visTypeXyChart'; + const xyChartSelector = 'xyVisChart'; await enableNewChartLibraryDebug(); const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Sum of bytes'); expect(data.length).to.be(5);