diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8856f7f0aaabb2..97e9f23784f60b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -128,5 +128,5 @@ pageLoadAssetSize: eventAnnotation: 19334 screenshotting: 22870 synthetics: 40958 - expressionXY: 31000 + expressionXY: 33000 kibanaUsageCollection: 16463 diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts index 68ac2963c96469..fc2e41700b94fb 100644 --- a/src/plugins/chart_expressions/expression_xy/common/constants.ts +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -9,6 +9,7 @@ export const XY_VIS = 'xyVis'; export const LAYERED_XY_VIS = 'layeredXyVis'; export const Y_CONFIG = 'yConfig'; +export const REFERENCE_LINE_Y_CONFIG = 'referenceLineYConfig'; export const EXTENDED_Y_CONFIG = 'extendedYConfig'; export const DATA_LAYER = 'dataLayer'; export const EXTENDED_DATA_LAYER = 'extendedDataLayer'; @@ -19,8 +20,8 @@ export const ANNOTATION_LAYER = 'annotationLayer'; export const EXTENDED_ANNOTATION_LAYER = 'extendedAnnotationLayer'; export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; +export const REFERENCE_LINE = 'referenceLine'; export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; -export const EXTENDED_REFERENCE_LINE_LAYER = 'extendedReferenceLineLayer'; export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts deleted file mode 100644 index d85f5ae2b2f770..00000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { EXTENDED_Y_CONFIG } from '../constants'; -import { strings } from '../i18n'; -import { ReferenceLineLayerFn, ExtendedReferenceLineLayerFn } from '../types'; - -type CommonReferenceLineLayerFn = ReferenceLineLayerFn | ExtendedReferenceLineLayerFn; - -export const commonReferenceLineLayerArgs: Omit = { - yConfig: { - types: [EXTENDED_Y_CONFIG], - help: strings.getRLYConfigHelp(), - multi: true, - }, - columnToLabel: { - types: ['string'], - help: strings.getColumnToLabelHelp(), - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts deleted file mode 100644 index 41b264cf53a4db..00000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, EXTENDED_REFERENCE_LINE_LAYER } from '../constants'; -import { ExtendedReferenceLineLayerFn } from '../types'; -import { strings } from '../i18n'; -import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args'; - -export const extendedReferenceLineLayerFunction: ExtendedReferenceLineLayerFn = { - name: EXTENDED_REFERENCE_LINE_LAYER, - aliases: [], - type: EXTENDED_REFERENCE_LINE_LAYER, - help: strings.getRLHelp(), - inputTypes: ['datatable'], - args: { - ...commonReferenceLineLayerArgs, - accessors: { - types: ['string'], - help: strings.getRLAccessorsHelp(), - multi: true, - }, - table: { - types: ['datatable'], - help: strings.getTableHelp(), - }, - layerId: { - types: ['string'], - help: strings.getLayerIdHelp(), - }, - }, - fn(input, args) { - const table = args.table ?? input; - const accessors = args.accessors ?? []; - accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); - - return { - type: EXTENDED_REFERENCE_LINE_LAYER, - ...args, - accessors: args.accessors ?? [], - layerType: LayerTypes.REFERENCELINE, - table, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts index 30a76217b5c0ea..dc82220db6e238 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -18,6 +18,6 @@ export * from './grid_lines_config'; export * from './axis_extent_config'; export * from './tick_labels_config'; export * from './labels_orientation_config'; +export * from './reference_line'; export * from './reference_line_layer'; -export * from './extended_reference_line_layer'; export * from './axis_titles_visibility_config'; 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 695bd16613715a..f419891e079ea7 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 @@ -6,10 +6,11 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { LayeredXyVisFn } from '../types'; import { EXTENDED_DATA_LAYER, - EXTENDED_REFERENCE_LINE_LAYER, + REFERENCE_LINE_LAYER, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, } from '../constants'; @@ -24,8 +25,10 @@ export const layeredXyVisFunction: LayeredXyVisFn = { args: { ...commonXYArgs, layers: { - types: [EXTENDED_DATA_LAYER, EXTENDED_REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER], - help: strings.getLayersHelp(), + types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER], + help: i18n.translate('expressionXY.layeredXyVis.layers.help', { + defaultMessage: 'Layers of visual series', + }), multi: true, }, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts new file mode 100644 index 00000000000000..b96f39923fab2e --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; +import { ReferenceLineArgs, ReferenceLineConfigResult } from '../types'; +import { referenceLineFunction } from './reference_line'; + +describe('referenceLine', () => { + test('produces the correct arguments for minimum arguments', async () => { + const args: ReferenceLineArgs = { + value: 100, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: false, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('produces the correct arguments for maximum arguments', async () => { + const args: ReferenceLineArgs = { + name: 'some value', + value: 100, + icon: 'alert', + iconPosition: 'below', + axisMode: 'bottom', + lineStyle: 'solid', + lineWidth: 10, + color: '#fff', + fill: 'below', + textVisibility: true, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('adds text visibility if name is provided ', async () => { + const args: ReferenceLineArgs = { + name: 'some name', + value: 100, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: true, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('hides text if textVisibility is true and no text is provided', async () => { + const args: ReferenceLineArgs = { + value: 100, + textVisibility: true, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: false, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('applies text visibility if name is provided', async () => { + const checktextVisibility = (textVisibility: boolean = false) => { + const args: ReferenceLineArgs = { + value: 100, + name: 'some text', + textVisibility, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility, + }, + ], + }; + expect(result).toEqual(expectedResult); + }; + + checktextVisibility(); + checktextVisibility(true); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts new file mode 100644 index 00000000000000..c294d6ca5aaecf --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts @@ -0,0 +1,114 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + AvailableReferenceLineIcons, + FillStyles, + IconPositions, + LayerTypes, + LineStyles, + REFERENCE_LINE, + REFERENCE_LINE_Y_CONFIG, + YAxisModes, +} from '../constants'; +import { ReferenceLineFn } from '../types'; +import { strings } from '../i18n'; + +export const referenceLineFunction: ReferenceLineFn = { + name: REFERENCE_LINE, + aliases: [], + type: REFERENCE_LINE, + help: strings.getRLHelp(), + inputTypes: ['datatable', 'null'], + args: { + name: { + types: ['string'], + help: strings.getReferenceLineNameHelp(), + }, + value: { + types: ['number'], + help: strings.getReferenceLineValueHelp(), + required: true, + }, + axisMode: { + types: ['string'], + options: [...Object.values(YAxisModes)], + help: strings.getAxisModeHelp(), + default: YAxisModes.AUTO, + strict: true, + }, + color: { + types: ['string'], + help: strings.getColorHelp(), + }, + lineStyle: { + types: ['string'], + options: [...Object.values(LineStyles)], + help: i18n.translate('expressionXY.yConfig.lineStyle.help', { + defaultMessage: 'The style of the reference line', + }), + default: LineStyles.SOLID, + strict: true, + }, + lineWidth: { + types: ['number'], + help: i18n.translate('expressionXY.yConfig.lineWidth.help', { + defaultMessage: 'The width of the reference line', + }), + default: 1, + }, + icon: { + types: ['string'], + help: i18n.translate('expressionXY.yConfig.icon.help', { + defaultMessage: 'An optional icon used for reference lines', + }), + options: [...Object.values(AvailableReferenceLineIcons)], + strict: true, + }, + iconPosition: { + types: ['string'], + options: [...Object.values(IconPositions)], + help: i18n.translate('expressionXY.yConfig.iconPosition.help', { + defaultMessage: 'The placement of the icon for the reference line', + }), + default: IconPositions.AUTO, + strict: true, + }, + textVisibility: { + types: ['boolean'], + help: i18n.translate('expressionXY.yConfig.textVisibility.help', { + defaultMessage: 'Visibility of the label on the reference line', + }), + }, + fill: { + types: ['string'], + options: [...Object.values(FillStyles)], + help: i18n.translate('expressionXY.yConfig.fill.help', { + defaultMessage: 'Fill', + }), + default: FillStyles.NONE, + strict: true, + }, + }, + fn(table, args) { + const textVisibility = + args.name !== undefined && args.textVisibility === undefined + ? true + : args.name === undefined + ? false + : args.textVisibility; + + return { + type: REFERENCE_LINE, + layerType: LayerTypes.REFERENCELINE, + lineLength: table?.rows.length ?? 0, + yConfig: [{ ...args, textVisibility, type: REFERENCE_LINE_Y_CONFIG }], + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts index 04c06f92d616f3..6b51edd2d209e0 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -7,10 +7,9 @@ */ import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, REFERENCE_LINE_LAYER } from '../constants'; +import { LayerTypes, REFERENCE_LINE_LAYER, EXTENDED_Y_CONFIG } from '../constants'; import { ReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; -import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args'; export const referenceLineLayerFunction: ReferenceLineLayerFn = { name: REFERENCE_LINE_LAYER, @@ -19,14 +18,31 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { help: strings.getRLHelp(), inputTypes: ['datatable'], args: { - ...commonReferenceLineLayerArgs, accessors: { - types: ['string', 'vis_dimension'], + types: ['string'], help: strings.getRLAccessorsHelp(), multi: true, }, + yConfig: { + types: [EXTENDED_Y_CONFIG], + help: strings.getRLYConfigHelp(), + multi: true, + }, + columnToLabel: { + types: ['string'], + help: strings.getColumnToLabelHelp(), + }, + table: { + types: ['datatable'], + help: strings.getTableHelp(), + }, + layerId: { + types: ['string'], + help: strings.getLayerIdHelp(), + }, }, - fn(table, args) { + fn(input, args) { + const table = args.table ?? input; const accessors = args.accessors ?? []; accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); @@ -34,8 +50,7 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { type: REFERENCE_LINE_LAYER, ...args, layerType: LayerTypes.REFERENCELINE, - accessors, - table, + table: args.table ?? input, }; }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 8ec19614166389..73d4444217d908 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -30,11 +30,12 @@ describe('xyVis', () => { } ), } as Datatable; + const { layers, ...rest } = args; const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; const result = await xyVisFunction.fn( newData, - { ...rest, ...restLayerArgs, referenceLineLayers: [], annotationLayers: [] }, + { ...rest, ...restLayerArgs, referenceLines: [], annotationLayers: [] }, createMockExecutionContext() ); @@ -60,7 +61,7 @@ describe('xyVis', () => { ...rest, ...{ ...sampleLayer, markSizeAccessor: 'b' }, markSizeRatio: 0, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -74,7 +75,7 @@ describe('xyVis', () => { ...rest, ...{ ...sampleLayer, markSizeAccessor: 'b' }, markSizeRatio: 101, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -92,7 +93,7 @@ describe('xyVis', () => { ...rest, ...restLayerArgs, minTimeBarInterval: '1q', - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -111,7 +112,7 @@ describe('xyVis', () => { ...rest, ...restLayerArgs, minTimeBarInterval: '1h', - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -131,7 +132,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], splitRowAccessor, }, @@ -152,7 +153,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], splitColumnAccessor, }, @@ -172,7 +173,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], markSizeRatio: 5, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 37baf028178ccb..7d2783cf6f1cde 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -7,7 +7,7 @@ */ import { XyVisFn } from '../types'; -import { XY_VIS, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants'; +import { XY_VIS, REFERENCE_LINE, ANNOTATION_LAYER } from '../constants'; import { strings } from '../i18n'; import { commonXYArgs } from './common_xy_args'; import { commonDataLayerArgs } from './common_data_layer_args'; @@ -33,9 +33,9 @@ export const xyVisFunction: XyVisFn = { help: strings.getAccessorsHelp(), multi: true, }, - referenceLineLayers: { - types: [REFERENCE_LINE_LAYER], - help: strings.getReferenceLineLayerHelp(), + referenceLines: { + types: [REFERENCE_LINE], + help: strings.getReferenceLinesHelp(), multi: true, }, annotationLayers: { 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 e879f33b76548f..3de2dd35831e40 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 @@ -13,7 +13,7 @@ import { } 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 } from '../constants'; +import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER, REFERENCE_LINE } from '../constants'; import { appendLayerIds, getAccessors, normalizeTable } from '../helpers'; import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types'; import { getLayerDimensions } from '../utils'; @@ -53,7 +53,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateAccessor(args.splitColumnAccessor, data.columns); const { - referenceLineLayers = [], + referenceLines = [], annotationLayers = [], // data_layer args seriesType, @@ -81,7 +81,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const layers: XYLayerConfig[] = [ ...appendLayerIds(dataLayers, 'dataLayers'), - ...appendLayerIds(referenceLineLayers, 'referenceLineLayers'), + ...appendLayerIds(referenceLines, 'referenceLines'), ...appendLayerIds(annotationLayers, 'annotationLayers'), ]; @@ -90,7 +90,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { handlers.inspectorAdapters.tables.allowCsvExport = true; const layerDimensions = layers.reduce((dimensions, layer) => { - if (layer.layerType === LayerTypes.ANNOTATIONS) { + if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { return dimensions; } diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts index a3eea973fbf912..895abdb7a60df4 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts @@ -63,7 +63,7 @@ describe('#getDataLayers', () => { palette: { type: 'system_palette', name: 'system' }, }, { - type: 'extendedReferenceLineLayer', + type: 'referenceLineLayer', layerType: 'referenceLine', accessors: ['y'], table: { rows: [], columns: [], type: 'datatable' }, 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 f3425ec2db625e..ba26bb973f64fe 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -93,9 +93,9 @@ export const strings = { i18n.translate('expressionXY.xyVis.dataLayer.help', { defaultMessage: 'Data layer of visual series', }), - getReferenceLineLayerHelp: () => - i18n.translate('expressionXY.xyVis.referenceLineLayer.help', { - defaultMessage: 'Reference line layer', + getReferenceLinesHelp: () => + i18n.translate('expressionXY.xyVis.referenceLines.help', { + defaultMessage: 'Reference line', }), getAnnotationLayerHelp: () => i18n.translate('expressionXY.xyVis.annotationLayer.help', { @@ -237,4 +237,12 @@ export const strings = { i18n.translate('expressionXY.annotationLayer.annotations.help', { defaultMessage: 'Annotations', }), + getReferenceLineNameHelp: () => + i18n.translate('expressionXY.referenceLine.name.help', { + defaultMessage: 'Reference line name', + }), + getReferenceLineValueHelp: () => + i18n.translate('expressionXY.referenceLine.Value.help', { + defaultMessage: 'Reference line value', + }), }; diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index 7211a7a7db1b76..005f6c2867c180 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -58,6 +58,5 @@ export type { ReferenceLineLayerConfigResult, CommonXYReferenceLineLayerConfig, AxisTitlesVisibilityConfigResult, - ExtendedReferenceLineLayerConfigResult, CommonXYReferenceLineLayerConfigResult, } from './types'; 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 0e10f680811ec9..0a7b93c495c29d 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 @@ -26,7 +26,7 @@ import { XYCurveTypes, YAxisModes, YScaleTypes, - REFERENCE_LINE_LAYER, + REFERENCE_LINE, Y_CONFIG, AXIS_TITLES_VISIBILITY_CONFIG, LABELS_ORIENTATION_CONFIG, @@ -36,7 +36,7 @@ import { DATA_LAYER, AXIS_EXTENT_CONFIG, EXTENDED_DATA_LAYER, - EXTENDED_REFERENCE_LINE_LAYER, + REFERENCE_LINE_LAYER, ANNOTATION_LAYER, EndValues, EXTENDED_Y_CONFIG, @@ -44,6 +44,7 @@ import { XY_VIS, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, + REFERENCE_LINE_Y_CONFIG, } from '../constants'; import { XYRender } from './expression_renderers'; @@ -194,7 +195,7 @@ export interface XYArgs extends DataLayerArgs { endValue?: EndValue; emphasizeFitting?: boolean; valueLabels: ValueLabelMode; - referenceLineLayers: ReferenceLineLayerConfigResult[]; + referenceLines: ReferenceLineConfigResult[]; annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; @@ -287,13 +288,12 @@ export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & layerType: typeof LayerTypes.ANNOTATIONS; }; -export interface ReferenceLineLayerArgs { - accessors: Array; - columnToLabel?: string; - yConfig?: ExtendedYConfigResult[]; +export interface ReferenceLineArgs extends Omit { + name?: string; + value: number; } -export interface ExtendedReferenceLineLayerArgs { +export interface ReferenceLineLayerArgs { layerId?: string; accessors: string[]; columnToLabel?: string; @@ -301,26 +301,31 @@ export interface ExtendedReferenceLineLayerArgs { table?: Datatable; } -export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs; -export type XYLayerConfig = DataLayerConfig | ReferenceLineLayerConfig | AnnotationLayerConfig; +export type XYLayerArgs = DataLayerArgs | ReferenceLineArgs | AnnotationLayerArgs; +export type XYLayerConfig = DataLayerConfig | ReferenceLineConfig | AnnotationLayerConfig; export type XYExtendedLayerConfig = | ExtendedDataLayerConfig - | ExtendedReferenceLineLayerConfig + | ReferenceLineLayerConfig | ExtendedAnnotationLayerConfig; export type XYExtendedLayerConfigResult = | ExtendedDataLayerConfigResult - | ExtendedReferenceLineLayerConfigResult + | ReferenceLineLayerConfigResult | ExtendedAnnotationLayerConfigResult; -export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { - type: typeof REFERENCE_LINE_LAYER; +export interface ReferenceLineYConfig extends ReferenceLineArgs { + type: typeof REFERENCE_LINE_Y_CONFIG; +} + +export interface ReferenceLineConfigResult { + type: typeof REFERENCE_LINE; layerType: typeof LayerTypes.REFERENCELINE; - table: Datatable; -}; + lineLength: number; + yConfig: [ReferenceLineYConfig]; +} -export type ExtendedReferenceLineLayerConfigResult = ExtendedReferenceLineLayerArgs & { - type: typeof EXTENDED_REFERENCE_LINE_LAYER; +export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { + type: typeof REFERENCE_LINE_LAYER; layerType: typeof LayerTypes.REFERENCELINE; table: Datatable; }; @@ -337,11 +342,11 @@ export interface WithLayerId { } export type DataLayerConfig = DataLayerConfigResult & WithLayerId; -export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId; +export type ReferenceLineConfig = ReferenceLineConfigResult & WithLayerId; export type AnnotationLayerConfig = AnnotationLayerConfigResult & WithLayerId; export type ExtendedDataLayerConfig = ExtendedDataLayerConfigResult & WithLayerId; -export type ExtendedReferenceLineLayerConfig = ExtendedReferenceLineLayerConfigResult & WithLayerId; +export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId; export type ExtendedAnnotationLayerConfig = ExtendedAnnotationLayerConfigResult & WithLayerId; export type ExtendedDataLayerConfigResult = Omit & { @@ -370,13 +375,11 @@ export type TickLabelsConfigResult = AxesSettingsConfig & { type: typeof TICK_LA export type CommonXYLayerConfig = XYLayerConfig | XYExtendedLayerConfig; export type CommonXYDataLayerConfigResult = DataLayerConfigResult | ExtendedDataLayerConfigResult; export type CommonXYReferenceLineLayerConfigResult = - | ReferenceLineLayerConfigResult - | ExtendedReferenceLineLayerConfigResult; + | ReferenceLineConfigResult + | ReferenceLineLayerConfigResult; export type CommonXYDataLayerConfig = DataLayerConfig | ExtendedDataLayerConfig; -export type CommonXYReferenceLineLayerConfig = - | ReferenceLineLayerConfig - | ExtendedReferenceLineLayerConfig; +export type CommonXYReferenceLineLayerConfig = ReferenceLineConfig | ReferenceLineLayerConfig; export type CommonXYAnnotationLayerConfig = AnnotationLayerConfig | ExtendedAnnotationLayerConfig; @@ -400,18 +403,18 @@ export type ExtendedDataLayerFn = ExpressionFunctionDefinition< Promise >; +export type ReferenceLineFn = ExpressionFunctionDefinition< + typeof REFERENCE_LINE, + Datatable | null, + ReferenceLineArgs, + ReferenceLineConfigResult +>; export type ReferenceLineLayerFn = ExpressionFunctionDefinition< typeof REFERENCE_LINE_LAYER, Datatable, ReferenceLineLayerArgs, ReferenceLineLayerConfigResult >; -export type ExtendedReferenceLineLayerFn = ExpressionFunctionDefinition< - typeof EXTENDED_REFERENCE_LINE_LAYER, - Datatable, - ExtendedReferenceLineLayerArgs, - ExtendedReferenceLineLayerConfigResult ->; export type YConfigFn = ExpressionFunctionDefinition; export type ExtendedYConfigFn = ExpressionFunctionDefinition< 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 79a3cbd2eef196..44026b30ed4932 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 @@ -8,13 +8,9 @@ import { ExecutionContext } from '@kbn/expressions-plugin'; import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes } from '../constants'; +import { LayerTypes, REFERENCE_LINE } from '../constants'; import { strings } from '../i18n'; -import { - CommonXYDataLayerConfig, - CommonXYLayerConfig, - CommonXYReferenceLineLayerConfig, -} from '../types'; +import { CommonXYDataLayerConfig, CommonXYLayerConfig, ReferenceLineLayerConfig } from '../types'; export const logDatatables = (layers: CommonXYLayerConfig[], handlers: ExecutionContext) => { if (!handlers?.inspectorAdapters?.tables) { @@ -25,16 +21,17 @@ export const logDatatables = (layers: CommonXYLayerConfig[], handlers: Execution handlers.inspectorAdapters.tables.allowCsvExport = true; layers.forEach((layer) => { - if (layer.layerType === LayerTypes.ANNOTATIONS) { + if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { return; } + const logTable = prepareLogTable(layer.table, getLayerDimensions(layer), true); handlers.inspectorAdapters.tables.logDatatable(layer.layerId, logTable); }); }; export const getLayerDimensions = ( - layer: CommonXYDataLayerConfig | CommonXYReferenceLineLayerConfig + layer: CommonXYDataLayerConfig | ReferenceLineLayerConfig ): Dimension[] => { let xAccessor; let splitAccessor; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 842baeb82d78d3..6d76a230737edb 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -7,7 +7,7 @@ */ import './annotations.scss'; -import './reference_lines.scss'; +import './reference_lines/reference_lines.scss'; import React from 'react'; import { snakeCase } from 'lodash'; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx deleted file mode 100644 index 23e5011fe54a70..00000000000000 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/* - * 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 { LineAnnotation, RectAnnotation } from '@elastic/charts'; -import { shallow } from 'enzyme'; -import React from 'react'; -import { Datatable } from '@kbn/expressions-plugin/common'; -import { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { LayerTypes } from '../../common/constants'; -import { - ReferenceLineLayerArgs, - ReferenceLineLayerConfig, - ExtendedYConfig, -} from '../../common/types'; -import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines'; - -const row: Record = { - xAccessorFirstId: 1, - xAccessorSecondId: 2, - yAccessorLeftFirstId: 5, - yAccessorLeftSecondId: 10, - yAccessorRightFirstId: 5, - yAccessorRightSecondId: 10, -}; - -const data: Datatable = { - type: 'datatable', - rows: [row], - columns: Object.keys(row).map((id) => ({ - id, - name: `Static value: ${row[id]}`, - meta: { - type: 'number', - params: { id: 'number' }, - }, - })), -}; - -function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] { - return [ - { - layerId: 'first', - accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), - yConfig: yConfigs, - type: 'referenceLineLayer', - layerType: LayerTypes.REFERENCELINE, - table: data, - }, - ]; -} - -interface YCoords { - y0: number | undefined; - y1: number | undefined; -} -interface XCoords { - x0: number | undefined; - x1: number | undefined; -} - -function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] { - return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; -} - -const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; - -describe('ReferenceLineAnnotations', () => { - describe('with fill', () => { - let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - let defaultProps: Omit; - - beforeEach(() => { - formatters = { - left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - }; - - defaultProps = { - formatters, - isHorizontal: false, - axesMap: { left: true, right: false }, - paddingMap: {}, - }; - }); - - it.each([ - ['yAccessorLeft', 'above'], - ['yAccessorLeft', 'below'], - ['yAccessorRight', 'above'], - ['yAccessorRight', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( - 'should render a RectAnnotation for a reference line with fill set: %s %s', - (layerPrefix, fill) => { - const axisMode = getAxisFromId(layerPrefix); - const wrapper = shallow( - - ); - - const y0 = fill === 'above' ? 5 : undefined; - const y1 = fill === 'above' ? undefined : 5; - - expect(wrapper.find(LineAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { x0: undefined, x1: undefined, y0, y1 }, - details: y0 ?? y1, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['xAccessor', 'above'], - ['xAccessor', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( - 'should render a RectAnnotation for a reference line with fill set: %s %s', - (layerPrefix, fill) => { - const wrapper = shallow( - - ); - - const x0 = fill === 'above' ? 1 : undefined; - const x1 = fill === 'above' ? undefined : 1; - - expect(wrapper.find(LineAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, x0, x1 }, - details: x0 ?? x1, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( - 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', - (layerPrefix, fill, coordsA, coordsB) => { - const axisMode = getAxisFromId(layerPrefix); - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.y0 ?? coordsA.y1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.y1 ?? coordsB.y0, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], - ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], - ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( - 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', - (layerPrefix, fill, coordsA, coordsB) => { - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.x0 ?? coordsA.x1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.x1 ?? coordsB.x0, - header: undefined, - }, - ]) - ); - } - ); - - it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( - 'should let areas in different directions overlap: %s', - (layerPrefix) => { - const axisMode = getAxisFromId(layerPrefix); - - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, - details: axisMode === 'bottom' ? 1 : 5, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, - details: axisMode === 'bottom' ? 2 : 10, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)( - 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', - (fill, coordsA, coordsB) => { - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.y0 ?? coordsA.y1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.y1 ?? coordsB.y0, - header: undefined, - }, - ]) - ); - } - ); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx deleted file mode 100644 index d17dbf2a70ad17..00000000000000 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/* - * 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 './reference_lines.scss'; - -import React from 'react'; -import { groupBy } from 'lodash'; -import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; -import { euiLightVars } from '@kbn/ui-theme'; -import type { FieldFormat } from '@kbn/field-formats-plugin/common'; -import type { CommonXYReferenceLineLayerConfig, IconPosition, YAxisMode } from '../../common/types'; -import { - LINES_MARKER_SIZE, - mapVerticalToHorizontalPlacement, - Marker, - MarkerBody, -} from '../helpers'; - -export const computeChartMargins = ( - referenceLinePaddings: Partial>, - labelVisibility: Partial>, - titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, - isHorizontal: boolean -) => { - const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = referenceLinePaddings.bottom; - } - if ( - referenceLinePaddings.left && - (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = referenceLinePaddings.left; - } - if ( - referenceLinePaddings.right && - (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = referenceLinePaddings.right; - } - // there's no top axis, so just check if a margin has been computed - if (referenceLinePaddings.top) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = referenceLinePaddings.top; - } - return result; -}; - -// if there's just one axis, put it on the other one -// otherwise use the same axis -// this function assume the chart is vertical -export function getBaseIconPlacement( - iconPosition: IconPosition | undefined, - axesMap?: Record, - axisMode?: YAxisMode -) { - if (iconPosition === 'auto') { - if (axisMode === 'bottom') { - return Position.Top; - } - if (axesMap) { - if (axisMode === 'left') { - return axesMap.right ? Position.Left : Position.Right; - } - return axesMap.left ? Position.Right : Position.Left; - } - } - - if (iconPosition === 'left') { - return Position.Left; - } - if (iconPosition === 'right') { - return Position.Right; - } - if (iconPosition === 'below') { - return Position.Bottom; - } - return Position.Top; -} - -export interface ReferenceLineAnnotationsProps { - layers: CommonXYReferenceLineLayerConfig[]; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - axesMap: Record<'left' | 'right', boolean>; - isHorizontal: boolean; - paddingMap: Partial>; -} - -export const ReferenceLineAnnotations = ({ - layers, - formatters, - axesMap, - isHorizontal, - paddingMap, -}: ReferenceLineAnnotationsProps) => { - return ( - <> - {layers.flatMap((layer) => { - if (!layer.yConfig) { - return []; - } - const { columnToLabel, yConfig: yConfigs, table } = layer; - const columnToLabelMap: Record = columnToLabel - ? JSON.parse(columnToLabel) - : {}; - - const row = table.rows[0]; - - const yConfigByValue = yConfigs.sort( - ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] - ); - - const groupedByDirection = groupBy(yConfigByValue, 'fill'); - if (groupedByDirection.below) { - groupedByDirection.below.reverse(); - } - - return yConfigByValue.flatMap((yConfig, i) => { - // Find the formatter for the given axis - const groupId = - yConfig.axisMode === 'bottom' - ? undefined - : yConfig.axisMode === 'right' - ? 'right' - : 'left'; - - const formatter = formatters[groupId || 'bottom']; - - const defaultColor = euiLightVars.euiColorDarkShade; - - // get the position for vertical chart - const markerPositionVertical = getBaseIconPlacement( - yConfig.iconPosition, - axesMap, - yConfig.axisMode - ); - // the padding map is built for vertical chart - const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; - - const props = { - groupId, - marker: ( - - ), - markerBody: ( - - ), - // rotate the position if required - markerPosition: isHorizontal - ? mapVerticalToHorizontalPlacement(markerPositionVertical) - : markerPositionVertical, - }; - const annotations = []; - - const sharedStyle = { - strokeWidth: yConfig.lineWidth || 1, - stroke: yConfig.color || defaultColor, - dash: - yConfig.lineStyle === 'dashed' - ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] - : yConfig.lineStyle === 'dotted' - ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] - : undefined, - }; - - annotations.push( - ({ - dataValue: row[yConfig.forAccessor], - header: columnToLabelMap[yConfig.forAccessor], - details: formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }))} - domainType={ - yConfig.axisMode === 'bottom' - ? AnnotationDomainType.XDomain - : AnnotationDomainType.YDomain - } - style={{ - line: { - ...sharedStyle, - opacity: 1, - }, - }} - /> - ); - - if (yConfig.fill && yConfig.fill !== 'none') { - const isFillAbove = yConfig.fill === 'above'; - const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( - ({ forAccessor }) => forAccessor === yConfig.forAccessor - ); - const shouldCheckNextReferenceLine = - indexFromSameType < groupedByDirection[yConfig.fill].length - 1; - annotations.push( - { - const nextValue = shouldCheckNextReferenceLine - ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] - : undefined; - if (yConfig.axisMode === 'bottom') { - return { - coordinates: { - x0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - y0: undefined, - x1: isFillAbove ? nextValue : row[yConfig.forAccessor], - y1: undefined, - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - } - return { - coordinates: { - x0: undefined, - y0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - x1: undefined, - y1: isFillAbove ? nextValue : row[yConfig.forAccessor], - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - })} - style={{ - ...sharedStyle, - fill: yConfig.color || defaultColor, - opacity: 0.1, - }} - /> - ); - } - return annotations; - }); - })} - - ); -}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts new file mode 100644 index 00000000000000..62b3b31bf8bd57 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './reference_lines'; +export * from './utils'; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx new file mode 100644 index 00000000000000..74bb18597f2f25 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx @@ -0,0 +1,56 @@ +/* + * 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, { FC } from 'react'; +import { Position } from '@elastic/charts'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { ReferenceLineConfig } from '../../../common/types'; +import { getGroupId } from './utils'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +interface ReferenceLineProps { + layer: ReferenceLineConfig; + paddingMap: Partial>; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +export const ReferenceLine: FC = ({ + layer, + axesMap, + formatters, + paddingMap, + isHorizontal, +}) => { + const { + yConfig: [yConfig], + } = layer; + + if (!yConfig) { + return null; + } + + const { axisMode, value } = yConfig; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + + const formatter = formatters[groupId || 'bottom']; + const id = `${layer.layerId}-${value}`; + + return ( + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx new file mode 100644 index 00000000000000..b5b94b4c2df51a --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx @@ -0,0 +1,137 @@ +/* + * 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 { AnnotationDomainType, LineAnnotation, Position, RectAnnotation } from '@elastic/charts'; +import { euiLightVars } from '@kbn/ui-theme'; +import React, { FC } from 'react'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LINES_MARKER_SIZE } from '../../helpers'; +import { + AvailableReferenceLineIcon, + FillStyle, + IconPosition, + LineStyle, + YAxisMode, +} from '../../../common/types'; +import { + getBaseIconPlacement, + getBottomRect, + getGroupId, + getHorizontalRect, + getLineAnnotationProps, + getSharedStyle, +} from './utils'; + +export interface ReferenceLineAnnotationConfig { + id: string; + name?: string; + value: number; + nextValue?: number; + icon?: AvailableReferenceLineIcon; + lineWidth?: number; + lineStyle?: LineStyle; + fill?: FillStyle; + iconPosition?: IconPosition; + textVisibility?: boolean; + axisMode?: YAxisMode; + color?: string; +} + +interface Props { + config: ReferenceLineAnnotationConfig; + paddingMap: Partial>; + formatter?: FieldFormat; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +const getRectDataValue = ( + annotationConfig: ReferenceLineAnnotationConfig, + formatter: FieldFormat | undefined +) => { + const { name, value, nextValue, fill, axisMode } = annotationConfig; + const isFillAbove = fill === 'above'; + + if (axisMode === 'bottom') { + return getBottomRect(name, isFillAbove, formatter, value, nextValue); + } + + return getHorizontalRect(name, isFillAbove, formatter, value, nextValue); +}; + +export const ReferenceLineAnnotations: FC = ({ + config, + axesMap, + formatter, + paddingMap, + isHorizontal, +}) => { + const { id, axisMode, iconPosition, name, textVisibility, value, fill, color } = config; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + const defaultColor = euiLightVars.euiColorDarkShade; + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement(iconPosition, axesMap, axisMode); + // the padding map is built for vertical chart + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + + const props = getLineAnnotationProps( + config, + { + markerLabel: name, + markerBodyLabel: textVisibility && !hasReducedPadding ? name : undefined, + }, + axesMap, + paddingMap, + groupId, + isHorizontal + ); + + const sharedStyle = getSharedStyle(config); + + const dataValues = { + dataValue: value, + header: name, + details: formatter?.convert(value) || value.toString(), + }; + + const line = ( + + ); + + let rect; + if (fill && fill !== 'none') { + const rectDataValues = getRectDataValue(config, formatter); + + rect = ( + + ); + } + return ( + <> + {line} + {rect} + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx new file mode 100644 index 00000000000000..210f5bda0126bf --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx @@ -0,0 +1,92 @@ +/* + * 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, { FC } from 'react'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { groupBy } from 'lodash'; +import { Position } from '@elastic/charts'; +import { ReferenceLineLayerConfig } from '../../../common/types'; +import { getGroupId } from './utils'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +interface ReferenceLineLayerProps { + layer: ReferenceLineLayerConfig; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + paddingMap: Partial>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +export const ReferenceLineLayer: FC = ({ + layer, + formatters, + paddingMap, + axesMap, + isHorizontal, +}) => { + if (!layer.yConfig) { + return null; + } + + const { columnToLabel, yConfig: yConfigs, table } = layer; + const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; + + const row = table.rows[0]; + + const yConfigByValue = yConfigs.sort( + ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] + ); + + const groupedByDirection = groupBy(yConfigByValue, 'fill'); + if (groupedByDirection.below) { + groupedByDirection.below.reverse(); + } + + const referenceLineElements = yConfigByValue.flatMap((yConfig) => { + const { axisMode } = yConfig; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + + const formatter = formatters[groupId || 'bottom']; + const name = columnToLabelMap[yConfig.forAccessor]; + const value = row[yConfig.forAccessor]; + const yConfigsWithSameDirection = groupedByDirection[yConfig.fill!]; + const indexFromSameType = yConfigsWithSameDirection.findIndex( + ({ forAccessor }) => forAccessor === yConfig.forAccessor + ); + + const shouldCheckNextReferenceLine = indexFromSameType < yConfigsWithSameDirection.length - 1; + + const nextValue = shouldCheckNextReferenceLine + ? row[yConfigsWithSameDirection[indexFromSameType + 1].forAccessor] + : undefined; + + const { forAccessor, type, ...restAnnotationConfig } = yConfig; + const id = `${layer.layerId}-${yConfig.forAccessor}`; + + return ( + + ); + }); + + return <>{referenceLineElements}; +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.scss similarity index 100% rename from src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss rename to src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.scss diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx new file mode 100644 index 00000000000000..35e434d65bc18e --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx @@ -0,0 +1,683 @@ +/* + * 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 { LineAnnotation, RectAnnotation } from '@elastic/charts'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { Datatable } from '@kbn/expressions-plugin/common'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LayerTypes } from '../../../common/constants'; +import { + ReferenceLineLayerArgs, + ReferenceLineLayerConfig, + ExtendedYConfig, + ReferenceLineArgs, + ReferenceLineConfig, +} from '../../../common/types'; +import { ReferenceLines, ReferenceLinesProps } from './reference_lines'; +import { ReferenceLineLayer } from './reference_line_layer'; +import { ReferenceLine } from './reference_line'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +const row: Record = { + xAccessorFirstId: 1, + xAccessorSecondId: 2, + yAccessorLeftFirstId: 5, + yAccessorLeftSecondId: 10, + yAccessorRightFirstId: 5, + yAccessorRightSecondId: 10, +}; + +const data: Datatable = { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((id) => ({ + id, + name: `Static value: ${row[id]}`, + meta: { + type: 'number', + params: { id: 'number' }, + }, + })), +}; + +function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] { + return [ + { + layerId: 'first', + accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), + yConfig: yConfigs, + type: 'referenceLineLayer', + layerType: LayerTypes.REFERENCELINE, + table: data, + }, + ]; +} + +function createReferenceLine( + layerId: string, + lineLength: number, + args: ReferenceLineArgs +): ReferenceLineConfig { + return { + layerId, + type: 'referenceLine', + layerType: 'referenceLine', + lineLength, + yConfig: [{ type: 'referenceLineYConfig', ...args }], + }; +} + +interface YCoords { + y0: number | undefined; + y1: number | undefined; +} +interface XCoords { + x0: number | undefined; + x1: number | undefined; +} + +function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] { + return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; +} + +const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; + +describe('ReferenceLines', () => { + describe('referenceLineLayers', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + + const y0 = fill === 'above' ? 5 : undefined; + const y1 = fill === 'above' ? undefined : 5; + + const referenceLineAnnotation = referenceLineLayer.find(ReferenceLineAnnotations).dive(); + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + + const x0 = fill === 'above' ? 1 : undefined; + const x1 = fill === 'above' ? undefined : 1; + + const referenceLineAnnotation = referenceLineLayer.find(ReferenceLineAnnotations).dive(); + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], + ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.x0 ?? coordsA.x1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.x1 ?? coordsB.x0, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, + details: axisMode === 'bottom' ? 1 : 5, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, + details: axisMode === 'bottom' ? 2 : 10, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', + (fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + }); + + describe('referenceLines', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const value = 5; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + const y0 = fill === 'above' ? value : undefined; + const y1 = fill === 'above' ? undefined : value; + + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const value = 1; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + const x0 = fill === 'above' ? value : undefined; + const x1 = fill === 'above' ? undefined : value; + + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 10, y1: undefined }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: undefined, y1: 5 }], + ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const value = coordsA.y0 ?? coordsA.y1!; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: value, + header: undefined, + }, + ]) + ); + expect(referenceLineAnnotation.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: value, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: undefined }, { x0: 1, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: undefined, x1: 1 }], + ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const value = coordsA.x0 ?? coordsA.x1!; + const wrapper = shallow( + + ); + const referenceLine1 = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation1 = referenceLine1.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation1.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: value, + header: undefined, + }, + ]) + ); + + const referenceLine2 = wrapper.find(ReferenceLine).last().dive(); + const referenceLineAnnotation2 = referenceLine2.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation2.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: value, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + const value1 = 1; + const value2 = 10; + const wrapper = shallow( + + ); + const referenceLine1 = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation1 = referenceLine1.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation1.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { + ...emptyCoords, + ...(axisMode === 'bottom' ? { x0: value1 } : { y0: value1 }), + }, + details: value1, + header: undefined, + }, + ]) + ); + + const referenceLine2 = wrapper.find(ReferenceLine).last().dive(); + const referenceLineAnnotation2 = referenceLine2.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation2.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { + ...emptyCoords, + ...(axisMode === 'bottom' ? { x1: value2 } : { y1: value2 }), + }, + details: value2, + header: undefined, + }, + ]) + ); + } + ); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx new file mode 100644 index 00000000000000..9dca7b6107072e --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx @@ -0,0 +1,79 @@ +/* + * 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 './reference_lines.scss'; + +import React from 'react'; +import { Position } from '@elastic/charts'; +import type { FieldFormat } from '@kbn/field-formats-plugin/common'; +import type { CommonXYReferenceLineLayerConfig } from '../../../common/types'; +import { isReferenceLine, mapVerticalToHorizontalPlacement } from '../../helpers'; +import { ReferenceLineLayer } from './reference_line_layer'; +import { ReferenceLine } from './reference_line'; + +export const computeChartMargins = ( + referenceLinePaddings: Partial>, + labelVisibility: Partial>, + titleVisibility: Partial>, + axesMap: Record<'left' | 'right', unknown>, + isHorizontal: boolean +) => { + const result: Partial> = {}; + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; + result[placement] = referenceLinePaddings.bottom; + } + if ( + referenceLinePaddings.left && + (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; + result[placement] = referenceLinePaddings.left; + } + if ( + referenceLinePaddings.right && + (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; + result[placement] = referenceLinePaddings.right; + } + // there's no top axis, so just check if a margin has been computed + if (referenceLinePaddings.top) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; + result[placement] = referenceLinePaddings.top; + } + return result; +}; + +export interface ReferenceLinesProps { + layers: CommonXYReferenceLineLayerConfig[]; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; + paddingMap: Partial>; +} + +export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => { + return ( + <> + {layers.flatMap((layer) => { + if (!layer.yConfig) { + return null; + } + + if (isReferenceLine(layer)) { + return ; + } + + 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 new file mode 100644 index 00000000000000..1a6eae6a490e68 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx @@ -0,0 +1,143 @@ +/* + * 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 from 'react'; +import { Position } from '@elastic/charts'; +import { euiLightVars } from '@kbn/ui-theme'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { IconPosition, YAxisMode } from '../../../common/types'; +import { + LINES_MARKER_SIZE, + mapVerticalToHorizontalPlacement, + Marker, + MarkerBody, +} from '../../helpers'; +import { ReferenceLineAnnotationConfig } from './reference_line_annotations'; + +// if there's just one axis, put it on the other one +// otherwise use the same axis +// this function assume the chart is vertical +export function getBaseIconPlacement( + iconPosition: IconPosition | undefined, + axesMap?: Record, + axisMode?: YAxisMode +) { + if (iconPosition === 'auto') { + if (axisMode === 'bottom') { + return Position.Top; + } + if (axesMap) { + if (axisMode === 'left') { + return axesMap.right ? Position.Left : Position.Right; + } + return axesMap.left ? Position.Right : Position.Left; + } + } + + if (iconPosition === 'left') { + return Position.Left; + } + if (iconPosition === 'right') { + return Position.Right; + } + if (iconPosition === 'below') { + return Position.Bottom; + } + return Position.Top; +} + +export const getSharedStyle = (config: ReferenceLineAnnotationConfig) => ({ + strokeWidth: config.lineWidth || 1, + stroke: config.color || euiLightVars.euiColorDarkShade, + dash: + config.lineStyle === 'dashed' + ? [(config.lineWidth || 1) * 3, config.lineWidth || 1] + : config.lineStyle === 'dotted' + ? [config.lineWidth || 1, config.lineWidth || 1] + : undefined, +}); + +export const getLineAnnotationProps = ( + config: ReferenceLineAnnotationConfig, + labels: { markerLabel?: string; markerBodyLabel?: string }, + axesMap: Record<'left' | 'right', boolean>, + paddingMap: Partial>, + groupId: 'left' | 'right' | undefined, + isHorizontal: boolean +) => { + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement( + config.iconPosition, + axesMap, + config.axisMode + ); + // the padding map is built for vertical chart + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + + return { + groupId, + marker: ( + + ), + markerBody: ( + + ), + // rotate the position if required + markerPosition: isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical, + }; +}; + +export const getGroupId = (axisMode: YAxisMode | undefined) => + axisMode === 'bottom' ? undefined : axisMode === 'right' ? 'right' : 'left'; + +export const getBottomRect = ( + headerLabel: string | undefined, + isFillAbove: boolean, + formatter: FieldFormat | undefined, + currentValue: number, + nextValue?: number +) => ({ + coordinates: { + x0: isFillAbove ? currentValue : nextValue, + y0: undefined, + x1: isFillAbove ? nextValue : currentValue, + y1: undefined, + }, + header: headerLabel, + details: formatter?.convert(currentValue) || currentValue.toString(), +}); + +export const getHorizontalRect = ( + headerLabel: string | undefined, + isFillAbove: boolean, + formatter: FieldFormat | undefined, + currentValue: number, + nextValue?: number +) => ({ + coordinates: { + x0: undefined, + y0: isFillAbove ? currentValue : nextValue, + x1: undefined, + y1: isFillAbove ? nextValue : currentValue, + }, + header: headerLabel, + details: formatter?.convert(currentValue) || currentValue.toString(), +}); 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 9bb3ea4f498e4f..80048bcb84038e 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 @@ -42,14 +42,24 @@ import { LegendSizeToPixels, } from '@kbn/visualizations-plugin/common/constants'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; -import type { CommonXYDataLayerConfig, SeriesType, XYChartProps } from '../../common/types'; +import type { + CommonXYDataLayerConfig, + ExtendedYConfig, + ReferenceLineYConfig, + SeriesType, + XYChartProps, +} from '../../common/types'; import { isHorizontalChart, getAnnotationsLayers, getDataLayers, Series, getFormat, + isReferenceLineYConfig, getFormattedTablesByLayers, +} from '../helpers'; + +import { getFilteredLayers, getReferenceLayers, isDataLayer, @@ -60,7 +70,7 @@ import { } from '../helpers'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './legend_action'; -import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; +import { ReferenceLines, computeChartMargins } from './reference_lines'; import { visualizationDefinitions } from '../definitions'; import { CommonXYLayerConfig } from '../../common/types'; import { SplitChart } from './split_chart'; @@ -270,6 +280,7 @@ export function XYChart({ }; const referenceLineLayers = getReferenceLayers(layers); + const annotationsLayers = getAnnotationsLayers(layers); const firstTable = dataLayers[0]?.table; @@ -286,7 +297,9 @@ export function XYChart({ const rangeAnnotations = getRangeAnnotations(annotationsLayers); const visualConfigs = [ - ...referenceLineLayers.flatMap(({ yConfig }) => yConfig), + ...referenceLineLayers.flatMap( + ({ yConfig }) => yConfig + ), ...groupedLineAnnotations, ].filter(Boolean); @@ -364,9 +377,10 @@ export function XYChart({ l.yConfig ? l.yConfig.map((yConfig) => ({ layerId: l.layerId, yConfig })) : [] ) .filter(({ yConfig }) => yConfig.axisMode === axis.groupId) - .map( - ({ layerId, yConfig }) => - `${layerId}-${yConfig.forAccessor}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` + .map(({ layerId, yConfig }) => + isReferenceLineYConfig(yConfig) + ? `${layerId}-${yConfig.value}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` + : `${layerId}-${yConfig.forAccessor}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` ), }; }; @@ -668,7 +682,7 @@ export function XYChart({ /> )} {referenceLineLayers.length ? ( - ( - (layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => { + return layers.filter( + (layer): layer is ReferenceLineLayerConfig | CommonXYDataLayerConfig => { let table: Datatable | undefined; let accessors: Array = []; let xAccessor: undefined | string | number; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts index e2f95491dbce8c..900cba47848538 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -7,7 +7,7 @@ */ import type { CommonXYLayerConfig, SeriesType, ExtendedYConfig, YConfig } from '../../common'; -import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization'; +import { getDataLayers, isAnnotationsLayer, isDataLayer, isReferenceLine } from './visualization'; export function isHorizontalSeries(seriesType: SeriesType) { return ( @@ -26,7 +26,11 @@ export function isHorizontalChart(layers: CommonXYLayerConfig[]) { } export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => { - if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) { + if ( + (isDataLayer(layer) && layer.splitAccessor) || + isAnnotationsLayer(layer) || + isReferenceLine(layer) + ) { return null; } const yConfig: Array | undefined = layer?.yConfig; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts index db0b431d56fac0..480fa5374238ea 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -6,12 +6,21 @@ * Side Public License, v 1. */ -import { LayerTypes } from '../../common/constants'; +import { + LayerTypes, + REFERENCE_LINE, + REFERENCE_LINE_LAYER, + REFERENCE_LINE_Y_CONFIG, +} from '../../common/constants'; import { CommonXYLayerConfig, CommonXYDataLayerConfig, CommonXYReferenceLineLayerConfig, CommonXYAnnotationLayerConfig, + ReferenceLineLayerConfig, + ReferenceLineConfig, + ExtendedYConfigResult, + ReferenceLineYConfig, } from '../../common/types'; export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig => @@ -20,13 +29,24 @@ export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLa export const getDataLayers = (layers: CommonXYLayerConfig[]) => (layers || []).filter((layer): layer is CommonXYDataLayerConfig => isDataLayer(layer)); -export const isReferenceLayer = ( +export const isReferenceLayer = (layer: CommonXYLayerConfig): layer is ReferenceLineLayerConfig => + layer.layerType === LayerTypes.REFERENCELINE && layer.type === REFERENCE_LINE_LAYER; + +export const isReferenceLine = (layer: CommonXYLayerConfig): layer is ReferenceLineConfig => + layer.type === REFERENCE_LINE; + +export const isReferenceLineYConfig = ( + yConfig: ReferenceLineYConfig | ExtendedYConfigResult +): yConfig is ReferenceLineYConfig => yConfig.type === REFERENCE_LINE_Y_CONFIG; + +export const isReferenceLineOrLayer = ( layer: CommonXYLayerConfig ): layer is CommonXYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE; export const getReferenceLayers = (layers: CommonXYLayerConfig[]) => - (layers || []).filter((layer): layer is CommonXYReferenceLineLayerConfig => - isReferenceLayer(layer) + (layers || []).filter( + (layer): layer is CommonXYReferenceLineLayerConfig => + isReferenceLayer(layer) || isReferenceLine(layer) ); const isAnnotationLayerCommon = ( diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 5c27da6b82b287..0dc6f62df3183d 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -24,8 +24,8 @@ import { gridlinesConfigFunction, axisExtentConfigFunction, tickLabelsConfigFunction, + referenceLineFunction, referenceLineLayerFunction, - extendedReferenceLineLayerFunction, annotationLayerFunction, labelsOrientationConfigFunction, axisTitlesVisibilityConfigFunction, @@ -64,8 +64,8 @@ export class ExpressionXyPlugin { expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index cefde5d38a5f48..4ddac2b3a3f798 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -19,10 +19,10 @@ import { tickLabelsConfigFunction, annotationLayerFunction, labelsOrientationConfigFunction, - referenceLineLayerFunction, + referenceLineFunction, axisTitlesVisibilityConfigFunction, extendedDataLayerFunction, - extendedReferenceLineLayerFunction, + referenceLineLayerFunction, layeredXyVisFunction, extendedAnnotationLayerFunction, } from '../common/expression_functions'; @@ -42,8 +42,8 @@ export class ExpressionXyPlugin expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index cb6e6cff2d70e6..ff5a692a76e960 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -356,7 +356,7 @@ const referenceLineLayerToExpression = ( chain: [ { type: 'function', - function: 'extendedReferenceLineLayer', + function: 'referenceLineLayer', arguments: { layerId: [layer.layerId], yConfig: layer.yConfig