[XY] Usable reference lines for xyVis. (#132192)

* ReferenceLineLayer -> referenceLine.

* Added the referenceLine and splitted the logic at ReferenceLineAnnotations.

* Fixed formatters of referenceLines

* Added referenceLines keys.

* Added test for the referenceLine fn.

* Added some tests for reference_lines.

* Unified the two different approaches of referenceLines.

* Fixed types at tests and limits.
This commit is contained in:
Yaroslav Kuznietsov 2022-05-20 11:18:17 +03:00 committed by GitHub
parent 6bdef36905
commit 3982bfd3fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1615 additions and 808 deletions

View file

@ -128,5 +128,5 @@ pageLoadAssetSize:
eventAnnotation: 19334
screenshotting: 22870
synthetics: 40958
expressionXY: 31000
expressionXY: 33000
kibanaUsageCollection: 16463

View file

@ -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';

View file

@ -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<CommonReferenceLineLayerFn['args'], 'accessors'> = {
yConfig: {
types: [EXTENDED_Y_CONFIG],
help: strings.getRLYConfigHelp(),
multi: true,
},
columnToLabel: {
types: ['string'],
help: strings.getColumnToLabelHelp(),
},
};

View file

@ -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,
};
},
};

View file

@ -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';

View file

@ -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,
},
},

View file

@ -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);
});
});

View file

@ -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 }],
};
},
};

View file

@ -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,
};
},
};

View file

@ -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,
},

View file

@ -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: {

View file

@ -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<Dimension[]>((dimensions, layer) => {
if (layer.layerType === LayerTypes.ANNOTATIONS) {
if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) {
return dimensions;
}

View file

@ -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' },

View file

@ -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',
}),
};

View file

@ -58,6 +58,5 @@ export type {
ReferenceLineLayerConfigResult,
CommonXYReferenceLineLayerConfig,
AxisTitlesVisibilityConfigResult,
ExtendedReferenceLineLayerConfigResult,
CommonXYReferenceLineLayerConfigResult,
} from './types';

View file

@ -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<ExpressionValueVisDimension | string>;
columnToLabel?: string;
yConfig?: ExtendedYConfigResult[];
export interface ReferenceLineArgs extends Omit<ExtendedYConfig, 'forAccessor'> {
name?: string;
value: number;
}
export interface ExtendedReferenceLineLayerArgs {
export interface ReferenceLineLayerArgs {
layerId?: string;
accessors: string[];
columnToLabel?: string;
@ -301,30 +301,35 @@ 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 interface ReferenceLineYConfig extends ReferenceLineArgs {
type: typeof REFERENCE_LINE_Y_CONFIG;
}
export interface ReferenceLineConfigResult {
type: typeof REFERENCE_LINE;
layerType: typeof LayerTypes.REFERENCELINE;
lineLength: number;
yConfig: [ReferenceLineYConfig];
}
export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & {
type: typeof REFERENCE_LINE_LAYER;
layerType: typeof LayerTypes.REFERENCELINE;
table: Datatable;
};
export type ExtendedReferenceLineLayerConfigResult = ExtendedReferenceLineLayerArgs & {
type: typeof EXTENDED_REFERENCE_LINE_LAYER;
layerType: typeof LayerTypes.REFERENCELINE;
table: Datatable;
};
export type DataLayerConfigResult = Omit<DataLayerArgs, 'palette'> & {
type: typeof DATA_LAYER;
layerType: typeof LayerTypes.DATA;
@ -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<ExtendedDataLayerArgs, 'palette'> & {
@ -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<ExtendedDataLayerConfigResult>
>;
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<typeof Y_CONFIG, null, YConfig, YConfigResult>;
export type ExtendedYConfigFn = ExpressionFunctionDefinition<

View file

@ -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;

View file

@ -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';

View file

@ -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<string, number> = {
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<ReferenceLineAnnotationsProps, 'data' | 'layers'>;
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
])}
/>
);
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode,
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
fill: 'above',
type: 'extendedYConfig',
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode,
lineStyle: 'solid',
fill: 'below',
type: 'extendedYConfig',
},
])}
/>
);
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(
<ReferenceLineAnnotations
{...defaultProps}
layers={createLayers([
{
forAccessor: `yAccessorLeftFirstId`,
axisMode: 'left',
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
{
forAccessor: `yAccessorRightSecondId`,
axisMode: 'right',
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
])}
/>
);
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,
},
])
);
}
);
});
});

View file

@ -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<Record<Position, number>>,
labelVisibility: Partial<Record<'x' | 'yLeft' | 'yRight', boolean>>,
titleVisibility: Partial<Record<'x' | 'yLeft' | 'yRight', boolean>>,
axesMap: Record<'left' | 'right', unknown>,
isHorizontal: boolean
) => {
const result: Partial<Record<Position, number>> = {};
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<string, unknown>,
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<Record<Position, number>>;
}
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<string, string> = 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: (
<Marker
config={yConfig}
label={columnToLabelMap[yConfig.forAccessor]}
isHorizontal={isHorizontal}
hasReducedPadding={hasReducedPadding}
/>
),
markerBody: (
<MarkerBody
label={
yConfig.textVisibility && !hasReducedPadding
? columnToLabelMap[yConfig.forAccessor]
: undefined
}
isHorizontal={
(!isHorizontal && yConfig.axisMode === 'bottom') ||
(isHorizontal && yConfig.axisMode !== 'bottom')
}
/>
),
// 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(
<LineAnnotation
{...props}
id={`${layer.layerId}-${yConfig.forAccessor}-line`}
key={`${layer.layerId}-${yConfig.forAccessor}-line`}
dataValues={table.rows.map(() => ({
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(
<RectAnnotation
{...props}
id={`${layer.layerId}-${yConfig.forAccessor}-rect`}
key={`${layer.layerId}-${yConfig.forAccessor}-rect`}
dataValues={table.rows.map(() => {
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;
});
})}
</>
);
};

View file

@ -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';

View file

@ -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<Record<Position, number>>;
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
axesMap: Record<'left' | 'right', boolean>;
isHorizontal: boolean;
}
export const ReferenceLine: FC<ReferenceLineProps> = ({
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 (
<ReferenceLineAnnotations
config={{ id, ...yConfig }}
paddingMap={paddingMap}
axesMap={axesMap}
formatter={formatter}
isHorizontal={isHorizontal}
/>
);
};

View file

@ -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<Record<Position, number>>;
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<Props> = ({
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 = (
<LineAnnotation
{...props}
id={`${id}-line`}
key={`${id}-line`}
dataValues={[dataValues]}
domainType={
axisMode === 'bottom' ? AnnotationDomainType.XDomain : AnnotationDomainType.YDomain
}
style={{ line: { ...sharedStyle, opacity: 1 } }}
/>
);
let rect;
if (fill && fill !== 'none') {
const rectDataValues = getRectDataValue(config, formatter);
rect = (
<RectAnnotation
{...props}
id={`${id}-rect`}
key={`${id}-rect`}
dataValues={[rectDataValues]}
style={{ ...sharedStyle, fill: color || defaultColor, opacity: 0.1 }}
/>
);
}
return (
<>
{line}
{rect}
</>
);
};

View file

@ -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<Record<Position, number>>;
axesMap: Record<'left' | 'right', boolean>;
isHorizontal: boolean;
}
export const ReferenceLineLayer: FC<ReferenceLineLayerProps> = ({
layer,
formatters,
paddingMap,
axesMap,
isHorizontal,
}) => {
if (!layer.yConfig) {
return null;
}
const { columnToLabel, yConfig: yConfigs, table } = layer;
const columnToLabelMap: Record<string, string> = 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 (
<ReferenceLineAnnotations
key={id}
config={{
id,
value,
nextValue,
name,
...restAnnotationConfig,
}}
paddingMap={paddingMap}
axesMap={axesMap}
formatter={formatter}
isHorizontal={isHorizontal}
/>
);
});
return <>{referenceLineElements}</>;
};

View file

@ -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<string, number> = {
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<ReferenceLinesProps, 'data' | 'layers'>;
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
])}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode,
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode: 'bottom',
lineStyle: 'solid',
type: 'extendedYConfig',
fill,
},
])}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `${layerPrefix}FirstId`,
axisMode,
lineStyle: 'solid',
fill: 'above',
type: 'extendedYConfig',
},
{
forAccessor: `${layerPrefix}SecondId`,
axisMode,
lineStyle: 'solid',
fill: 'below',
type: 'extendedYConfig',
},
])}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={createLayers([
{
forAccessor: `yAccessorLeftFirstId`,
axisMode: 'left',
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
{
forAccessor: `yAccessorRightSecondId`,
axisMode: 'right',
lineStyle: 'solid',
fill,
type: 'extendedYConfig',
},
])}
/>
);
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<ReferenceLinesProps, 'data' | 'layers'>;
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(
<ReferenceLines
{...defaultProps}
layers={[
createReferenceLine(layerPrefix, 1, {
axisMode,
lineStyle: 'solid',
fill,
value,
}),
]}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={[
createReferenceLine(layerPrefix, 1, {
axisMode: 'bottom',
lineStyle: 'solid',
fill,
value,
}),
]}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={[
createReferenceLine(layerPrefix, 10, {
axisMode,
lineStyle: 'solid',
fill,
value,
}),
createReferenceLine(layerPrefix, 10, {
axisMode,
lineStyle: 'solid',
fill,
value,
}),
]}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={[
createReferenceLine(layerPrefix, 10, {
axisMode: 'bottom',
lineStyle: 'solid',
fill,
value,
}),
createReferenceLine(layerPrefix, 10, {
axisMode: 'bottom',
lineStyle: 'solid',
fill,
value,
}),
]}
/>
);
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(
<ReferenceLines
{...defaultProps}
layers={[
createReferenceLine(layerPrefix, 10, {
axisMode,
lineStyle: 'solid',
fill: 'above',
value: value1,
}),
createReferenceLine(layerPrefix, 10, {
axisMode,
lineStyle: 'solid',
fill: 'below',
value: value2,
}),
]}
/>
);
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,
},
])
);
}
);
});
});

View file

@ -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<Record<Position, number>>,
labelVisibility: Partial<Record<'x' | 'yLeft' | 'yRight', boolean>>,
titleVisibility: Partial<Record<'x' | 'yLeft' | 'yRight', boolean>>,
axesMap: Record<'left' | 'right', unknown>,
isHorizontal: boolean
) => {
const result: Partial<Record<Position, number>> = {};
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<Record<Position, number>>;
}
export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => {
return (
<>
{layers.flatMap((layer) => {
if (!layer.yConfig) {
return null;
}
if (isReferenceLine(layer)) {
return <ReferenceLine key={`referenceLine-${layer.layerId}`} layer={layer} {...rest} />;
}
return (
<ReferenceLineLayer key={`referenceLine-${layer.layerId}`} layer={layer} {...rest} />
);
})}
</>
);
};

View file

@ -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<string, unknown>,
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<Record<Position, number>>,
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: (
<Marker
config={config}
label={labels.markerLabel}
isHorizontal={isHorizontal}
hasReducedPadding={hasReducedPadding}
/>
),
markerBody: (
<MarkerBody
label={labels.markerBodyLabel}
isHorizontal={
(!isHorizontal && config.axisMode === 'bottom') ||
(isHorizontal && config.axisMode !== 'bottom')
}
/>
),
// 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(),
});

View file

@ -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<ExtendedYConfig | ReferenceLineYConfig | undefined>(
({ 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 ? (
<ReferenceLineAnnotations
<ReferenceLines
layers={referenceLineLayers}
formatters={{
left: yAxesMap.left?.formatter,

View file

@ -12,13 +12,13 @@ import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/com
import {
CommonXYDataLayerConfig,
CommonXYLayerConfig,
CommonXYReferenceLineLayerConfig,
ReferenceLineLayerConfig,
} from '../../common/types';
import { isDataLayer, isReferenceLayer } from './visualization';
export function getFilteredLayers(layers: CommonXYLayerConfig[]) {
return layers.filter<CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig>(
(layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => {
return layers.filter<ReferenceLineLayerConfig | CommonXYDataLayerConfig>(
(layer): layer is ReferenceLineLayerConfig | CommonXYDataLayerConfig => {
let table: Datatable | undefined;
let accessors: Array<ExpressionValueVisDimension | string> = [];
let xAccessor: undefined | string | number;

View file

@ -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<YConfig | ExtendedYConfig> | undefined = layer?.yConfig;

View file

@ -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 = (

View file

@ -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);

View file

@ -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);

View file

@ -356,7 +356,7 @@ const referenceLineLayerToExpression = (
chain: [
{
type: 'function',
function: 'extendedReferenceLineLayer',
function: 'referenceLineLayer',
arguments: {
layerId: [layer.layerId],
yConfig: layer.yConfig