mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[XY] xyVis
and layeredXyVis
. (#128255)
* Added extended layers expressions. * Added support of tables at layers. * Added annotations to layeredXyVIs. * Refactored the implementation to be reusable. * Fixed undefined layers. * Fixed empty arrays problems. * Fixed input translations and removed not used arguments. * Fixed missing required args error, and added required to arguments. * Simplified expression configuration. * Added strict to all the expressions. * Moved dataLayer to the separate component. * Refactored dataLayers helpers and xy_chart. * fillOpacity usage validation is added. * Fixed valueLabels argument options. Removed not used. Added validation for usage. * Added validation to the layeredXyVis. * Fixed extent validation. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra <marta.bondyra@gmail.com> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co>
This commit is contained in:
parent
c1d44151e2
commit
b29b468961
118 changed files with 7394 additions and 3800 deletions
|
@ -124,7 +124,7 @@ pageLoadAssetSize:
|
|||
visTypeGauge: 24113
|
||||
unifiedSearch: 71059
|
||||
data: 454087
|
||||
expressionXY: 26500
|
||||
eventAnnotation: 19334
|
||||
screenshotting: 22870
|
||||
synthetics: 40958
|
||||
expressionXY: 29000
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Position } from '@elastic/charts';
|
|||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import { Datatable, DatatableRow } from '@kbn/expressions-plugin';
|
||||
import { LayerTypes } from '../constants';
|
||||
import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../types';
|
||||
import { DataLayerConfig, XYProps } from '../types';
|
||||
|
||||
export const mockPaletteOutput: PaletteOutput = {
|
||||
type: 'palette',
|
||||
|
@ -46,9 +46,9 @@ export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable =
|
|||
rows,
|
||||
});
|
||||
|
||||
export const sampleLayer: DataLayerConfigResult = {
|
||||
type: 'dataLayer',
|
||||
export const sampleLayer: DataLayerConfig = {
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'c',
|
||||
|
@ -59,9 +59,12 @@ export const sampleLayer: DataLayerConfigResult = {
|
|||
yScaleType: 'linear',
|
||||
isHistogram: false,
|
||||
palette: mockPaletteOutput,
|
||||
table: createSampleDatatableWithRows([]),
|
||||
};
|
||||
|
||||
export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLayer]): XYArgs => ({
|
||||
export const createArgsWithLayers = (
|
||||
layers: DataLayerConfig | DataLayerConfig[] = sampleLayer
|
||||
): XYProps => ({
|
||||
xTitle: '',
|
||||
yTitle: '',
|
||||
yRightTitle: '',
|
||||
|
@ -104,25 +107,17 @@ export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLa
|
|||
mode: 'full',
|
||||
type: 'axisExtentConfig',
|
||||
},
|
||||
layers,
|
||||
layers: Array.isArray(layers) ? layers : [layers],
|
||||
});
|
||||
|
||||
export function sampleArgs() {
|
||||
const data: LensMultiTable = {
|
||||
type: 'lens_multitable',
|
||||
tables: {
|
||||
first: createSampleDatatableWithRows([
|
||||
const data = createSampleDatatableWithRows([
|
||||
{ a: 1, b: 2, c: 'I', d: 'Foo' },
|
||||
{ a: 1, b: 5, c: 'J', d: 'Bar' },
|
||||
]),
|
||||
},
|
||||
dateRange: {
|
||||
fromDate: new Date('2019-01-02T05:00:00.000Z'),
|
||||
toDate: new Date('2019-01-03T05:00:00.000Z'),
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
args: createArgsWithLayers({ ...sampleLayer, table: data }),
|
||||
};
|
||||
|
||||
const args: XYArgs = createArgsWithLayers();
|
||||
|
||||
return { data, args };
|
||||
}
|
||||
|
|
|
@ -7,16 +7,21 @@
|
|||
*/
|
||||
|
||||
export const XY_VIS = 'xyVis';
|
||||
export const LAYERED_XY_VIS = 'layeredXyVis';
|
||||
export const Y_CONFIG = 'yConfig';
|
||||
export const EXTENDED_Y_CONFIG = 'extendedYConfig';
|
||||
export const MULTITABLE = 'lens_multitable';
|
||||
export const DATA_LAYER = 'dataLayer';
|
||||
export const EXTENDED_DATA_LAYER = 'extendedDataLayer';
|
||||
export const LEGEND_CONFIG = 'legendConfig';
|
||||
export const XY_VIS_RENDERER = 'xyVis';
|
||||
export const GRID_LINES_CONFIG = 'gridlinesConfig';
|
||||
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_LAYER = 'referenceLineLayer';
|
||||
export const EXTENDED_REFERENCE_LINE_LAYER = 'extendedReferenceLineLayer';
|
||||
export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig';
|
||||
export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig';
|
||||
|
||||
|
@ -106,6 +111,23 @@ export const XYCurveTypes = {
|
|||
|
||||
export const ValueLabelModes = {
|
||||
HIDE: 'hide',
|
||||
INSIDE: 'inside',
|
||||
OUTSIDE: 'outside',
|
||||
SHOW: 'show',
|
||||
} as const;
|
||||
|
||||
export const AvailableReferenceLineIcons = {
|
||||
EMPTY: 'empty',
|
||||
ASTERISK: 'asterisk',
|
||||
ALERT: 'alert',
|
||||
BELL: 'bell',
|
||||
BOLT: 'bolt',
|
||||
BUG: 'bug',
|
||||
CIRCLE: 'circle',
|
||||
EDITOR_COMMENT: 'editorComment',
|
||||
FLAG: 'flag',
|
||||
HEART: 'heart',
|
||||
MAP_MARKER: 'mapMarker',
|
||||
PIN_FILLED: 'pinFilled',
|
||||
STAR_EMPTY: 'starEmpty',
|
||||
TAG: 'tag',
|
||||
TRIANGLE: 'triangle',
|
||||
} as const;
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { LayerTypes, ANNOTATION_LAYER } from '../constants';
|
||||
import { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../types';
|
||||
import { strings } from '../i18n';
|
||||
|
||||
export function annotationLayerConfigFunction(): ExpressionFunctionDefinition<
|
||||
export function annotationLayerFunction(): ExpressionFunctionDefinition<
|
||||
typeof ANNOTATION_LAYER,
|
||||
null,
|
||||
Datatable,
|
||||
AnnotationLayerArgs,
|
||||
AnnotationLayerConfigResult
|
||||
> {
|
||||
|
@ -20,21 +21,17 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition<
|
|||
name: ANNOTATION_LAYER,
|
||||
aliases: [],
|
||||
type: ANNOTATION_LAYER,
|
||||
inputTypes: ['null'],
|
||||
help: 'Annotation layer in lens',
|
||||
inputTypes: ['datatable'],
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
args: {
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
},
|
||||
hide: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: 'Show details',
|
||||
help: strings.getAnnotationLayerHideHelp(),
|
||||
},
|
||||
annotations: {
|
||||
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
|
||||
help: '',
|
||||
help: strings.getAnnotationLayerAnnotationsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
},
|
||||
|
@ -42,6 +39,7 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition<
|
|||
return {
|
||||
type: ANNOTATION_LAYER,
|
||||
...args,
|
||||
annotations: args.annotations ?? [],
|
||||
layerType: LayerTypes.ANNOTATIONS,
|
||||
};
|
||||
},
|
|
@ -11,6 +11,13 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/commo
|
|||
import { AxisExtentConfig, AxisExtentConfigResult } from '../types';
|
||||
import { AxisExtentModes, AXIS_EXTENT_CONFIG } from '../constants';
|
||||
|
||||
const errors = {
|
||||
upperBoundLowerOrEqualToLowerBoundError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.axisExtentConfig.errors.emptyUpperBound', {
|
||||
defaultMessage: 'Upper bound should be greater than lower bound, if custom mode is enabled.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const axisExtentConfigFunction: ExpressionFunctionDefinition<
|
||||
typeof AXIS_EXTENT_CONFIG,
|
||||
null,
|
||||
|
@ -27,10 +34,12 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition<
|
|||
args: {
|
||||
mode: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(AxisExtentModes)],
|
||||
help: i18n.translate('expressionXY.axisExtentConfig.extentMode.help', {
|
||||
defaultMessage: 'The extent mode',
|
||||
}),
|
||||
options: [...Object.values(AxisExtentModes)],
|
||||
strict: true,
|
||||
default: AxisExtentModes.FULL,
|
||||
},
|
||||
lowerBound: {
|
||||
types: ['number'],
|
||||
|
@ -46,6 +55,16 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition<
|
|||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
if (args.mode === AxisExtentModes.CUSTOM) {
|
||||
if (
|
||||
args.lowerBound !== undefined &&
|
||||
args.upperBound !== undefined &&
|
||||
args.lowerBound >= args.upperBound
|
||||
) {
|
||||
throw new Error(errors.upperBoundLowerOrEqualToLowerBoundError());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: AXIS_EXTENT_CONFIG,
|
||||
...args,
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { SeriesTypes, XScaleTypes, YScaleTypes, Y_CONFIG } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { DataLayerFn, ExtendedDataLayerFn } from '../types';
|
||||
|
||||
type CommonDataLayerFn = DataLayerFn | ExtendedDataLayerFn;
|
||||
|
||||
export const commonDataLayerArgs: CommonDataLayerFn['args'] = {
|
||||
hide: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getHideHelp(),
|
||||
},
|
||||
xAccessor: {
|
||||
types: ['string'],
|
||||
help: strings.getXAccessorHelp(),
|
||||
},
|
||||
seriesType: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(SeriesTypes)],
|
||||
help: strings.getSeriesTypeHelp(),
|
||||
required: true,
|
||||
strict: true,
|
||||
},
|
||||
xScaleType: {
|
||||
options: [...Object.values(XScaleTypes)],
|
||||
help: strings.getXScaleTypeHelp(),
|
||||
default: XScaleTypes.ORDINAL,
|
||||
strict: true,
|
||||
},
|
||||
isHistogram: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getIsHistogramHelp(),
|
||||
},
|
||||
yScaleType: {
|
||||
options: [...Object.values(YScaleTypes)],
|
||||
help: strings.getYScaleTypeHelp(),
|
||||
default: YScaleTypes.LINEAR,
|
||||
strict: true,
|
||||
},
|
||||
splitAccessor: {
|
||||
types: ['string'],
|
||||
help: strings.getSplitAccessorHelp(),
|
||||
},
|
||||
accessors: {
|
||||
types: ['string'],
|
||||
help: strings.getAccessorsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
yConfig: {
|
||||
types: [Y_CONFIG],
|
||||
help: strings.getYConfigHelp(),
|
||||
multi: true,
|
||||
},
|
||||
columnToLabel: {
|
||||
types: ['string'],
|
||||
help: strings.getColumnToLabelHelp(),
|
||||
},
|
||||
palette: {
|
||||
types: ['palette', 'system_palette'],
|
||||
help: strings.getPaletteHelp(),
|
||||
default: '{palette}',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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: CommonReferenceLineLayerFn['args'] = {
|
||||
accessors: {
|
||||
types: ['string'],
|
||||
help: strings.getRLAccessorsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
yConfig: {
|
||||
types: [EXTENDED_Y_CONFIG],
|
||||
help: strings.getRLYConfigHelp(),
|
||||
multi: true,
|
||||
},
|
||||
columnToLabel: {
|
||||
types: ['string'],
|
||||
help: strings.getColumnToLabelHelp(),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 {
|
||||
AXIS_EXTENT_CONFIG,
|
||||
AXIS_TITLES_VISIBILITY_CONFIG,
|
||||
EndValues,
|
||||
FittingFunctions,
|
||||
GRID_LINES_CONFIG,
|
||||
LABELS_ORIENTATION_CONFIG,
|
||||
LEGEND_CONFIG,
|
||||
TICK_LABELS_CONFIG,
|
||||
ValueLabelModes,
|
||||
XYCurveTypes,
|
||||
} from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { LayeredXyVisFn, XyVisFn } from '../types';
|
||||
|
||||
type CommonXYFn = XyVisFn | LayeredXyVisFn;
|
||||
|
||||
export const commonXYArgs: CommonXYFn['args'] = {
|
||||
xTitle: {
|
||||
types: ['string'],
|
||||
help: strings.getXTitleHelp(),
|
||||
},
|
||||
yTitle: {
|
||||
types: ['string'],
|
||||
help: strings.getYTitleHelp(),
|
||||
},
|
||||
yRightTitle: {
|
||||
types: ['string'],
|
||||
help: strings.getYRightTitleHelp(),
|
||||
},
|
||||
yLeftExtent: {
|
||||
types: [AXIS_EXTENT_CONFIG],
|
||||
help: strings.getYLeftExtentHelp(),
|
||||
default: `{${AXIS_EXTENT_CONFIG}}`,
|
||||
},
|
||||
yRightExtent: {
|
||||
types: [AXIS_EXTENT_CONFIG],
|
||||
help: strings.getYRightExtentHelp(),
|
||||
default: `{${AXIS_EXTENT_CONFIG}}`,
|
||||
},
|
||||
legend: {
|
||||
types: [LEGEND_CONFIG],
|
||||
help: strings.getLegendHelp(),
|
||||
default: `{${LEGEND_CONFIG}}`,
|
||||
},
|
||||
fittingFunction: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(FittingFunctions)],
|
||||
help: strings.getFittingFunctionHelp(),
|
||||
strict: true,
|
||||
},
|
||||
endValue: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(EndValues)],
|
||||
help: strings.getEndValueHelp(),
|
||||
strict: true,
|
||||
},
|
||||
emphasizeFitting: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: '',
|
||||
},
|
||||
valueLabels: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(ValueLabelModes)],
|
||||
help: strings.getValueLabelsHelp(),
|
||||
strict: true,
|
||||
default: ValueLabelModes.HIDE,
|
||||
},
|
||||
tickLabelsVisibilitySettings: {
|
||||
types: [TICK_LABELS_CONFIG],
|
||||
help: strings.getTickLabelsVisibilitySettingsHelp(),
|
||||
},
|
||||
labelsOrientation: {
|
||||
types: [LABELS_ORIENTATION_CONFIG],
|
||||
help: strings.getLabelsOrientationHelp(),
|
||||
},
|
||||
gridlinesVisibilitySettings: {
|
||||
types: [GRID_LINES_CONFIG],
|
||||
help: strings.getGridlinesVisibilitySettingsHelp(),
|
||||
},
|
||||
axisTitlesVisibilitySettings: {
|
||||
types: [AXIS_TITLES_VISIBILITY_CONFIG],
|
||||
help: strings.getAxisTitlesVisibilitySettingsHelp(),
|
||||
},
|
||||
curveType: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(XYCurveTypes)],
|
||||
help: strings.getCurveTypeHelp(),
|
||||
strict: true,
|
||||
},
|
||||
fillOpacity: {
|
||||
types: ['number'],
|
||||
help: strings.getFillOpacityHelp(),
|
||||
},
|
||||
hideEndzones: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getHideEndzonesHelp(),
|
||||
},
|
||||
valuesInLegend: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getValuesInLegendHelp(),
|
||||
},
|
||||
ariaLabel: {
|
||||
types: ['string'],
|
||||
help: strings.getAriaLabelHelp(),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { YAxisModes } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { YConfigFn, ExtendedYConfigFn } from '../types';
|
||||
|
||||
type CommonYConfigFn = YConfigFn | ExtendedYConfigFn;
|
||||
|
||||
export const commonYConfigArgs: CommonYConfigFn['args'] = {
|
||||
forAccessor: {
|
||||
types: ['string'],
|
||||
help: strings.getForAccessorHelp(),
|
||||
},
|
||||
axisMode: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(YAxisModes)],
|
||||
help: strings.getAxisModeHelp(),
|
||||
strict: true,
|
||||
},
|
||||
color: {
|
||||
types: ['string'],
|
||||
help: strings.getColorHelp(),
|
||||
},
|
||||
};
|
|
@ -7,15 +7,15 @@
|
|||
*/
|
||||
|
||||
import { DataLayerArgs } from '../types';
|
||||
import { dataLayerConfigFunction } from '.';
|
||||
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
|
||||
import { mockPaletteOutput } from '../__mocks__';
|
||||
import { mockPaletteOutput, sampleArgs } from '../__mocks__';
|
||||
import { LayerTypes } from '../constants';
|
||||
import { dataLayerFunction } from './data_layer';
|
||||
|
||||
describe('dataLayerConfig', () => {
|
||||
test('produces the correct arguments', () => {
|
||||
const { data } = sampleArgs();
|
||||
const args: DataLayerArgs = {
|
||||
layerId: 'first',
|
||||
seriesType: 'line',
|
||||
xAccessor: 'c',
|
||||
accessors: ['a', 'b'],
|
||||
|
@ -26,8 +26,13 @@ describe('dataLayerConfig', () => {
|
|||
palette: mockPaletteOutput,
|
||||
};
|
||||
|
||||
const result = dataLayerConfigFunction.fn(null, args, createMockExecutionContext());
|
||||
const result = dataLayerFunction.fn(data, args, createMockExecutionContext());
|
||||
|
||||
expect(result).toEqual({ type: 'dataLayer', layerType: LayerTypes.DATA, ...args });
|
||||
expect(result).toEqual({
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
...args,
|
||||
table: data,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { DataLayerFn } from '../types';
|
||||
import { DATA_LAYER, LayerTypes } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { commonDataLayerArgs } from './common_data_layer_args';
|
||||
|
||||
export const dataLayerFunction: DataLayerFn = {
|
||||
name: DATA_LAYER,
|
||||
aliases: [],
|
||||
type: DATA_LAYER,
|
||||
help: strings.getDataLayerFnHelp(),
|
||||
inputTypes: ['datatable'],
|
||||
args: { ...commonDataLayerArgs },
|
||||
fn(table, args) {
|
||||
return {
|
||||
type: DATA_LAYER,
|
||||
...args,
|
||||
accessors: args.accessors ?? [],
|
||||
layerType: LayerTypes.DATA,
|
||||
table,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,123 +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 { i18n } from '@kbn/i18n';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { DataLayerArgs, DataLayerConfigResult } from '../types';
|
||||
import {
|
||||
DATA_LAYER,
|
||||
LayerTypes,
|
||||
SeriesTypes,
|
||||
XScaleTypes,
|
||||
YScaleTypes,
|
||||
Y_CONFIG,
|
||||
} from '../constants';
|
||||
|
||||
export const dataLayerConfigFunction: ExpressionFunctionDefinition<
|
||||
typeof DATA_LAYER,
|
||||
null,
|
||||
DataLayerArgs,
|
||||
DataLayerConfigResult
|
||||
> = {
|
||||
name: DATA_LAYER,
|
||||
aliases: [],
|
||||
type: DATA_LAYER,
|
||||
help: i18n.translate('expressionXY.dataLayer.help', {
|
||||
defaultMessage: `Configure a layer in the xy chart`,
|
||||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
hide: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('expressionXY.dataLayer.hide.help', {
|
||||
defaultMessage: 'Show / hide axis',
|
||||
}),
|
||||
},
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.dataLayer.layerId.help', {
|
||||
defaultMessage: 'Layer ID',
|
||||
}),
|
||||
},
|
||||
xAccessor: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.dataLayer.xAccessor.help', {
|
||||
defaultMessage: 'X-axis',
|
||||
}),
|
||||
},
|
||||
seriesType: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(SeriesTypes)],
|
||||
help: i18n.translate('expressionXY.dataLayer.seriesType.help', {
|
||||
defaultMessage: 'The type of chart to display.',
|
||||
}),
|
||||
},
|
||||
xScaleType: {
|
||||
options: [...Object.values(XScaleTypes)],
|
||||
help: i18n.translate('expressionXY.dataLayer.xScaleType.help', {
|
||||
defaultMessage: 'The scale type of the x axis',
|
||||
}),
|
||||
default: XScaleTypes.ORDINAL,
|
||||
},
|
||||
isHistogram: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('expressionXY.dataLayer.isHistogram.help', {
|
||||
defaultMessage: 'Whether to layout the chart as a histogram',
|
||||
}),
|
||||
},
|
||||
yScaleType: {
|
||||
options: [...Object.values(YScaleTypes)],
|
||||
help: i18n.translate('expressionXY.dataLayer.yScaleType.help', {
|
||||
defaultMessage: 'The scale type of the y axes',
|
||||
}),
|
||||
default: YScaleTypes.LINEAR,
|
||||
},
|
||||
splitAccessor: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.dataLayer.splitAccessor.help', {
|
||||
defaultMessage: 'The column to split by',
|
||||
}),
|
||||
},
|
||||
accessors: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.dataLayer.accessors.help', {
|
||||
defaultMessage: 'The columns to display on the y axis.',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
yConfig: {
|
||||
types: [Y_CONFIG],
|
||||
help: i18n.translate('expressionXY.dataLayer.yConfig.help', {
|
||||
defaultMessage: 'Additional configuration for y axes',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
columnToLabel: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.dataLayer.columnToLabel.help', {
|
||||
defaultMessage: 'JSON key-value pairs of column ID to label',
|
||||
}),
|
||||
},
|
||||
palette: {
|
||||
types: ['palette', 'system_palette'],
|
||||
help: i18n.translate('expressionXY.dataLayer.palette.help', {
|
||||
defaultMessage: 'Palette',
|
||||
}),
|
||||
default: '{palette}',
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: DATA_LAYER,
|
||||
...args,
|
||||
layerType: LayerTypes.DATA,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { LayerTypes, EXTENDED_ANNOTATION_LAYER } from '../constants';
|
||||
import { ExtendedAnnotationLayerConfigResult, ExtendedAnnotationLayerArgs } from '../types';
|
||||
import { strings } from '../i18n';
|
||||
|
||||
export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition<
|
||||
typeof EXTENDED_ANNOTATION_LAYER,
|
||||
Datatable,
|
||||
ExtendedAnnotationLayerArgs,
|
||||
ExtendedAnnotationLayerConfigResult
|
||||
> {
|
||||
return {
|
||||
name: EXTENDED_ANNOTATION_LAYER,
|
||||
aliases: [],
|
||||
type: EXTENDED_ANNOTATION_LAYER,
|
||||
inputTypes: ['datatable'],
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
args: {
|
||||
hide: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getAnnotationLayerHideHelp(),
|
||||
},
|
||||
annotations: {
|
||||
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
|
||||
help: strings.getAnnotationLayerAnnotationsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: strings.getLayerIdHelp(),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
return {
|
||||
type: EXTENDED_ANNOTATION_LAYER,
|
||||
...args,
|
||||
annotations: args.annotations ?? [],
|
||||
layerType: LayerTypes.ANNOTATIONS,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { ExtendedDataLayerFn } from '../types';
|
||||
import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { commonDataLayerArgs } from './common_data_layer_args';
|
||||
|
||||
export const extendedDataLayerFunction: ExtendedDataLayerFn = {
|
||||
name: EXTENDED_DATA_LAYER,
|
||||
aliases: [],
|
||||
type: EXTENDED_DATA_LAYER,
|
||||
help: strings.getDataLayerFnHelp(),
|
||||
inputTypes: ['datatable'],
|
||||
args: {
|
||||
...commonDataLayerArgs,
|
||||
table: {
|
||||
types: ['datatable'],
|
||||
help: strings.getTableHelp(),
|
||||
},
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: strings.getLayerIdHelp(),
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: EXTENDED_DATA_LAYER,
|
||||
...args,
|
||||
accessors: args.accessors ?? [],
|
||||
layerType: LayerTypes.DATA,
|
||||
table: args.table ?? input,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { 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,
|
||||
table: {
|
||||
types: ['datatable'],
|
||||
help: strings.getTableHelp(),
|
||||
},
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: strings.getLayerIdHelp(),
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: EXTENDED_REFERENCE_LINE_LAYER,
|
||||
...args,
|
||||
accessors: args.accessors ?? [],
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
table: args.table ?? input,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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,
|
||||
EXTENDED_Y_CONFIG,
|
||||
FillStyles,
|
||||
IconPositions,
|
||||
LineStyles,
|
||||
} from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { ExtendedYConfigFn } from '../types';
|
||||
import { commonYConfigArgs } from './common_y_config_args';
|
||||
|
||||
export const extendedYAxisConfigFunction: ExtendedYConfigFn = {
|
||||
name: EXTENDED_Y_CONFIG,
|
||||
aliases: [],
|
||||
type: EXTENDED_Y_CONFIG,
|
||||
help: strings.getYConfigFnHelp(),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
...commonYConfigArgs,
|
||||
lineStyle: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(LineStyles)],
|
||||
help: i18n.translate('expressionXY.yConfig.lineStyle.help', {
|
||||
defaultMessage: 'The style of the reference line',
|
||||
}),
|
||||
strict: true,
|
||||
},
|
||||
lineWidth: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('expressionXY.yConfig.lineWidth.help', {
|
||||
defaultMessage: 'The width of the reference line',
|
||||
}),
|
||||
},
|
||||
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',
|
||||
}),
|
||||
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',
|
||||
}),
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: EXTENDED_Y_CONFIG,
|
||||
...args,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -7,13 +7,18 @@
|
|||
*/
|
||||
|
||||
export * from './xy_vis';
|
||||
export * from './layered_xy_vis';
|
||||
export * from './legend_config';
|
||||
export * from './annotation_layer_config';
|
||||
export * from './annotation_layer';
|
||||
export * from './extended_annotation_layer';
|
||||
export * from './y_axis_config';
|
||||
export * from './data_layer_config';
|
||||
export * from './extended_y_axis_config';
|
||||
export * from './data_layer';
|
||||
export * from './extended_data_layer';
|
||||
export * from './grid_lines_config';
|
||||
export * from './axis_extent_config';
|
||||
export * from './tick_labels_config';
|
||||
export * from './labels_orientation_config';
|
||||
export * from './reference_line_layer_config';
|
||||
export * from './reference_line_layer';
|
||||
export * from './extended_reference_line_layer';
|
||||
export * from './axis_titles_visibility_config';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { LayeredXyVisFn } from '../types';
|
||||
import {
|
||||
EXTENDED_DATA_LAYER,
|
||||
EXTENDED_REFERENCE_LINE_LAYER,
|
||||
LAYERED_XY_VIS,
|
||||
EXTENDED_ANNOTATION_LAYER,
|
||||
} from '../constants';
|
||||
import { commonXYArgs } from './common_xy_args';
|
||||
import { strings } from '../i18n';
|
||||
|
||||
export const layeredXyVisFunction: LayeredXyVisFn = {
|
||||
name: LAYERED_XY_VIS,
|
||||
type: 'render',
|
||||
inputTypes: ['datatable'],
|
||||
help: strings.getXYHelp(),
|
||||
args: {
|
||||
...commonXYArgs,
|
||||
layers: {
|
||||
types: [EXTENDED_DATA_LAYER, EXTENDED_REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER],
|
||||
help: i18n.translate('expressionXY.layeredXyVis.layers.help', {
|
||||
defaultMessage: 'Layers of visual series',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
},
|
||||
async fn(data, args, handlers) {
|
||||
const { layeredXyVisFn } = await import('./layered_xy_vis_fn');
|
||||
return await layeredXyVisFn(data, args, handlers);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { XY_VIS_RENDERER } from '../constants';
|
||||
import { appendLayerIds } from '../helpers';
|
||||
import { LayeredXyVisFn } from '../types';
|
||||
import { logDatatables } from '../utils';
|
||||
|
||||
export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => {
|
||||
const layers = appendLayerIds(args.layers ?? [], 'layers');
|
||||
|
||||
logDatatables(layers, handlers);
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
as: XY_VIS_RENDERER,
|
||||
value: {
|
||||
args: {
|
||||
...args,
|
||||
layers,
|
||||
ariaLabel:
|
||||
args.ariaLabel ??
|
||||
(handlers.variables?.embeddableTitle as string) ??
|
||||
handlers.getExecutionContext?.()?.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -12,9 +12,9 @@ import { LegendConfig } from '../types';
|
|||
import { legendConfigFunction } from './legend_config';
|
||||
|
||||
describe('legendConfigFunction', () => {
|
||||
test('produces the correct arguments', () => {
|
||||
test('produces the correct arguments', async () => {
|
||||
const args: LegendConfig = { isVisible: true, position: Position.Left };
|
||||
const result = legendConfigFunction.fn(null, args, createMockExecutionContext());
|
||||
const result = await legendConfigFunction.fn(null, args, createMockExecutionContext());
|
||||
|
||||
expect(result).toEqual({ type: 'legendConfig', ...args });
|
||||
});
|
||||
|
|
|
@ -8,16 +8,10 @@
|
|||
|
||||
import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { LEGEND_CONFIG } from '../constants';
|
||||
import { LegendConfig, LegendConfigResult } from '../types';
|
||||
import { LegendConfigFn } from '../types';
|
||||
|
||||
export const legendConfigFunction: ExpressionFunctionDefinition<
|
||||
typeof LEGEND_CONFIG,
|
||||
null,
|
||||
LegendConfig,
|
||||
LegendConfigResult
|
||||
> = {
|
||||
export const legendConfigFunction: LegendConfigFn = {
|
||||
name: LEGEND_CONFIG,
|
||||
aliases: [],
|
||||
type: LEGEND_CONFIG,
|
||||
|
@ -31,6 +25,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
|
|||
help: i18n.translate('expressionXY.legendConfig.isVisible.help', {
|
||||
defaultMessage: 'Specifies whether or not the legend is visible.',
|
||||
}),
|
||||
default: true,
|
||||
},
|
||||
position: {
|
||||
types: ['string'],
|
||||
|
@ -38,6 +33,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
|
|||
help: i18n.translate('expressionXY.legendConfig.position.help', {
|
||||
defaultMessage: 'Specifies the legend position.',
|
||||
}),
|
||||
strict: true,
|
||||
},
|
||||
showSingleSeries: {
|
||||
types: ['boolean'],
|
||||
|
@ -58,6 +54,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
|
|||
defaultMessage:
|
||||
'Specifies the horizontal alignment of the legend when it is displayed inside chart.',
|
||||
}),
|
||||
strict: true,
|
||||
},
|
||||
verticalAlignment: {
|
||||
types: ['string'],
|
||||
|
@ -66,6 +63,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
|
|||
defaultMessage:
|
||||
'Specifies the vertical alignment of the legend when it is displayed inside chart.',
|
||||
}),
|
||||
strict: true,
|
||||
},
|
||||
floatingColumns: {
|
||||
types: ['number'],
|
||||
|
@ -93,10 +91,8 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: LEGEND_CONFIG,
|
||||
...args,
|
||||
};
|
||||
async fn(input, args, handlers) {
|
||||
const { legendConfigFn } = await import('./legend_config_fn');
|
||||
return await legendConfigFn(input, args, handlers);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { LEGEND_CONFIG } from '../constants';
|
||||
import { LegendConfigFn } from '../types';
|
||||
|
||||
const errors = {
|
||||
positionUsageWithIsInsideError: () =>
|
||||
i18n.translate(
|
||||
'expressionXY.reusable.function.legendConfig.errors.positionUsageWithIsInsideError',
|
||||
{
|
||||
defaultMessage:
|
||||
'`position` argument is not applied if `isInside = true`. Please, use `horizontalAlignment` and `verticalAlignment` arguments instead.',
|
||||
}
|
||||
),
|
||||
alignmentUsageWithFalsyIsInsideError: () =>
|
||||
i18n.translate(
|
||||
'expressionXY.reusable.function.legendConfig.errors.alignmentUsageWithFalsyIsInsideError',
|
||||
{
|
||||
defaultMessage:
|
||||
'`horizontalAlignment` and `verticalAlignment` arguments are not applied if `isInside = false`. Please, use the `position` argument instead.',
|
||||
}
|
||||
),
|
||||
floatingColumnsWithFalsyIsInsideError: () =>
|
||||
i18n.translate(
|
||||
'expressionXY.reusable.function.legendConfig.errors.floatingColumnsWithFalsyIsInsideError',
|
||||
{
|
||||
defaultMessage: '`floatingColumns` arguments are not applied if `isInside = false`.',
|
||||
}
|
||||
),
|
||||
legendSizeWithFalsyIsInsideError: () =>
|
||||
i18n.translate(
|
||||
'expressionXY.reusable.function.legendConfig.errors.legendSizeWithFalsyIsInsideError',
|
||||
{
|
||||
defaultMessage: '`legendSize` argument is not applied if `isInside = false`.',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const legendConfigFn: LegendConfigFn['fn'] = async (data, args) => {
|
||||
if (args.isInside) {
|
||||
if (args.position) {
|
||||
throw new Error(errors.positionUsageWithIsInsideError());
|
||||
}
|
||||
|
||||
if (args.legendSize !== undefined) {
|
||||
throw new Error(errors.legendSizeWithFalsyIsInsideError());
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.isInside) {
|
||||
if (args.verticalAlignment || args.horizontalAlignment) {
|
||||
throw new Error(errors.alignmentUsageWithFalsyIsInsideError());
|
||||
}
|
||||
|
||||
if (args.floatingColumns !== undefined) {
|
||||
throw new Error(errors.floatingColumnsWithFalsyIsInsideError());
|
||||
}
|
||||
}
|
||||
|
||||
return { type: LEGEND_CONFIG, ...args };
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { LayerTypes, REFERENCE_LINE_LAYER } 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,
|
||||
aliases: [],
|
||||
type: REFERENCE_LINE_LAYER,
|
||||
help: strings.getRLHelp(),
|
||||
inputTypes: ['datatable'],
|
||||
args: { ...commonReferenceLineLayerArgs },
|
||||
fn(table, args) {
|
||||
return {
|
||||
type: REFERENCE_LINE_LAYER,
|
||||
...args,
|
||||
accessors: args.accessors ?? [],
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
table,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,62 +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 { i18n } from '@kbn/i18n';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { LayerTypes, REFERENCE_LINE_LAYER, Y_CONFIG } from '../constants';
|
||||
import { ReferenceLineLayerArgs, ReferenceLineLayerConfigResult } from '../types';
|
||||
|
||||
export const referenceLineLayerConfigFunction: ExpressionFunctionDefinition<
|
||||
typeof REFERENCE_LINE_LAYER,
|
||||
null,
|
||||
ReferenceLineLayerArgs,
|
||||
ReferenceLineLayerConfigResult
|
||||
> = {
|
||||
name: REFERENCE_LINE_LAYER,
|
||||
aliases: [],
|
||||
type: REFERENCE_LINE_LAYER,
|
||||
help: i18n.translate('expressionXY.referenceLineLayer.help', {
|
||||
defaultMessage: `Configure a reference line in the xy chart`,
|
||||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
layerId: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.referenceLineLayer.layerId.help', {
|
||||
defaultMessage: `Layer ID`,
|
||||
}),
|
||||
},
|
||||
accessors: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.referenceLineLayer.accessors.help', {
|
||||
defaultMessage: 'The columns to display on the y axis.',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
yConfig: {
|
||||
types: [Y_CONFIG],
|
||||
help: i18n.translate('expressionXY.referenceLineLayer.yConfig.help', {
|
||||
defaultMessage: 'Additional configuration for y axes',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
columnToLabel: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.referenceLineLayer.columnToLabel.help', {
|
||||
defaultMessage: 'JSON key-value pairs of column ID to label',
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: REFERENCE_LINE_LAYER,
|
||||
...args,
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { AxisExtentModes, ValueLabelModes } from '../constants';
|
||||
import {
|
||||
AxisExtentConfigResult,
|
||||
DataLayerConfigResult,
|
||||
ValueLabelMode,
|
||||
CommonXYDataLayerConfig,
|
||||
} from '../types';
|
||||
|
||||
const errors = {
|
||||
extendBoundsAreInvalidError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.xyVis.errors.extendBoundsAreInvalidError', {
|
||||
defaultMessage:
|
||||
'For area and bar modes, and custom extent mode, the lower bound should be less or greater than 0 and the upper bound - be greater or equal than 0',
|
||||
}),
|
||||
notUsedFillOpacityError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.xyVis.errors.notUsedFillOpacityError', {
|
||||
defaultMessage: '`fillOpacity` argument is applicable only for area charts.',
|
||||
}),
|
||||
valueLabelsForNotBarsOrHistogramBarsChartsError: () =>
|
||||
i18n.translate(
|
||||
'expressionXY.reusable.function.xyVis.errors.valueLabelsForNotBarsOrHistogramBarsChartsError',
|
||||
{
|
||||
defaultMessage:
|
||||
'`valueLabels` argument is applicable only for bar charts, which are not histograms.',
|
||||
}
|
||||
),
|
||||
dataBoundsForNotLineChartError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', {
|
||||
defaultMessage: 'Only line charts can be fit to the data bounds',
|
||||
}),
|
||||
};
|
||||
|
||||
export const hasBarLayer = (layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>) =>
|
||||
layers.filter(({ seriesType }) => seriesType.includes('bar')).length > 0;
|
||||
|
||||
export const hasAreaLayer = (layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>) =>
|
||||
layers.filter(({ seriesType }) => seriesType.includes('area')).length > 0;
|
||||
|
||||
export const hasHistogramBarLayer = (
|
||||
layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
|
||||
) =>
|
||||
layers.filter(({ seriesType, isHistogram }) => seriesType.includes('bar') && isHistogram).length >
|
||||
0;
|
||||
|
||||
export const isValidExtentWithCustomMode = (extent: AxisExtentConfigResult) => {
|
||||
const isValidLowerBound =
|
||||
extent.lowerBound === undefined || (extent.lowerBound !== undefined && extent.lowerBound <= 0);
|
||||
const isValidUpperBound =
|
||||
extent.upperBound === undefined || (extent.upperBound !== undefined && extent.upperBound >= 0);
|
||||
|
||||
return isValidLowerBound && isValidUpperBound;
|
||||
};
|
||||
|
||||
export const validateExtentForDataBounds = (
|
||||
extent: AxisExtentConfigResult,
|
||||
layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
|
||||
) => {
|
||||
const lineSeries = layers.filter(({ seriesType }) => seriesType.includes('line'));
|
||||
if (!lineSeries.length && extent.mode === AxisExtentModes.DATA_BOUNDS) {
|
||||
throw new Error(errors.dataBoundsForNotLineChartError());
|
||||
}
|
||||
};
|
||||
|
||||
export const validateExtent = (
|
||||
extent: AxisExtentConfigResult,
|
||||
hasBarOrArea: boolean,
|
||||
dataLayers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
|
||||
) => {
|
||||
if (
|
||||
extent.mode === AxisExtentModes.CUSTOM &&
|
||||
hasBarOrArea &&
|
||||
!isValidExtentWithCustomMode(extent)
|
||||
) {
|
||||
throw new Error(errors.extendBoundsAreInvalidError());
|
||||
}
|
||||
|
||||
validateExtentForDataBounds(extent, dataLayers);
|
||||
};
|
||||
|
||||
export const validateFillOpacity = (fillOpacity: number | undefined, hasArea: boolean) => {
|
||||
if (fillOpacity !== undefined && !hasArea) {
|
||||
throw new Error(errors.notUsedFillOpacityError());
|
||||
}
|
||||
};
|
||||
|
||||
export const validateValueLabels = (
|
||||
valueLabels: ValueLabelMode,
|
||||
hasBar: boolean,
|
||||
hasNotHistogramBars: boolean
|
||||
) => {
|
||||
if ((!hasBar || !hasNotHistogramBars) && valueLabels !== ValueLabelModes.HIDE) {
|
||||
throw new Error(errors.valueLabelsForNotBarsOrHistogramBarsChartsError());
|
||||
}
|
||||
};
|
|
@ -8,14 +8,23 @@
|
|||
|
||||
import { xyVisFunction } from '.';
|
||||
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
|
||||
import { sampleArgs } from '../__mocks__';
|
||||
import { sampleArgs, sampleLayer } from '../__mocks__';
|
||||
import { XY_VIS } from '../constants';
|
||||
|
||||
describe('xyVis', () => {
|
||||
test('it renders with the specified data and args', () => {
|
||||
test('it renders with the specified data and args', async () => {
|
||||
const { data, args } = sampleArgs();
|
||||
const result = xyVisFunction.fn(data, args, createMockExecutionContext());
|
||||
const { layers, ...rest } = args;
|
||||
const result = await xyVisFunction.fn(
|
||||
data,
|
||||
{ ...rest, dataLayers: [sampleLayer], referenceLineLayers: [], annotationLayers: [] },
|
||||
createMockExecutionContext()
|
||||
);
|
||||
|
||||
expect(result).toEqual({ type: 'render', as: XY_VIS, value: { data, args } });
|
||||
expect(result).toEqual({
|
||||
type: 'render',
|
||||
as: XY_VIS,
|
||||
value: { args: { ...rest, layers: [sampleLayer] } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,243 +6,36 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin';
|
||||
import { prepareLogTable } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { LensMultiTable, XYArgs, XYRender } from '../types';
|
||||
import {
|
||||
XY_VIS,
|
||||
DATA_LAYER,
|
||||
MULTITABLE,
|
||||
XYCurveTypes,
|
||||
LEGEND_CONFIG,
|
||||
ValueLabelModes,
|
||||
FittingFunctions,
|
||||
GRID_LINES_CONFIG,
|
||||
XY_VIS_RENDERER,
|
||||
AXIS_EXTENT_CONFIG,
|
||||
TICK_LABELS_CONFIG,
|
||||
REFERENCE_LINE_LAYER,
|
||||
LABELS_ORIENTATION_CONFIG,
|
||||
AXIS_TITLES_VISIBILITY_CONFIG,
|
||||
EndValues,
|
||||
ANNOTATION_LAYER,
|
||||
LayerTypes,
|
||||
} from '../constants';
|
||||
import { XyVisFn } from '../types';
|
||||
import { XY_VIS, DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { commonXYArgs } from './common_xy_args';
|
||||
|
||||
const strings = {
|
||||
getMetricHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.metric', {
|
||||
defaultMessage: 'Vertical axis',
|
||||
}),
|
||||
getXAxisHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.x', {
|
||||
defaultMessage: 'Horizontal axis',
|
||||
}),
|
||||
getBreakdownHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
getReferenceLineHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
};
|
||||
|
||||
export const xyVisFunction: ExpressionFunctionDefinition<
|
||||
typeof XY_VIS,
|
||||
LensMultiTable,
|
||||
XYArgs,
|
||||
XYRender
|
||||
> = {
|
||||
export const xyVisFunction: XyVisFn = {
|
||||
name: XY_VIS,
|
||||
type: 'render',
|
||||
inputTypes: [MULTITABLE],
|
||||
help: i18n.translate('expressionXY.xyVis.help', {
|
||||
defaultMessage: 'An X/Y chart',
|
||||
}),
|
||||
inputTypes: ['datatable'],
|
||||
help: strings.getXYHelp(),
|
||||
args: {
|
||||
title: {
|
||||
types: ['string'],
|
||||
help: 'The chart title.',
|
||||
},
|
||||
description: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
},
|
||||
xTitle: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.xyVis.xTitle.help', {
|
||||
defaultMessage: 'X axis title',
|
||||
}),
|
||||
},
|
||||
yTitle: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.xyVis.yLeftTitle.help', {
|
||||
defaultMessage: 'Y left axis title',
|
||||
}),
|
||||
},
|
||||
yRightTitle: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.xyVis.yRightTitle.help', {
|
||||
defaultMessage: 'Y right axis title',
|
||||
}),
|
||||
},
|
||||
yLeftExtent: {
|
||||
types: [AXIS_EXTENT_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.yLeftExtent.help', {
|
||||
defaultMessage: 'Y left axis extents',
|
||||
}),
|
||||
},
|
||||
yRightExtent: {
|
||||
types: [AXIS_EXTENT_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.yRightExtent.help', {
|
||||
defaultMessage: 'Y right axis extents',
|
||||
}),
|
||||
},
|
||||
legend: {
|
||||
types: [LEGEND_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.legend.help', {
|
||||
defaultMessage: 'Configure the chart legend.',
|
||||
}),
|
||||
},
|
||||
fittingFunction: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(FittingFunctions)],
|
||||
help: i18n.translate('expressionXY.xyVis.fittingFunction.help', {
|
||||
defaultMessage: 'Define how missing values are treated',
|
||||
}),
|
||||
},
|
||||
endValue: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(EndValues)],
|
||||
help: i18n.translate('expressionXY.xyVis.endValue.help', {
|
||||
defaultMessage: 'End value',
|
||||
}),
|
||||
},
|
||||
emphasizeFitting: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: '',
|
||||
},
|
||||
valueLabels: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(ValueLabelModes)],
|
||||
help: i18n.translate('expressionXY.xyVis.valueLabels.help', {
|
||||
defaultMessage: 'Value labels mode',
|
||||
}),
|
||||
},
|
||||
tickLabelsVisibilitySettings: {
|
||||
types: [TICK_LABELS_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes tick labels',
|
||||
}),
|
||||
},
|
||||
labelsOrientation: {
|
||||
types: [LABELS_ORIENTATION_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.labelsOrientation.help', {
|
||||
defaultMessage: 'Defines the rotation of the axis labels',
|
||||
}),
|
||||
},
|
||||
gridlinesVisibilitySettings: {
|
||||
types: [GRID_LINES_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes gridlines',
|
||||
}),
|
||||
},
|
||||
axisTitlesVisibilitySettings: {
|
||||
types: [AXIS_TITLES_VISIBILITY_CONFIG],
|
||||
help: i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes titles',
|
||||
}),
|
||||
},
|
||||
layers: {
|
||||
types: [DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER],
|
||||
help: i18n.translate('expressionXY.xyVis.layers.help', {
|
||||
defaultMessage: 'Layers of visual series',
|
||||
}),
|
||||
...commonXYArgs,
|
||||
dataLayers: {
|
||||
types: [DATA_LAYER],
|
||||
help: strings.getDataLayerHelp(),
|
||||
multi: true,
|
||||
},
|
||||
curveType: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(XYCurveTypes)],
|
||||
help: i18n.translate('expressionXY.xyVis.curveType.help', {
|
||||
defaultMessage: 'Define how curve type is rendered for a line chart',
|
||||
}),
|
||||
referenceLineLayers: {
|
||||
types: [REFERENCE_LINE_LAYER],
|
||||
help: strings.getReferenceLineLayerHelp(),
|
||||
multi: true,
|
||||
},
|
||||
fillOpacity: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('expressionXY.xyVis.fillOpacity.help', {
|
||||
defaultMessage: 'Define the area chart fill opacity',
|
||||
}),
|
||||
},
|
||||
hideEndzones: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('expressionXY.xyVis.hideEndzones.help', {
|
||||
defaultMessage: 'Hide endzone markers for partial data',
|
||||
}),
|
||||
},
|
||||
valuesInLegend: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('expressionXY.xyVis.valuesInLegend.help', {
|
||||
defaultMessage: 'Show values in legend',
|
||||
}),
|
||||
},
|
||||
ariaLabel: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.xyVis.ariaLabel.help', {
|
||||
defaultMessage: 'Specifies the aria label of the xy chart',
|
||||
}),
|
||||
required: false,
|
||||
annotationLayers: {
|
||||
types: [ANNOTATION_LAYER],
|
||||
help: strings.getAnnotationLayerHelp(),
|
||||
multi: true,
|
||||
},
|
||||
},
|
||||
fn(data, args, handlers) {
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
args.layers.forEach((layer) => {
|
||||
if (layer.layerType === LayerTypes.ANNOTATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
let xAccessor;
|
||||
let splitAccessor;
|
||||
if (layer.layerType === LayerTypes.DATA) {
|
||||
xAccessor = layer.xAccessor;
|
||||
splitAccessor = layer.splitAccessor;
|
||||
}
|
||||
|
||||
const { layerId, accessors, layerType } = layer;
|
||||
const logTable = prepareLogTable(
|
||||
data.tables[layerId],
|
||||
[
|
||||
[
|
||||
accessors ? accessors : undefined,
|
||||
layerType === 'data' ? strings.getMetricHelp() : strings.getReferenceLineHelp(),
|
||||
],
|
||||
[xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()],
|
||||
[splitAccessor ? [splitAccessor] : undefined, strings.getBreakdownHelp()],
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
handlers.inspectorAdapters.tables.logDatatable(layerId, logTable);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
as: XY_VIS_RENDERER,
|
||||
value: {
|
||||
data,
|
||||
args: {
|
||||
...args,
|
||||
ariaLabel:
|
||||
args.ariaLabel ??
|
||||
(handlers.variables?.embeddableTitle as string) ??
|
||||
handlers.getExecutionContext?.()?.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
async fn(data, args, handlers) {
|
||||
const { xyVisFn } = await import('./xy_vis_fn');
|
||||
return await xyVisFn(data, args, handlers);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { LayerTypes, XY_VIS_RENDERER } from '../constants';
|
||||
import { appendLayerIds } from '../helpers';
|
||||
import { XYLayerConfig, XyVisFn } from '../types';
|
||||
import { getLayerDimensions } from '../utils';
|
||||
import {
|
||||
hasAreaLayer,
|
||||
hasBarLayer,
|
||||
hasHistogramBarLayer,
|
||||
validateExtent,
|
||||
validateFillOpacity,
|
||||
validateValueLabels,
|
||||
} from './validate';
|
||||
|
||||
export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
|
||||
const { dataLayers = [], referenceLineLayers = [], annotationLayers = [], ...restArgs } = args;
|
||||
const layers: XYLayerConfig[] = [
|
||||
...appendLayerIds(dataLayers, 'dataLayers'),
|
||||
...appendLayerIds(referenceLineLayers, 'referenceLineLayers'),
|
||||
...appendLayerIds(annotationLayers, 'annotationLayers'),
|
||||
];
|
||||
|
||||
if (handlers.inspectorAdapters.tables) {
|
||||
const layerDimensions = layers.reduce<Dimension[]>((dimensions, layer) => {
|
||||
if (layer.layerType === LayerTypes.ANNOTATIONS) {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
return [...dimensions, ...getLayerDimensions(layer)];
|
||||
}, []);
|
||||
|
||||
const logTable = prepareLogTable(data, layerDimensions, true);
|
||||
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
|
||||
}
|
||||
|
||||
const hasBar = hasBarLayer(dataLayers);
|
||||
const hasArea = hasAreaLayer(dataLayers);
|
||||
|
||||
validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers);
|
||||
validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers);
|
||||
validateFillOpacity(args.fillOpacity, hasArea);
|
||||
|
||||
const hasNotHistogramBars = !hasHistogramBarLayer(dataLayers);
|
||||
|
||||
validateValueLabels(args.valueLabels, hasBar, hasNotHistogramBars);
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
as: XY_VIS_RENDERER,
|
||||
value: {
|
||||
args: {
|
||||
...restArgs,
|
||||
layers,
|
||||
ariaLabel:
|
||||
args.ariaLabel ??
|
||||
(handlers.variables?.embeddableTitle as string) ??
|
||||
handlers.getExecutionContext?.()?.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -6,84 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { FillStyles, IconPositions, LineStyles, YAxisModes, Y_CONFIG } from '../constants';
|
||||
import { YConfig, YConfigResult } from '../types';
|
||||
import { Y_CONFIG } from '../constants';
|
||||
import { YConfigFn } from '../types';
|
||||
import { strings } from '../i18n';
|
||||
import { commonYConfigArgs } from './common_y_config_args';
|
||||
|
||||
export const yAxisConfigFunction: ExpressionFunctionDefinition<
|
||||
typeof Y_CONFIG,
|
||||
null,
|
||||
YConfig,
|
||||
YConfigResult
|
||||
> = {
|
||||
export const yAxisConfigFunction: YConfigFn = {
|
||||
name: Y_CONFIG,
|
||||
aliases: [],
|
||||
type: Y_CONFIG,
|
||||
help: i18n.translate('expressionXY.yConfig.help', {
|
||||
defaultMessage: `Configure the behavior of a xy chart's y axis metric`,
|
||||
}),
|
||||
help: strings.getYConfigFnHelp(),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
forAccessor: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.yConfig.forAccessor.help', {
|
||||
defaultMessage: 'The accessor this configuration is for',
|
||||
}),
|
||||
},
|
||||
axisMode: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(YAxisModes)],
|
||||
help: i18n.translate('expressionXY.yConfig.axisMode.help', {
|
||||
defaultMessage: 'The axis mode of the metric',
|
||||
}),
|
||||
},
|
||||
color: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.yConfig.color.help', {
|
||||
defaultMessage: 'The color of the series',
|
||||
}),
|
||||
},
|
||||
lineStyle: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(LineStyles)],
|
||||
help: i18n.translate('expressionXY.yConfig.lineStyle.help', {
|
||||
defaultMessage: 'The style of the reference line',
|
||||
}),
|
||||
},
|
||||
lineWidth: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('expressionXY.yConfig.lineWidth.help', {
|
||||
defaultMessage: 'The width of the reference line',
|
||||
}),
|
||||
},
|
||||
icon: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionXY.yConfig.icon.help', {
|
||||
defaultMessage: 'An optional icon used for reference lines',
|
||||
}),
|
||||
},
|
||||
iconPosition: {
|
||||
types: ['string'],
|
||||
options: [...Object.values(IconPositions)],
|
||||
help: i18n.translate('expressionXY.yConfig.iconPosition.help', {
|
||||
defaultMessage: 'The placement of the icon for the reference line',
|
||||
}),
|
||||
},
|
||||
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',
|
||||
}),
|
||||
},
|
||||
},
|
||||
args: { ...commonYConfigArgs },
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: Y_CONFIG,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { appendLayerIds } from './layers';
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { generateLayerId, appendLayerIds } from './layers';
|
||||
|
||||
describe('#generateLayerId', () => {
|
||||
it('should return the combination of keyword and index', () => {
|
||||
const key = 'some-key';
|
||||
const index = 10;
|
||||
const id = generateLayerId(key, index);
|
||||
expect(id).toBe(`${key}-${index}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#appendLayerIds', () => {
|
||||
it('should add layerId to each layer', () => {
|
||||
const layers = [{ name: 'someName' }, { name: 'someName2' }, { name: 'someName3' }];
|
||||
const keyword = 'keyword';
|
||||
const expectedLayerIds = [
|
||||
{ ...layers[0], layerId: `${keyword}-0` },
|
||||
{ ...layers[1], layerId: `${keyword}-1` },
|
||||
{ ...layers[2], layerId: `${keyword}-2` },
|
||||
];
|
||||
|
||||
const layersWithIds = appendLayerIds(layers, keyword);
|
||||
expect(layersWithIds).toStrictEqual(expectedLayerIds);
|
||||
});
|
||||
|
||||
it('should filter out undefined layers', () => {
|
||||
const layers = [undefined, undefined, undefined];
|
||||
const result = appendLayerIds(layers, 'some-key');
|
||||
expect(result).toStrictEqual([]);
|
||||
|
||||
const layers2 = [{ name: 'someName' }, undefined, { name: 'someName3' }];
|
||||
const keyword = 'keyword';
|
||||
const expectedLayerIds = [
|
||||
{ ...layers2[0], layerId: `${keyword}-0` },
|
||||
{ ...layers2[2], layerId: `${keyword}-1` },
|
||||
];
|
||||
|
||||
const layersWithIds = appendLayerIds(layers2, keyword);
|
||||
expect(layersWithIds).toStrictEqual(expectedLayerIds);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { WithLayerId } from '../types';
|
||||
|
||||
function isWithLayerId<T>(layer: T): layer is T & WithLayerId {
|
||||
return (layer as T & WithLayerId).layerId ? true : false;
|
||||
}
|
||||
|
||||
export const generateLayerId = (keyword: string, index: number) => `${keyword}-${index}`;
|
||||
|
||||
export function appendLayerIds<T>(
|
||||
layers: Array<T | undefined>,
|
||||
keyword: string
|
||||
): Array<T & WithLayerId> {
|
||||
return layers
|
||||
.filter((l): l is T => l !== undefined)
|
||||
.map((l, index) => ({
|
||||
...l,
|
||||
layerId: isWithLayerId(l) ? l.layerId : generateLayerId(keyword, index),
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const strings = {
|
||||
getXYHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.help', {
|
||||
defaultMessage: 'An X/Y chart',
|
||||
}),
|
||||
getMetricHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.metric', {
|
||||
defaultMessage: 'Vertical axis',
|
||||
}),
|
||||
getXAxisHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.x', {
|
||||
defaultMessage: 'Horizontal axis',
|
||||
}),
|
||||
getBreakdownHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
getReferenceLineHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
getXTitleHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.xTitle.help', {
|
||||
defaultMessage: 'X axis title',
|
||||
}),
|
||||
getYTitleHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.yLeftTitle.help', {
|
||||
defaultMessage: 'Y left axis title',
|
||||
}),
|
||||
getYRightTitleHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.yRightTitle.help', {
|
||||
defaultMessage: 'Y right axis title',
|
||||
}),
|
||||
getYLeftExtentHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.yLeftExtent.help', {
|
||||
defaultMessage: 'Y left axis extents',
|
||||
}),
|
||||
getYRightExtentHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.yRightExtent.help', {
|
||||
defaultMessage: 'Y right axis extents',
|
||||
}),
|
||||
getLegendHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.legend.help', {
|
||||
defaultMessage: 'Configure the chart legend.',
|
||||
}),
|
||||
getFittingFunctionHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.fittingFunction.help', {
|
||||
defaultMessage: 'Define how missing values are treated',
|
||||
}),
|
||||
getEndValueHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.endValue.help', {
|
||||
defaultMessage: 'End value',
|
||||
}),
|
||||
getValueLabelsHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.valueLabels.help', {
|
||||
defaultMessage: 'Value labels mode',
|
||||
}),
|
||||
getTickLabelsVisibilitySettingsHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes tick labels',
|
||||
}),
|
||||
getLabelsOrientationHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.labelsOrientation.help', {
|
||||
defaultMessage: 'Defines the rotation of the axis labels',
|
||||
}),
|
||||
getGridlinesVisibilitySettingsHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes gridlines',
|
||||
}),
|
||||
getAxisTitlesVisibilitySettingsHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', {
|
||||
defaultMessage: 'Show x and y axes titles',
|
||||
}),
|
||||
getDataLayerHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.dataLayer.help', {
|
||||
defaultMessage: 'Data layer of visual series',
|
||||
}),
|
||||
getReferenceLineLayerHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.referenceLineLayer.help', {
|
||||
defaultMessage: 'Reference line layer',
|
||||
}),
|
||||
getAnnotationLayerHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.annotationLayer.help', {
|
||||
defaultMessage: 'Annotation layer',
|
||||
}),
|
||||
getCurveTypeHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.curveType.help', {
|
||||
defaultMessage: 'Define how curve type is rendered for a line chart',
|
||||
}),
|
||||
getFillOpacityHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.fillOpacity.help', {
|
||||
defaultMessage: 'Define the area chart fill opacity',
|
||||
}),
|
||||
getHideEndzonesHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.hideEndzones.help', {
|
||||
defaultMessage: 'Hide endzone markers for partial data',
|
||||
}),
|
||||
getValuesInLegendHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.valuesInLegend.help', {
|
||||
defaultMessage: 'Show values in legend',
|
||||
}),
|
||||
getAriaLabelHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.ariaLabel.help', {
|
||||
defaultMessage: 'Specifies the aria label of the xy chart',
|
||||
}),
|
||||
getDataLayerFnHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.help', {
|
||||
defaultMessage: `Configure a layer in the xy chart`,
|
||||
}),
|
||||
getHideHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.hide.help', {
|
||||
defaultMessage: 'Show / hide axis',
|
||||
}),
|
||||
getXAccessorHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.xAccessor.help', {
|
||||
defaultMessage: 'X-axis',
|
||||
}),
|
||||
getSeriesTypeHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.seriesType.help', {
|
||||
defaultMessage: 'The type of chart to display.',
|
||||
}),
|
||||
getXScaleTypeHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.xScaleType.help', {
|
||||
defaultMessage: 'The scale type of the x axis',
|
||||
}),
|
||||
getIsHistogramHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.isHistogram.help', {
|
||||
defaultMessage: 'Whether to layout the chart as a histogram',
|
||||
}),
|
||||
getYScaleTypeHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.yScaleType.help', {
|
||||
defaultMessage: 'The scale type of the y axes',
|
||||
}),
|
||||
getSplitAccessorHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.splitAccessor.help', {
|
||||
defaultMessage: 'The column to split by',
|
||||
}),
|
||||
getAccessorsHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.accessors.help', {
|
||||
defaultMessage: 'The columns to display on the y axis.',
|
||||
}),
|
||||
getYConfigHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.yConfig.help', {
|
||||
defaultMessage: 'Additional configuration for y axes',
|
||||
}),
|
||||
getColumnToLabelHelp: () =>
|
||||
i18n.translate('expressionXY.layer.columnToLabel.help', {
|
||||
defaultMessage: 'JSON key-value pairs of column ID to label',
|
||||
}),
|
||||
getPaletteHelp: () =>
|
||||
i18n.translate('expressionXY.dataLayer.palette.help', {
|
||||
defaultMessage: 'Palette',
|
||||
}),
|
||||
getTableHelp: () =>
|
||||
i18n.translate('expressionXY.layers.table.help', {
|
||||
defaultMessage: 'Table',
|
||||
}),
|
||||
getLayerIdHelp: () =>
|
||||
i18n.translate('expressionXY.layers.layerId.help', {
|
||||
defaultMessage: 'Layer ID',
|
||||
}),
|
||||
getRLAccessorsHelp: () =>
|
||||
i18n.translate('expressionXY.referenceLineLayer.accessors.help', {
|
||||
defaultMessage: 'The columns to display on the y axis.',
|
||||
}),
|
||||
getRLYConfigHelp: () =>
|
||||
i18n.translate('expressionXY.referenceLineLayer.yConfig.help', {
|
||||
defaultMessage: 'Additional configuration for y axes',
|
||||
}),
|
||||
getRLHelp: () =>
|
||||
i18n.translate('expressionXY.referenceLineLayer.help', {
|
||||
defaultMessage: `Configure a reference line in the xy chart`,
|
||||
}),
|
||||
getYConfigFnHelp: () =>
|
||||
i18n.translate('expressionXY.yConfig.help', {
|
||||
defaultMessage: `Configure the behavior of a xy chart's y axis metric`,
|
||||
}),
|
||||
getForAccessorHelp: () =>
|
||||
i18n.translate('expressionXY.yConfig.forAccessor.help', {
|
||||
defaultMessage: 'The accessor this configuration is for',
|
||||
}),
|
||||
getAxisModeHelp: () =>
|
||||
i18n.translate('expressionXY.yConfig.axisMode.help', {
|
||||
defaultMessage: 'The axis mode of the metric',
|
||||
}),
|
||||
getColorHelp: () =>
|
||||
i18n.translate('expressionXY.yConfig.color.help', {
|
||||
defaultMessage: 'The color of the series',
|
||||
}),
|
||||
getAnnotationLayerFnHelp: () =>
|
||||
i18n.translate('expressionXY.annotationLayer.help', {
|
||||
defaultMessage: `Configure an annotation layer in the xy chart`,
|
||||
}),
|
||||
getAnnotationLayerHideHelp: () =>
|
||||
i18n.translate('expressionXY.annotationLayer.hide.help', {
|
||||
defaultMessage: 'Show / hide details',
|
||||
}),
|
||||
getAnnotationLayerAnnotationsHelp: () =>
|
||||
i18n.translate('expressionXY.annotationLayer.annotations.help', {
|
||||
defaultMessage: 'Annotations',
|
||||
}),
|
||||
};
|
|
@ -9,20 +9,6 @@
|
|||
export const PLUGIN_ID = 'expressionXy';
|
||||
export const PLUGIN_NAME = 'expressionXy';
|
||||
|
||||
export {
|
||||
xyVisFunction,
|
||||
yAxisConfigFunction,
|
||||
legendConfigFunction,
|
||||
gridlinesConfigFunction,
|
||||
dataLayerConfigFunction,
|
||||
axisExtentConfigFunction,
|
||||
tickLabelsConfigFunction,
|
||||
annotationLayerConfigFunction,
|
||||
labelsOrientationConfigFunction,
|
||||
referenceLineLayerConfigFunction,
|
||||
axisTitlesVisibilityConfigFunction,
|
||||
} from './expression_functions';
|
||||
|
||||
export type {
|
||||
XYArgs,
|
||||
YConfig,
|
||||
|
@ -42,25 +28,37 @@ export type {
|
|||
XYChartProps,
|
||||
LegendConfig,
|
||||
IconPosition,
|
||||
YConfigResult,
|
||||
DataLayerArgs,
|
||||
LensMultiTable,
|
||||
ValueLabelMode,
|
||||
AxisExtentMode,
|
||||
DataLayerConfig,
|
||||
FittingFunction,
|
||||
ExtendedYConfig,
|
||||
AxisExtentConfig,
|
||||
CollectiveConfig,
|
||||
LegendConfigResult,
|
||||
AxesSettingsConfig,
|
||||
CommonXYLayerConfig,
|
||||
AnnotationLayerArgs,
|
||||
XYLayerConfigResult,
|
||||
ExtendedYConfigResult,
|
||||
GridlinesConfigResult,
|
||||
DataLayerConfigResult,
|
||||
TickLabelsConfigResult,
|
||||
AxisExtentConfigResult,
|
||||
ReferenceLineLayerArgs,
|
||||
CommonXYDataLayerConfig,
|
||||
LabelsOrientationConfig,
|
||||
AnnotationLayerConfigResult,
|
||||
ReferenceLineLayerConfig,
|
||||
AvailableReferenceLineIcon,
|
||||
XYExtendedLayerConfigResult,
|
||||
CommonXYAnnotationLayerConfig,
|
||||
ExtendedDataLayerConfigResult,
|
||||
LabelsOrientationConfigResult,
|
||||
CommonXYDataLayerConfigResult,
|
||||
ReferenceLineLayerConfigResult,
|
||||
CommonXYReferenceLineLayerConfig,
|
||||
AxisTitlesVisibilityConfigResult,
|
||||
ExtendedReferenceLineLayerConfigResult,
|
||||
CommonXYReferenceLineLayerConfigResult,
|
||||
} from './types';
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import { Datatable } from '@kbn/expressions-plugin';
|
||||
import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin';
|
||||
import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common';
|
||||
import {
|
||||
AxisExtentModes,
|
||||
|
@ -34,9 +34,17 @@ import {
|
|||
LEGEND_CONFIG,
|
||||
DATA_LAYER,
|
||||
AXIS_EXTENT_CONFIG,
|
||||
EXTENDED_DATA_LAYER,
|
||||
EXTENDED_REFERENCE_LINE_LAYER,
|
||||
ANNOTATION_LAYER,
|
||||
EndValues,
|
||||
EXTENDED_Y_CONFIG,
|
||||
AvailableReferenceLineIcons,
|
||||
XY_VIS,
|
||||
LAYERED_XY_VIS,
|
||||
EXTENDED_ANNOTATION_LAYER,
|
||||
} from '../constants';
|
||||
import { XYRender } from './expression_renderers';
|
||||
|
||||
export type EndValue = $Values<typeof EndValues>;
|
||||
export type LayerType = $Values<typeof LayerTypes>;
|
||||
|
@ -51,6 +59,7 @@ export type IconPosition = $Values<typeof IconPositions>;
|
|||
export type ValueLabelMode = $Values<typeof ValueLabelModes>;
|
||||
export type AxisExtentMode = $Values<typeof AxisExtentModes>;
|
||||
export type FittingFunction = $Values<typeof FittingFunctions>;
|
||||
export type AvailableReferenceLineIcon = $Values<typeof AvailableReferenceLineIcons>;
|
||||
|
||||
export interface AxesSettingsConfig {
|
||||
x: boolean;
|
||||
|
@ -69,11 +78,8 @@ export interface AxisConfig {
|
|||
hide?: boolean;
|
||||
}
|
||||
|
||||
export interface YConfig {
|
||||
forAccessor: string;
|
||||
axisMode?: YAxisMode;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
export interface ExtendedYConfig extends YConfig {
|
||||
icon?: AvailableReferenceLineIcon;
|
||||
lineWidth?: number;
|
||||
lineStyle?: LineStyle;
|
||||
fill?: FillStyle;
|
||||
|
@ -81,12 +87,13 @@ export interface YConfig {
|
|||
textVisibility?: boolean;
|
||||
}
|
||||
|
||||
export interface ValidLayer extends DataLayerConfigResult {
|
||||
xAccessor: NonNullable<DataLayerConfigResult['xAccessor']>;
|
||||
export interface YConfig {
|
||||
forAccessor: string;
|
||||
axisMode?: YAxisMode;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface DataLayerArgs {
|
||||
layerId: string;
|
||||
accessors: string[];
|
||||
seriesType: SeriesType;
|
||||
xAccessor?: string;
|
||||
|
@ -96,11 +103,30 @@ export interface DataLayerArgs {
|
|||
yScaleType: YScaleType;
|
||||
xScaleType: XScaleType;
|
||||
isHistogram: boolean;
|
||||
// palette will always be set on the expression
|
||||
palette: PaletteOutput;
|
||||
yConfig?: YConfigResult[];
|
||||
}
|
||||
|
||||
export interface ValidLayer extends DataLayerConfigResult {
|
||||
xAccessor: NonNullable<DataLayerConfigResult['xAccessor']>;
|
||||
}
|
||||
|
||||
export interface ExtendedDataLayerArgs {
|
||||
layerId?: string;
|
||||
accessors: string[];
|
||||
seriesType: SeriesType;
|
||||
xAccessor?: string;
|
||||
hide?: boolean;
|
||||
splitAccessor?: string;
|
||||
columnToLabel?: string; // Actually a JSON key-value pair
|
||||
yScaleType: YScaleType;
|
||||
xScaleType: XScaleType;
|
||||
isHistogram: boolean;
|
||||
palette: PaletteOutput;
|
||||
yConfig?: YConfigResult[];
|
||||
table?: Datatable;
|
||||
}
|
||||
|
||||
export interface LegendConfig {
|
||||
/**
|
||||
* Flag whether the legend should be shown. If there is just a single series, it will be hidden
|
||||
|
@ -121,11 +147,11 @@ export interface LegendConfig {
|
|||
/**
|
||||
* Horizontal Alignment of the legend when it is set inside chart
|
||||
*/
|
||||
horizontalAlignment?: HorizontalAlignment;
|
||||
horizontalAlignment?: typeof HorizontalAlignment.Right | typeof HorizontalAlignment.Left;
|
||||
/**
|
||||
* Vertical Alignment of the legend when it is set inside chart
|
||||
*/
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
|
||||
/**
|
||||
* Number of columns when legend is set inside chart
|
||||
*/
|
||||
|
@ -155,8 +181,54 @@ export interface LabelsOrientationConfig {
|
|||
|
||||
// Arguments to XY chart expression, with computed properties
|
||||
export interface XYArgs {
|
||||
title?: string;
|
||||
description?: string;
|
||||
xTitle: string;
|
||||
yTitle: string;
|
||||
yRightTitle: string;
|
||||
yLeftExtent: AxisExtentConfigResult;
|
||||
yRightExtent: AxisExtentConfigResult;
|
||||
legend: LegendConfigResult;
|
||||
endValue?: EndValue;
|
||||
emphasizeFitting?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
dataLayers: DataLayerConfigResult[];
|
||||
referenceLineLayers: ReferenceLineLayerConfigResult[];
|
||||
annotationLayers: AnnotationLayerConfigResult[];
|
||||
fittingFunction?: FittingFunction;
|
||||
axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult;
|
||||
tickLabelsVisibilitySettings?: TickLabelsConfigResult;
|
||||
gridlinesVisibilitySettings?: GridlinesConfigResult;
|
||||
labelsOrientation?: LabelsOrientationConfigResult;
|
||||
curveType?: XYCurveType;
|
||||
fillOpacity?: number;
|
||||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface LayeredXYArgs {
|
||||
xTitle: string;
|
||||
yTitle: string;
|
||||
yRightTitle: string;
|
||||
yLeftExtent: AxisExtentConfigResult;
|
||||
yRightExtent: AxisExtentConfigResult;
|
||||
legend: LegendConfigResult;
|
||||
endValue?: EndValue;
|
||||
emphasizeFitting?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
layers?: XYExtendedLayerConfigResult[];
|
||||
fittingFunction?: FittingFunction;
|
||||
axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult;
|
||||
tickLabelsVisibilitySettings?: TickLabelsConfigResult;
|
||||
gridlinesVisibilitySettings?: GridlinesConfigResult;
|
||||
labelsOrientation?: LabelsOrientationConfigResult;
|
||||
curveType?: XYCurveType;
|
||||
fillOpacity?: number;
|
||||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface XYProps {
|
||||
xTitle: string;
|
||||
yTitle: string;
|
||||
yRightTitle: string;
|
||||
|
@ -164,7 +236,7 @@ export interface XYArgs {
|
|||
yRightExtent: AxisExtentConfigResult;
|
||||
legend: LegendConfigResult;
|
||||
valueLabels: ValueLabelMode;
|
||||
layers: XYLayerConfigResult[];
|
||||
layers: CommonXYLayerConfig[];
|
||||
endValue?: EndValue;
|
||||
emphasizeFitting?: boolean;
|
||||
fittingFunction?: FittingFunction;
|
||||
|
@ -181,28 +253,48 @@ export interface XYArgs {
|
|||
|
||||
export interface AnnotationLayerArgs {
|
||||
annotations: EventAnnotationOutput[];
|
||||
layerId: string;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
export type ExtendedAnnotationLayerArgs = AnnotationLayerArgs & {
|
||||
layerId?: string;
|
||||
};
|
||||
|
||||
export type AnnotationLayerConfigResult = AnnotationLayerArgs & {
|
||||
type: typeof ANNOTATION_LAYER;
|
||||
layerType: typeof LayerTypes.ANNOTATIONS;
|
||||
};
|
||||
|
||||
export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & {
|
||||
type: typeof EXTENDED_ANNOTATION_LAYER;
|
||||
layerType: typeof LayerTypes.ANNOTATIONS;
|
||||
};
|
||||
|
||||
export interface ReferenceLineLayerArgs {
|
||||
layerId: string;
|
||||
accessors: string[];
|
||||
columnToLabel?: string;
|
||||
yConfig?: YConfigResult[];
|
||||
yConfig?: ExtendedYConfigResult[];
|
||||
}
|
||||
|
||||
export interface ExtendedReferenceLineLayerArgs {
|
||||
layerId?: string;
|
||||
accessors: string[];
|
||||
columnToLabel?: string;
|
||||
yConfig?: ExtendedYConfigResult[];
|
||||
table?: Datatable;
|
||||
}
|
||||
|
||||
export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs;
|
||||
export type XYLayerConfig = DataLayerConfig | ReferenceLineLayerConfig | AnnotationLayerConfig;
|
||||
export type XYExtendedLayerConfig =
|
||||
| ExtendedDataLayerConfig
|
||||
| ExtendedReferenceLineLayerConfig
|
||||
| ExtendedAnnotationLayerConfig;
|
||||
|
||||
export type XYLayerConfigResult =
|
||||
| DataLayerConfigResult
|
||||
| ReferenceLineLayerConfigResult
|
||||
| AnnotationLayerConfigResult;
|
||||
export type XYExtendedLayerConfigResult =
|
||||
| ExtendedDataLayerConfigResult
|
||||
| ExtendedReferenceLineLayerConfigResult
|
||||
| ExtendedAnnotationLayerConfigResult;
|
||||
|
||||
export interface LensMultiTable {
|
||||
type: typeof MULTITABLE;
|
||||
|
@ -216,14 +308,43 @@ export interface LensMultiTable {
|
|||
export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & {
|
||||
type: typeof REFERENCE_LINE_LAYER;
|
||||
layerType: typeof LayerTypes.REFERENCELINE;
|
||||
table: Datatable;
|
||||
};
|
||||
|
||||
export type DataLayerConfigResult = DataLayerArgs & {
|
||||
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;
|
||||
palette: PaletteOutput;
|
||||
table: Datatable;
|
||||
};
|
||||
|
||||
export interface WithLayerId {
|
||||
layerId: string;
|
||||
}
|
||||
|
||||
export type DataLayerConfig = DataLayerConfigResult & WithLayerId;
|
||||
export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId;
|
||||
export type AnnotationLayerConfig = AnnotationLayerConfigResult & WithLayerId;
|
||||
|
||||
export type ExtendedDataLayerConfig = ExtendedDataLayerConfigResult & WithLayerId;
|
||||
export type ExtendedReferenceLineLayerConfig = ExtendedReferenceLineLayerConfigResult & WithLayerId;
|
||||
export type ExtendedAnnotationLayerConfig = ExtendedAnnotationLayerConfigResult & WithLayerId;
|
||||
|
||||
export type ExtendedDataLayerConfigResult = Omit<ExtendedDataLayerArgs, 'palette'> & {
|
||||
type: typeof EXTENDED_DATA_LAYER;
|
||||
layerType: typeof LayerTypes.DATA;
|
||||
palette: PaletteOutput;
|
||||
table: Datatable;
|
||||
};
|
||||
|
||||
export type YConfigResult = YConfig & { type: typeof Y_CONFIG };
|
||||
export type ExtendedYConfigResult = ExtendedYConfig & { type: typeof EXTENDED_Y_CONFIG };
|
||||
|
||||
export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & {
|
||||
type: typeof AXIS_TITLES_VISIBILITY_CONFIG;
|
||||
|
@ -237,3 +358,70 @@ export type LegendConfigResult = LegendConfig & { type: typeof LEGEND_CONFIG };
|
|||
export type AxisExtentConfigResult = AxisExtentConfig & { type: typeof AXIS_EXTENT_CONFIG };
|
||||
export type GridlinesConfigResult = AxesSettingsConfig & { type: typeof GRID_LINES_CONFIG };
|
||||
export type TickLabelsConfigResult = AxesSettingsConfig & { type: typeof TICK_LABELS_CONFIG };
|
||||
|
||||
export type CommonXYLayerConfig = XYLayerConfig | XYExtendedLayerConfig;
|
||||
export type CommonXYDataLayerConfigResult = DataLayerConfigResult | ExtendedDataLayerConfigResult;
|
||||
export type CommonXYReferenceLineLayerConfigResult =
|
||||
| ReferenceLineLayerConfigResult
|
||||
| ExtendedReferenceLineLayerConfigResult;
|
||||
|
||||
export type CommonXYDataLayerConfig = DataLayerConfig | ExtendedDataLayerConfig;
|
||||
export type CommonXYReferenceLineLayerConfig =
|
||||
| ReferenceLineLayerConfig
|
||||
| ExtendedReferenceLineLayerConfig;
|
||||
|
||||
export type CommonXYAnnotationLayerConfig = AnnotationLayerConfig | ExtendedAnnotationLayerConfig;
|
||||
|
||||
export type XyVisFn = ExpressionFunctionDefinition<
|
||||
typeof XY_VIS,
|
||||
Datatable,
|
||||
XYArgs,
|
||||
Promise<XYRender>
|
||||
>;
|
||||
export type LayeredXyVisFn = ExpressionFunctionDefinition<
|
||||
typeof LAYERED_XY_VIS,
|
||||
Datatable,
|
||||
LayeredXYArgs,
|
||||
Promise<XYRender>
|
||||
>;
|
||||
|
||||
export type DataLayerFn = ExpressionFunctionDefinition<
|
||||
typeof DATA_LAYER,
|
||||
Datatable,
|
||||
DataLayerArgs,
|
||||
DataLayerConfigResult
|
||||
>;
|
||||
export type ExtendedDataLayerFn = ExpressionFunctionDefinition<
|
||||
typeof EXTENDED_DATA_LAYER,
|
||||
Datatable,
|
||||
ExtendedDataLayerArgs,
|
||||
ExtendedDataLayerConfigResult
|
||||
>;
|
||||
|
||||
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<
|
||||
typeof EXTENDED_Y_CONFIG,
|
||||
null,
|
||||
ExtendedYConfig,
|
||||
ExtendedYConfigResult
|
||||
>;
|
||||
|
||||
export type LegendConfigFn = ExpressionFunctionDefinition<
|
||||
typeof LEGEND_CONFIG,
|
||||
null,
|
||||
LegendConfig,
|
||||
Promise<LegendConfigResult>
|
||||
>;
|
||||
|
|
|
@ -6,12 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AnnotationTooltipFormatter } from '@elastic/charts';
|
||||
import {
|
||||
AvailableAnnotationIcon,
|
||||
ManualPointEventAnnotationArgs,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import { XY_VIS_RENDERER } from '../constants';
|
||||
import { LensMultiTable, XYArgs } from './expression_functions';
|
||||
import { XYProps } from './expression_functions';
|
||||
|
||||
export interface XYChartProps {
|
||||
data: LensMultiTable;
|
||||
args: XYArgs;
|
||||
args: XYProps;
|
||||
}
|
||||
|
||||
export interface XYRender {
|
||||
|
@ -19,3 +23,10 @@ export interface XYRender {
|
|||
as: typeof XY_VIS_RENDERER;
|
||||
value: XYChartProps;
|
||||
}
|
||||
|
||||
export interface CollectiveConfig extends Omit<ManualPointEventAnnotationArgs, 'icon'> {
|
||||
roundedTimestamp: number;
|
||||
axisMode: 'bottom';
|
||||
icon?: AvailableAnnotationIcon | string;
|
||||
customTooltipDetails?: AnnotationTooltipFormatter | undefined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { logDatatables, getLayerDimensions } from './log_datatables';
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { ExecutionContext } from '@kbn/expressions-plugin';
|
||||
import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { LayerTypes } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import {
|
||||
CommonXYDataLayerConfig,
|
||||
CommonXYLayerConfig,
|
||||
CommonXYReferenceLineLayerConfig,
|
||||
} from '../types';
|
||||
|
||||
export const logDatatables = (layers: CommonXYLayerConfig[], handlers: ExecutionContext) => {
|
||||
if (!handlers?.inspectorAdapters?.tables) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlers.inspectorAdapters.tables.reset();
|
||||
handlers.inspectorAdapters.tables.allowCsvExport = true;
|
||||
|
||||
layers.forEach((layer) => {
|
||||
if (layer.layerType === LayerTypes.ANNOTATIONS) {
|
||||
return;
|
||||
}
|
||||
const logTable = prepareLogTable(layer.table, getLayerDimensions(layer), true);
|
||||
handlers.inspectorAdapters.tables.logDatatable(layer.layerId, logTable);
|
||||
});
|
||||
};
|
||||
|
||||
export const getLayerDimensions = (
|
||||
layer: CommonXYDataLayerConfig | CommonXYReferenceLineLayerConfig
|
||||
): Dimension[] => {
|
||||
let xAccessor;
|
||||
let splitAccessor;
|
||||
if (layer.layerType === LayerTypes.DATA) {
|
||||
xAccessor = layer.xAccessor;
|
||||
splitAccessor = layer.splitAccessor;
|
||||
}
|
||||
|
||||
const { accessors, layerType } = layer;
|
||||
return [
|
||||
[
|
||||
accessors ? accessors : undefined,
|
||||
layerType === LayerTypes.DATA ? strings.getMetricHelp() : strings.getReferenceLineHelp(),
|
||||
],
|
||||
[xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()],
|
||||
[splitAccessor ? [splitAccessor] : undefined, strings.getBreakdownHelp()],
|
||||
];
|
||||
};
|
|
@ -6,9 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../../common';
|
||||
import { LensMultiTable } from '../../common';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import { DataLayerConfig, XYProps } from '../../common/types';
|
||||
import { mockPaletteOutput, sampleArgs } from '../../common/__mocks__';
|
||||
|
||||
const chartSetupContract = chartPluginMock.createSetupContract();
|
||||
|
@ -166,9 +168,9 @@ export const dateHistogramData: LensMultiTable = {
|
|||
},
|
||||
};
|
||||
|
||||
export const dateHistogramLayer: DataLayerConfigResult = {
|
||||
export const dateHistogramLayer: DataLayerConfig = {
|
||||
layerId: 'dateHistogramLayer',
|
||||
type: 'dataLayer',
|
||||
layerId: 'timeLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
hide: false,
|
||||
xAccessor: 'xAccessorId',
|
||||
|
@ -179,17 +181,12 @@ export const dateHistogramLayer: DataLayerConfigResult = {
|
|||
seriesType: 'bar_stacked',
|
||||
accessors: ['yAccessorId'],
|
||||
palette: mockPaletteOutput,
|
||||
table: dateHistogramData.tables.timeLayer,
|
||||
};
|
||||
|
||||
export function sampleArgsWithReferenceLine(value: number = 150) {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
referenceLine: {
|
||||
const { args: sArgs } = sampleArgs();
|
||||
const data: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
|
@ -199,26 +196,22 @@ export function sampleArgsWithReferenceLine(value: number = 150) {
|
|||
},
|
||||
],
|
||||
rows: [{ 'referenceLine-a': value }],
|
||||
},
|
||||
},
|
||||
} as LensMultiTable,
|
||||
args: {
|
||||
...args,
|
||||
};
|
||||
|
||||
const args: XYProps = {
|
||||
...sArgs,
|
||||
layers: [
|
||||
...args.layers,
|
||||
...sArgs.layers,
|
||||
{
|
||||
layerId: 'referenceLine-a',
|
||||
type: 'referenceLineLayer',
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
accessors: ['referenceLine-a'],
|
||||
layerId: 'referenceLine',
|
||||
seriesType: 'line',
|
||||
xScaleType: 'linear',
|
||||
yScaleType: 'linear',
|
||||
palette: mockPaletteOutput,
|
||||
isHistogram: false,
|
||||
hide: true,
|
||||
yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'yConfig' }],
|
||||
yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'extendedYConfig' }],
|
||||
table: data,
|
||||
},
|
||||
],
|
||||
} as XYArgs,
|
||||
};
|
||||
|
||||
return { data, args };
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -29,7 +29,12 @@ import {
|
|||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import type { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../../common/types';
|
||||
import type {
|
||||
AnnotationLayerArgs,
|
||||
CommonXYAnnotationLayerConfig,
|
||||
CollectiveConfig,
|
||||
} from '../../common';
|
||||
|
||||
import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers';
|
||||
import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers';
|
||||
|
||||
|
@ -52,12 +57,6 @@ export interface AnnotationsProps {
|
|||
outsideDimension: number;
|
||||
}
|
||||
|
||||
interface CollectiveConfig extends ManualPointEventAnnotationArgs {
|
||||
roundedTimestamp: number;
|
||||
axisMode: 'bottom';
|
||||
customTooltipDetails?: AnnotationTooltipFormatter | undefined;
|
||||
}
|
||||
|
||||
const groupVisibleConfigsByInterval = (
|
||||
layers: AnnotationLayerArgs[],
|
||||
minInterval?: number,
|
||||
|
@ -131,7 +130,7 @@ const getCommonStyles = (configArr: ManualPointEventAnnotationArgs[]) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getRangeAnnotations = (layers: AnnotationLayerConfigResult[]) => {
|
||||
export const getRangeAnnotations = (layers: CommonXYAnnotationLayerConfig[]) => {
|
||||
return layers
|
||||
.flatMap(({ annotations }) =>
|
||||
annotations.filter(
|
||||
|
@ -146,7 +145,7 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8;
|
|||
export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2;
|
||||
|
||||
export const getAnnotationsGroupedByInterval = (
|
||||
layers: AnnotationLayerConfigResult[],
|
||||
layers: CommonXYAnnotationLayerConfig[],
|
||||
minInterval?: number,
|
||||
firstTimestamp?: number,
|
||||
formatter?: FieldFormat
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 {
|
||||
AreaSeries,
|
||||
BarSeries,
|
||||
CurveType,
|
||||
LabelOverflowConstraint,
|
||||
LineSeries,
|
||||
} from '@elastic/charts';
|
||||
import React, { FC } from 'react';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { FormatFactory } from '@kbn/field-formats-plugin/common';
|
||||
import {
|
||||
CommonXYDataLayerConfig,
|
||||
EndValue,
|
||||
FittingFunction,
|
||||
ValueLabelMode,
|
||||
XYCurveType,
|
||||
} from '../../common';
|
||||
import { SeriesTypes, ValueLabelModes } from '../../common/constants';
|
||||
import {
|
||||
getColorAssignments,
|
||||
getFitOptions,
|
||||
GroupsConfiguration,
|
||||
getSeriesProps,
|
||||
DatatablesWithFormatInfo,
|
||||
} from '../helpers';
|
||||
|
||||
interface Props {
|
||||
layers: CommonXYDataLayerConfig[];
|
||||
formatFactory: FormatFactory;
|
||||
chartHasMoreThanOneBarSeries?: boolean;
|
||||
yAxesConfiguration: GroupsConfiguration;
|
||||
curveType?: XYCurveType;
|
||||
fittingFunction?: FittingFunction;
|
||||
endValue?: EndValue | undefined;
|
||||
paletteService: PaletteRegistry;
|
||||
formattedDatatables: DatatablesWithFormatInfo;
|
||||
syncColors?: boolean;
|
||||
timeZone?: string;
|
||||
emphasizeFitting?: boolean;
|
||||
fillOpacity?: number;
|
||||
shouldShowValueLabels?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
}
|
||||
|
||||
export const DataLayers: FC<Props> = ({
|
||||
layers,
|
||||
endValue,
|
||||
timeZone,
|
||||
curveType,
|
||||
syncColors,
|
||||
valueLabels,
|
||||
fillOpacity,
|
||||
formatFactory,
|
||||
paletteService,
|
||||
fittingFunction,
|
||||
emphasizeFitting,
|
||||
yAxesConfiguration,
|
||||
shouldShowValueLabels,
|
||||
formattedDatatables,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
}) => {
|
||||
const colorAssignments = getColorAssignments(layers, formatFactory);
|
||||
return (
|
||||
<>
|
||||
{layers.flatMap((layer) =>
|
||||
layer.accessors.map((accessor, accessorIndex) => {
|
||||
const { seriesType, columnToLabel, layerId } = layer;
|
||||
const columnToLabelMap: Record<string, string> = columnToLabel
|
||||
? JSON.parse(columnToLabel)
|
||||
: {};
|
||||
|
||||
// what if row values are not primitive? That is the case of, for instance, Ranges
|
||||
// remaps them to their serialized version with the formatHint metadata
|
||||
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
|
||||
const formattedDatatableInfo = formattedDatatables[layerId];
|
||||
|
||||
const isPercentage = seriesType.includes('percentage');
|
||||
|
||||
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
|
||||
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
|
||||
);
|
||||
|
||||
const seriesProps = getSeriesProps({
|
||||
layer,
|
||||
accessor,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
colorAssignments,
|
||||
formatFactory,
|
||||
columnToLabelMap,
|
||||
paletteService,
|
||||
formattedDatatableInfo,
|
||||
syncColors,
|
||||
yAxis,
|
||||
timeZone,
|
||||
emphasizeFitting,
|
||||
fillOpacity,
|
||||
});
|
||||
|
||||
const index = `${layer.layerId}-${accessorIndex}`;
|
||||
|
||||
const curve = curveType ? CurveType[curveType] : undefined;
|
||||
|
||||
switch (seriesType) {
|
||||
case SeriesTypes.LINE:
|
||||
return (
|
||||
<LineSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curve}
|
||||
/>
|
||||
);
|
||||
case SeriesTypes.BAR:
|
||||
case SeriesTypes.BAR_STACKED:
|
||||
case SeriesTypes.BAR_PERCENTAGE_STACKED:
|
||||
case SeriesTypes.BAR_HORIZONTAL:
|
||||
case SeriesTypes.BAR_HORIZONTAL_STACKED:
|
||||
case SeriesTypes.BAR_HORIZONTAL_PERCENTAGE_STACKED:
|
||||
const valueLabelsSettings = {
|
||||
displayValueSettings: {
|
||||
// This format double fixes two issues in elastic-chart
|
||||
// * when rotating the chart, the formatter is not correctly picked
|
||||
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
|
||||
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
|
||||
showValueLabel: shouldShowValueLabels && valueLabels !== ValueLabelModes.HIDE,
|
||||
isValueContainedInElement: false,
|
||||
isAlternatingValueLabel: false,
|
||||
overflowConstraints: [
|
||||
LabelOverflowConstraint.ChartEdges,
|
||||
LabelOverflowConstraint.BarGeometry,
|
||||
],
|
||||
},
|
||||
};
|
||||
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
|
||||
case SeriesTypes.AREA_STACKED:
|
||||
case SeriesTypes.AREA_PERCENTAGE_STACKED:
|
||||
return (
|
||||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction, endValue)}
|
||||
curve={curve}
|
||||
/>
|
||||
);
|
||||
case SeriesTypes.AREA:
|
||||
return (
|
||||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curve}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -11,27 +11,12 @@ import { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
|
|||
import { EuiPopover } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { ComponentType, ReactWrapper } from 'enzyme';
|
||||
import type { LensMultiTable } from '../../common';
|
||||
import type { DataLayerConfig, LensMultiTable } from '../../common';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import type { DataLayerArgs } from '../../common';
|
||||
import { getLegendAction } from './legend_action';
|
||||
import { LegendActionPopover } from './legend_action_popover';
|
||||
import { mockPaletteOutput } from '../../common/__mocks__';
|
||||
|
||||
const sampleLayer = {
|
||||
layerId: 'first',
|
||||
layerType: LayerTypes.DATA,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'c',
|
||||
accessors: ['a', 'b'],
|
||||
splitAccessor: 'splitAccessorId',
|
||||
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
||||
xScaleType: 'ordinal',
|
||||
yScaleType: 'linear',
|
||||
isHistogram: false,
|
||||
palette: mockPaletteOutput,
|
||||
} as DataLayerArgs;
|
||||
|
||||
const tables = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
|
@ -168,11 +153,26 @@ const tables = {
|
|||
},
|
||||
} as LensMultiTable['tables'];
|
||||
|
||||
const sampleLayer: DataLayerConfig = {
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'c',
|
||||
accessors: ['a', 'b'],
|
||||
splitAccessor: 'splitAccessorId',
|
||||
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
||||
xScaleType: 'ordinal',
|
||||
yScaleType: 'linear',
|
||||
isHistogram: false,
|
||||
palette: mockPaletteOutput,
|
||||
table: tables.first,
|
||||
};
|
||||
|
||||
describe('getLegendAction', function () {
|
||||
let wrapperProps: LegendActionProps;
|
||||
const Component: ComponentType<LegendActionProps> = getLegendAction(
|
||||
[sampleLayer],
|
||||
tables,
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
{}
|
||||
|
|
|
@ -9,23 +9,28 @@
|
|||
import React from 'react';
|
||||
import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts';
|
||||
import type { FilterEvent } from '../types';
|
||||
import type { LensMultiTable, DataLayerArgs } from '../../common';
|
||||
import type { CommonXYDataLayerConfig } from '../../common';
|
||||
import type { FormatFactory } from '../types';
|
||||
import { LegendActionPopover } from './legend_action_popover';
|
||||
import { DatatablesWithFormatInfo } from '../helpers';
|
||||
|
||||
export const getLegendAction = (
|
||||
filteredLayers: DataLayerArgs[],
|
||||
tables: LensMultiTable['tables'],
|
||||
dataLayers: CommonXYDataLayerConfig[],
|
||||
onFilter: (data: FilterEvent['data']) => void,
|
||||
formatFactory: FormatFactory,
|
||||
layersAlreadyFormatted: Record<string, boolean>
|
||||
formattedDatatables: DatatablesWithFormatInfo
|
||||
): LegendAction =>
|
||||
React.memo(({ series: [xySeries] }) => {
|
||||
const series = xySeries as XYChartSeriesIdentifier;
|
||||
const layer = filteredLayers.find((l) =>
|
||||
const layerIndex = dataLayers.findIndex((l) =>
|
||||
series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
|
||||
);
|
||||
|
||||
if (layerIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layer = dataLayers[layerIndex];
|
||||
if (!layer || !layer.splitAccessor) {
|
||||
return null;
|
||||
}
|
||||
|
@ -33,12 +38,12 @@ export const getLegendAction = (
|
|||
const splitLabel = series.seriesKeys[0] as string;
|
||||
const accessor = layer.splitAccessor;
|
||||
|
||||
const table = tables[layer.layerId];
|
||||
const { table } = layer;
|
||||
const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor);
|
||||
const formatter = formatFactory(splitColumn && splitColumn.meta?.params);
|
||||
|
||||
const rowIndex = table.rows.findIndex((row) => {
|
||||
if (layersAlreadyFormatted[accessor]) {
|
||||
if (formattedDatatables[layer.layerId]?.formattedColumns[accessor]) {
|
||||
// stringify the value to compare with the chart value
|
||||
return formatter.convert(row[accessor]) === splitLabel;
|
||||
}
|
||||
|
@ -63,7 +68,7 @@ export const getLegendAction = (
|
|||
return (
|
||||
<LegendActionPopover
|
||||
label={
|
||||
!layersAlreadyFormatted[accessor] && formatter
|
||||
!formattedDatatables[layer.layerId]?.formattedColumns[accessor] && formatter
|
||||
? formatter.convert(splitLabel)
|
||||
: splitLabel
|
||||
}
|
||||
|
|
|
@ -9,9 +9,14 @@
|
|||
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 { LensMultiTable } from '../../common';
|
||||
import { ReferenceLineLayerArgs, YConfig } from '../../common/types';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import {
|
||||
ReferenceLineLayerArgs,
|
||||
ReferenceLineLayerConfig,
|
||||
ExtendedYConfig,
|
||||
} from '../../common/types';
|
||||
import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines';
|
||||
|
||||
const row: Record<string, number> = {
|
||||
|
@ -23,10 +28,7 @@ const row: Record<string, number> = {
|
|||
yAccessorRightSecondId: 10,
|
||||
};
|
||||
|
||||
const histogramData: LensMultiTable = {
|
||||
type: 'lens_multitable',
|
||||
tables: {
|
||||
firstLayer: {
|
||||
const data: Datatable = {
|
||||
type: 'datatable',
|
||||
rows: [row],
|
||||
columns: Object.keys(row).map((id) => ({
|
||||
|
@ -37,20 +39,17 @@ const histogramData: LensMultiTable = {
|
|||
params: { id: 'number' },
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
dateRange: {
|
||||
fromDate: new Date('2020-04-01T16:14:16.246Z'),
|
||||
toDate: new Date('2020-04-01T17:15:41.263Z'),
|
||||
},
|
||||
};
|
||||
|
||||
function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerArgs[] {
|
||||
function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] {
|
||||
return [
|
||||
{
|
||||
layerId: 'firstLayer',
|
||||
layerId: 'first',
|
||||
accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor),
|
||||
yConfig: yConfigs,
|
||||
type: 'referenceLineLayer',
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
table: data,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -64,7 +63,7 @@ interface XCoords {
|
|||
x1: number | undefined;
|
||||
}
|
||||
|
||||
function getAxisFromId(layerPrefix: string): YConfig['axisMode'] {
|
||||
function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] {
|
||||
return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom';
|
||||
}
|
||||
|
||||
|
@ -95,21 +94,20 @@ describe('ReferenceLineAnnotations', () => {
|
|||
['yAccessorLeft', 'below'],
|
||||
['yAccessorRight', 'above'],
|
||||
['yAccessorRight', 'below'],
|
||||
] as Array<[string, YConfig['fill']]>)(
|
||||
] 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}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `${layerPrefix}FirstId`,
|
||||
axisMode,
|
||||
lineStyle: 'solid',
|
||||
fill,
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
|
@ -135,19 +133,18 @@ describe('ReferenceLineAnnotations', () => {
|
|||
it.each([
|
||||
['xAccessor', 'above'],
|
||||
['xAccessor', 'below'],
|
||||
] as Array<[string, YConfig['fill']]>)(
|
||||
] 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}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `${layerPrefix}FirstId`,
|
||||
axisMode: 'bottom',
|
||||
lineStyle: 'solid',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
fill,
|
||||
},
|
||||
])}
|
||||
|
@ -176,27 +173,26 @@ describe('ReferenceLineAnnotations', () => {
|
|||
['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, YConfig['fill'], YCoords, YCoords]>)(
|
||||
] 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}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `${layerPrefix}FirstId`,
|
||||
axisMode,
|
||||
lineStyle: 'solid',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
fill,
|
||||
},
|
||||
{
|
||||
forAccessor: `${layerPrefix}SecondId`,
|
||||
axisMode,
|
||||
lineStyle: 'solid',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
fill,
|
||||
},
|
||||
])}
|
||||
|
@ -227,26 +223,25 @@ describe('ReferenceLineAnnotations', () => {
|
|||
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, YConfig['fill'], XCoords, XCoords]>)(
|
||||
] 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}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `${layerPrefix}FirstId`,
|
||||
axisMode: 'bottom',
|
||||
lineStyle: 'solid',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
fill,
|
||||
},
|
||||
{
|
||||
forAccessor: `${layerPrefix}SecondId`,
|
||||
axisMode: 'bottom',
|
||||
lineStyle: 'solid',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
fill,
|
||||
},
|
||||
])}
|
||||
|
@ -282,21 +277,20 @@ describe('ReferenceLineAnnotations', () => {
|
|||
const wrapper = shallow(
|
||||
<ReferenceLineAnnotations
|
||||
{...defaultProps}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `${layerPrefix}FirstId`,
|
||||
axisMode,
|
||||
lineStyle: 'solid',
|
||||
fill: 'above',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
},
|
||||
{
|
||||
forAccessor: `${layerPrefix}SecondId`,
|
||||
axisMode,
|
||||
lineStyle: 'solid',
|
||||
fill: 'below',
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
|
@ -326,27 +320,26 @@ describe('ReferenceLineAnnotations', () => {
|
|||
it.each([
|
||||
['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }],
|
||||
['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }],
|
||||
] as Array<[YConfig['fill'], YCoords, YCoords]>)(
|
||||
] 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}
|
||||
data={histogramData}
|
||||
layers={createLayers([
|
||||
{
|
||||
forAccessor: `yAccessorLeftFirstId`,
|
||||
axisMode: 'left',
|
||||
lineStyle: 'solid',
|
||||
fill,
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
},
|
||||
{
|
||||
forAccessor: `yAccessorRightSecondId`,
|
||||
axisMode: 'right',
|
||||
lineStyle: 'solid',
|
||||
fill,
|
||||
type: 'yConfig',
|
||||
type: 'extendedYConfig',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
|
|
|
@ -13,8 +13,7 @@ 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 { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/types';
|
||||
import type { LensMultiTable } from '../../common/types';
|
||||
import type { CommonXYReferenceLineLayerConfig, IconPosition, YAxisMode } from '../../common/types';
|
||||
import {
|
||||
LINES_MARKER_SIZE,
|
||||
mapVerticalToHorizontalPlacement,
|
||||
|
@ -89,8 +88,7 @@ export function getBaseIconPlacement(
|
|||
}
|
||||
|
||||
export interface ReferenceLineAnnotationsProps {
|
||||
layers: ReferenceLineLayerArgs[];
|
||||
data: LensMultiTable;
|
||||
layers: CommonXYReferenceLineLayerConfig[];
|
||||
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
|
||||
axesMap: Record<'left' | 'right', boolean>;
|
||||
isHorizontal: boolean;
|
||||
|
@ -99,7 +97,6 @@ export interface ReferenceLineAnnotationsProps {
|
|||
|
||||
export const ReferenceLineAnnotations = ({
|
||||
layers,
|
||||
data,
|
||||
formatters,
|
||||
axesMap,
|
||||
isHorizontal,
|
||||
|
@ -111,11 +108,10 @@ export const ReferenceLineAnnotations = ({
|
|||
if (!layer.yConfig) {
|
||||
return [];
|
||||
}
|
||||
const { columnToLabel, yConfig: yConfigs, layerId } = layer;
|
||||
const { columnToLabel, yConfig: yConfigs, table } = layer;
|
||||
const columnToLabelMap: Record<string, string> = columnToLabel
|
||||
? JSON.parse(columnToLabel)
|
||||
: {};
|
||||
const table = data.tables[layerId];
|
||||
|
||||
const row = table.rows[0];
|
||||
|
||||
|
@ -194,8 +190,8 @@ export const ReferenceLineAnnotations = ({
|
|||
annotations.push(
|
||||
<LineAnnotation
|
||||
{...props}
|
||||
id={`${layerId}-${yConfig.forAccessor}-line`}
|
||||
key={`${layerId}-${yConfig.forAccessor}-line`}
|
||||
id={`${layer.layerId}-${yConfig.forAccessor}-line`}
|
||||
key={`${layer.layerId}-${yConfig.forAccessor}-line`}
|
||||
dataValues={table.rows.map(() => ({
|
||||
dataValue: row[yConfig.forAccessor],
|
||||
header: columnToLabelMap[yConfig.forAccessor],
|
||||
|
@ -225,8 +221,8 @@ export const ReferenceLineAnnotations = ({
|
|||
annotations.push(
|
||||
<RectAnnotation
|
||||
{...props}
|
||||
id={`${layerId}-${yConfig.forAccessor}-rect`}
|
||||
key={`${layerId}-${yConfig.forAccessor}-rect`}
|
||||
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]
|
||||
|
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||
import moment from 'moment';
|
||||
import { Endzones } from '@kbn/charts-plugin/public';
|
||||
import { search } from '@kbn/data-plugin/public';
|
||||
import type { LensMultiTable, DataLayerArgs } from '../../common';
|
||||
import type { CommonXYDataLayerConfig } from '../../common';
|
||||
|
||||
export interface XDomain {
|
||||
min?: number;
|
||||
|
@ -19,17 +19,16 @@ export interface XDomain {
|
|||
minInterval?: number;
|
||||
}
|
||||
|
||||
export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTable) => {
|
||||
return Object.entries(data.tables)
|
||||
.map(([tableId, table]) => {
|
||||
const layer = layers.find((l) => l.layerId === tableId);
|
||||
const xColumn = table.columns.find((col) => col.id === layer?.xAccessor);
|
||||
export const getAppliedTimeRange = (layers: CommonXYDataLayerConfig[]) => {
|
||||
return layers
|
||||
.map(({ xAccessor, table }) => {
|
||||
const xColumn = table.columns.find((col) => col.id === xAccessor);
|
||||
const timeRange =
|
||||
xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange;
|
||||
if (timeRange) {
|
||||
return {
|
||||
timeRange,
|
||||
field: xColumn.meta.field,
|
||||
field: xColumn?.meta.field,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -37,13 +36,12 @@ export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTabl
|
|||
};
|
||||
|
||||
export const getXDomain = (
|
||||
layers: DataLayerArgs[],
|
||||
data: LensMultiTable,
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
minInterval: number | undefined,
|
||||
isTimeViz: boolean,
|
||||
isHistogram: boolean
|
||||
) => {
|
||||
const appliedTimeRange = getAppliedTimeRange(layers, data)?.timeRange;
|
||||
const appliedTimeRange = getAppliedTimeRange(layers)?.timeRange;
|
||||
const from = appliedTimeRange?.from;
|
||||
const to = appliedTimeRange?.to;
|
||||
const baseDomain = isTimeViz
|
||||
|
@ -59,8 +57,8 @@ export const getXDomain = (
|
|||
if (isHistogram && isFullyQualified(baseDomain)) {
|
||||
const xValues = uniq(
|
||||
layers
|
||||
.flatMap((layer) =>
|
||||
data.tables[layer.layerId].rows.map((row) => row[layer.xAccessor!].valueOf() as number)
|
||||
.flatMap<number>(({ table, xAccessor }) =>
|
||||
table.rows.map((row) => row[xAccessor!].valueOf())
|
||||
)
|
||||
.sort()
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,66 +6,56 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import {
|
||||
Chart,
|
||||
Settings,
|
||||
Axis,
|
||||
LineSeries,
|
||||
AreaSeries,
|
||||
BarSeries,
|
||||
Position,
|
||||
GeometryValue,
|
||||
XYChartSeriesIdentifier,
|
||||
StackMode,
|
||||
VerticalAlignment,
|
||||
HorizontalAlignment,
|
||||
LayoutDirection,
|
||||
ElementClickListener,
|
||||
BrushEndListener,
|
||||
XYBrushEvent,
|
||||
CurveType,
|
||||
LegendPositionConfig,
|
||||
LabelOverflowConstraint,
|
||||
DisplayValueStyle,
|
||||
RecursivePartial,
|
||||
AxisStyle,
|
||||
ScaleType,
|
||||
AreaSeriesProps,
|
||||
BarSeriesProps,
|
||||
LineSeriesProps,
|
||||
ColorVariant,
|
||||
Placement,
|
||||
} from '@elastic/charts';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PaletteRegistry, SeriesLayer } from '@kbn/coloring';
|
||||
import type { Datatable, DatatableRow, DatatableColumn } from '@kbn/expressions-plugin/public';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { RenderMode } from '@kbn/expressions-plugin/common';
|
||||
import { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public';
|
||||
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
|
||||
import type { FilterEvent, BrushEvent, FormatFactory } from '../types';
|
||||
import type { SeriesType, XYChartProps } from '../../common/types';
|
||||
import { isHorizontalChart, getSeriesColor, getAnnotationsLayers, getDataLayers } from '../helpers';
|
||||
import type { CommonXYDataLayerConfig, SeriesType, XYChartProps } from '../../common/types';
|
||||
import {
|
||||
isHorizontalChart,
|
||||
getAnnotationsLayers,
|
||||
getDataLayers,
|
||||
Series,
|
||||
getFormattedTablesByLayers,
|
||||
validateExtent,
|
||||
} from '../helpers';
|
||||
import {
|
||||
getFilteredLayers,
|
||||
getReferenceLayers,
|
||||
isDataLayer,
|
||||
getFitOptions,
|
||||
getAxesConfiguration,
|
||||
GroupsConfiguration,
|
||||
validateExtent,
|
||||
getColorAssignments,
|
||||
getLinesCausedPaddings,
|
||||
} from '../helpers';
|
||||
import { getXDomain, XyEndzones } from './x_domain';
|
||||
import { getLegendAction } from './legend_action';
|
||||
import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines';
|
||||
import { visualizationDefinitions } from '../definitions';
|
||||
import { XYLayerConfigResult } from '../../common/types';
|
||||
import { CommonXYLayerConfig } from '../../common/types';
|
||||
import {
|
||||
Annotations,
|
||||
getAnnotationsGroupedByInterval,
|
||||
|
@ -73,7 +63,8 @@ import {
|
|||
OUTSIDE_RECT_ANNOTATION_WIDTH,
|
||||
OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION,
|
||||
} from './annotations';
|
||||
|
||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants';
|
||||
import { DataLayers } from './data_layers';
|
||||
import './xy_chart.scss';
|
||||
|
||||
declare global {
|
||||
|
@ -85,8 +76,6 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
|
||||
|
||||
export type XYChartRenderProps = XYChartProps & {
|
||||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
chartsActiveCursorService: ChartsPluginStart['activeCursor'];
|
||||
|
@ -104,8 +93,6 @@ export type XYChartRenderProps = XYChartProps & {
|
|||
eventAnnotationService: EventAnnotationServiceType;
|
||||
};
|
||||
|
||||
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
|
||||
|
||||
function getValueLabelsStyling(isHorizontal: boolean): {
|
||||
displayValue: RecursivePartial<DisplayValueStyle>;
|
||||
} {
|
||||
|
@ -134,7 +121,6 @@ function getIconForSeriesType(seriesType: SeriesType): IconType {
|
|||
export const XYChartReportable = React.memo(XYChart);
|
||||
|
||||
export function XYChart({
|
||||
data,
|
||||
args,
|
||||
formatFactory,
|
||||
timeZone,
|
||||
|
@ -166,50 +152,47 @@ export function XYChart({
|
|||
const chartTheme = chartsThemeService.useChartsTheme();
|
||||
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
|
||||
const darkMode = chartsThemeService.useDarkMode();
|
||||
const filteredLayers = getFilteredLayers(layers, data);
|
||||
const layersById = filteredLayers.reduce<Record<string, XYLayerConfigResult>>(
|
||||
(hashMap, layer) => {
|
||||
hashMap[layer.layerId] = layer;
|
||||
return hashMap;
|
||||
},
|
||||
const filteredLayers = getFilteredLayers(layers);
|
||||
const layersById = filteredLayers.reduce<Record<string, CommonXYLayerConfig>>(
|
||||
(hashMap, layer) => ({ ...hashMap, [layer.layerId]: layer }),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, {
|
||||
datatables: Object.values(data.tables),
|
||||
datatables: filteredLayers.map(({ table }) => table),
|
||||
});
|
||||
|
||||
const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer);
|
||||
const formattedDatatables = useMemo(
|
||||
() => getFormattedTablesByLayers(dataLayers, formatFactory),
|
||||
[dataLayers, formatFactory]
|
||||
);
|
||||
|
||||
if (filteredLayers.length === 0) {
|
||||
const icon: IconType = getIconForSeriesType(getDataLayers(layers)?.[0]?.seriesType || 'bar');
|
||||
const icon: IconType = getIconForSeriesType(
|
||||
getDataLayers(layers)?.[0]?.seriesType || SeriesTypes.BAR
|
||||
);
|
||||
return <EmptyPlaceholder className="xyChart__empty" icon={icon} />;
|
||||
}
|
||||
|
||||
// use formatting hint of first x axis column to format ticks
|
||||
const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find(
|
||||
({ id }) => id === filteredLayers[0].xAccessor
|
||||
);
|
||||
const xAxisColumn = dataLayers[0]?.table.columns.find(({ id }) => id === dataLayers[0].xAccessor);
|
||||
|
||||
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params);
|
||||
const layersAlreadyFormatted: Record<string, boolean> = {};
|
||||
|
||||
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
|
||||
const safeXAccessorLabelRenderer = (value: unknown): string =>
|
||||
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id]
|
||||
xAxisColumn && formattedDatatables[dataLayers[0]?.layerId]?.formattedColumns[xAxisColumn.id]
|
||||
? String(value)
|
||||
: String(xAxisFormatter.convert(value));
|
||||
|
||||
const chartHasMoreThanOneSeries =
|
||||
filteredLayers.length > 1 ||
|
||||
filteredLayers.some((layer) => layer.accessors.length > 1) ||
|
||||
filteredLayers.some((layer) => layer.splitAccessor);
|
||||
const shouldRotate = isHorizontalChart(filteredLayers);
|
||||
filteredLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor);
|
||||
const shouldRotate = isHorizontalChart(dataLayers);
|
||||
|
||||
const yAxesConfiguration = getAxesConfiguration(
|
||||
filteredLayers,
|
||||
shouldRotate,
|
||||
data.tables,
|
||||
formatFactory
|
||||
);
|
||||
const yAxesConfiguration = getAxesConfiguration(dataLayers, shouldRotate, formatFactory);
|
||||
|
||||
const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name);
|
||||
const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || {
|
||||
|
@ -223,25 +206,20 @@ export function XYChart({
|
|||
yRight: true,
|
||||
};
|
||||
|
||||
const labelsOrientation = args.labelsOrientation || {
|
||||
x: 0,
|
||||
yLeft: 0,
|
||||
yRight: 0,
|
||||
};
|
||||
const labelsOrientation = args.labelsOrientation || { x: 0, yLeft: 0, yRight: 0 };
|
||||
|
||||
const filteredBarLayers = filteredLayers.filter((layer) => layer.seriesType.includes('bar'));
|
||||
const filteredBarLayers = dataLayers.filter((layer) => layer.seriesType.includes('bar'));
|
||||
|
||||
const chartHasMoreThanOneBarSeries =
|
||||
filteredBarLayers.length > 1 ||
|
||||
filteredBarLayers.some((layer) => layer.accessors.length > 1) ||
|
||||
filteredBarLayers.some((layer) => layer.splitAccessor);
|
||||
filteredBarLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor);
|
||||
|
||||
const isTimeViz = Boolean(filteredLayers.every((l) => l.xScaleType === 'time'));
|
||||
const isHistogramViz = filteredLayers.every((l) => l.isHistogram);
|
||||
const isTimeViz = Boolean(dataLayers.every((l) => l.xScaleType === 'time'));
|
||||
const isHistogramViz = dataLayers.every((l) => l.isHistogram);
|
||||
|
||||
const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain(
|
||||
filteredLayers,
|
||||
data,
|
||||
dataLayers,
|
||||
minInterval,
|
||||
isTimeViz,
|
||||
isHistogramViz
|
||||
|
@ -252,17 +230,16 @@ export function XYChart({
|
|||
right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
|
||||
};
|
||||
|
||||
const getYAxesTitles = (
|
||||
axisSeries: Array<{ layer: string; accessor: string }>,
|
||||
groupId: string
|
||||
) => {
|
||||
const getYAxesTitles = (axisSeries: Series[], groupId: 'right' | 'left') => {
|
||||
const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle;
|
||||
return (
|
||||
yTitle ||
|
||||
axisSeries
|
||||
.map(
|
||||
(series) =>
|
||||
data.tables[series.layer].columns.find((column) => column.id === series.accessor)?.name
|
||||
filteredLayers
|
||||
.find(({ layerId }) => series.layer === layerId)
|
||||
?.table.columns.find((column) => column.id === series.accessor)?.name
|
||||
)
|
||||
.filter((name) => Boolean(name))[0]
|
||||
);
|
||||
|
@ -270,9 +247,9 @@ export function XYChart({
|
|||
|
||||
const referenceLineLayers = getReferenceLayers(layers);
|
||||
const annotationsLayers = getAnnotationsLayers(layers);
|
||||
const firstTable = data.tables[filteredLayers[0].layerId];
|
||||
const firstTable = dataLayers[0]?.table;
|
||||
|
||||
const xColumnId = firstTable.columns.find((col) => col.id === filteredLayers[0].xAccessor)?.id;
|
||||
const xColumnId = firstTable.columns.find((col) => col.id === dataLayers[0]?.xAccessor)?.id;
|
||||
|
||||
const groupedLineAnnotations = getAnnotationsGroupedByInterval(
|
||||
annotationsLayers,
|
||||
|
@ -339,10 +316,11 @@ export function XYChart({
|
|||
return layer.seriesType.includes('bar') || layer.seriesType.includes('area');
|
||||
})
|
||||
);
|
||||
const fit = !hasBarOrArea && extent.mode === 'dataBounds';
|
||||
|
||||
const fit = !hasBarOrArea && extent.mode === AxisExtentModes.DATA_BOUNDS;
|
||||
|
||||
let min: number = NaN;
|
||||
let max: number = NaN;
|
||||
|
||||
if (extent.mode === 'custom') {
|
||||
const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent);
|
||||
if (!inclusiveZeroError && !boundaryError) {
|
||||
|
@ -369,38 +347,42 @@ export function XYChart({
|
|||
|
||||
const shouldShowValueLabels =
|
||||
// No stacked bar charts
|
||||
filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) &&
|
||||
dataLayers.every((layer) => !layer.seriesType.includes('stacked')) &&
|
||||
// No histogram charts
|
||||
!isHistogramViz;
|
||||
|
||||
const valueLabelsStyling =
|
||||
shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate);
|
||||
|
||||
const colorAssignments = getColorAssignments(getDataLayers(args.layers), data, formatFactory);
|
||||
shouldShowValueLabels &&
|
||||
valueLabels !== ValueLabelModes.HIDE &&
|
||||
getValueLabelsStyling(shouldRotate);
|
||||
|
||||
const clickHandler: ElementClickListener = ([[geometry, series]]) => {
|
||||
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
|
||||
const xySeries = series as XYChartSeriesIdentifier;
|
||||
const xyGeometry = geometry as GeometryValue;
|
||||
|
||||
const layer = filteredLayers.find((l) =>
|
||||
const layerIndex = dataLayers.findIndex((l) =>
|
||||
xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
|
||||
);
|
||||
if (!layer) {
|
||||
|
||||
if (layerIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = data.tables[layer.layerId];
|
||||
const layer = dataLayers[layerIndex];
|
||||
const { table } = layer;
|
||||
|
||||
const xColumn = table.columns.find((col) => col.id === layer.xAccessor);
|
||||
const currentXFormatter =
|
||||
layer.xAccessor && layersAlreadyFormatted[layer.xAccessor] && xColumn
|
||||
layer.xAccessor &&
|
||||
formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor] &&
|
||||
xColumn
|
||||
? formatFactory(xColumn.meta.params)
|
||||
: xAxisFormatter;
|
||||
|
||||
const rowIndex = table.rows.findIndex((row) => {
|
||||
if (layer.xAccessor) {
|
||||
if (layersAlreadyFormatted[layer.xAccessor]) {
|
||||
if (formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor]) {
|
||||
// stringify the value to compare with the chart value
|
||||
return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x;
|
||||
}
|
||||
|
@ -425,7 +407,7 @@ export function XYChart({
|
|||
points.push({
|
||||
row: table.rows.findIndex((row) => {
|
||||
if (layer.splitAccessor) {
|
||||
if (layersAlreadyFormatted[layer.splitAccessor]) {
|
||||
if (formattedDatatables[layer.layerId]?.formattedColumns[layer.splitAccessor]) {
|
||||
return splitFormatter.convert(row[layer.splitAccessor]) === pointValue;
|
||||
}
|
||||
return row[layer.splitAccessor] === pointValue;
|
||||
|
@ -436,12 +418,7 @@ export function XYChart({
|
|||
});
|
||||
}
|
||||
const context: FilterEvent['data'] = {
|
||||
data: points.map((point) => ({
|
||||
row: point.row,
|
||||
column: point.column,
|
||||
value: point.value,
|
||||
table,
|
||||
})),
|
||||
data: points.map(({ row, column, value }) => ({ row, column, value, table })),
|
||||
};
|
||||
onClickValue(context);
|
||||
};
|
||||
|
@ -455,27 +432,23 @@ export function XYChart({
|
|||
return;
|
||||
}
|
||||
|
||||
const table = data.tables[filteredLayers[0].layerId];
|
||||
const { table } = dataLayers[0];
|
||||
|
||||
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor);
|
||||
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === dataLayers[0].xAccessor);
|
||||
|
||||
const context: BrushEvent['data'] = {
|
||||
range: [min, max],
|
||||
table,
|
||||
column: xAxisColumnIndex,
|
||||
};
|
||||
const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex };
|
||||
onSelectRange(context);
|
||||
};
|
||||
|
||||
const legendInsideParams = {
|
||||
const legendInsideParams: LegendPositionConfig = {
|
||||
vAlign: legend.verticalAlignment ?? VerticalAlignment.Top,
|
||||
hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right,
|
||||
direction: LayoutDirection.Vertical,
|
||||
floating: true,
|
||||
floatingColumns: legend?.floatingColumns ?? 1,
|
||||
} as LegendPositionConfig;
|
||||
};
|
||||
|
||||
const isHistogramModeEnabled = filteredLayers.some(
|
||||
const isHistogramModeEnabled = dataLayers.some(
|
||||
({ isHistogram, seriesType }) =>
|
||||
isHistogram &&
|
||||
(seriesType.includes('stacked') ||
|
||||
|
@ -570,13 +543,7 @@ export function XYChart({
|
|||
onElementClick={interactive ? clickHandler : undefined}
|
||||
legendAction={
|
||||
interactive
|
||||
? getLegendAction(
|
||||
filteredLayers,
|
||||
data.tables,
|
||||
onClickValue,
|
||||
formatFactory,
|
||||
layersAlreadyFormatted
|
||||
)
|
||||
? getLegendAction(dataLayers, onClickValue, formatFactory, formattedDatatables)
|
||||
: undefined
|
||||
}
|
||||
showLegendExtra={isHistogramViz && valuesInLegend}
|
||||
|
@ -589,7 +556,7 @@ export function XYChart({
|
|||
position={shouldRotate ? Position.Left : Position.Bottom}
|
||||
title={xTitle}
|
||||
gridLine={gridLineStyle}
|
||||
hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor}
|
||||
hide={dataLayers[0]?.hide || !dataLayers[0]?.xAccessor}
|
||||
tickFormat={(d) => safeXAccessorLabelRenderer(d)}
|
||||
style={xAxisStyle}
|
||||
timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0}
|
||||
|
@ -609,9 +576,9 @@ export function XYChart({
|
|||
? gridlinesVisibilitySettings?.yRight
|
||||
: gridlinesVisibilitySettings?.yLeft,
|
||||
}}
|
||||
hide={filteredLayers[0].hide}
|
||||
hide={dataLayers[0]?.hide}
|
||||
tickFormat={(d) => axis.formatter?.convert(d) || ''}
|
||||
style={getYAxesStyle(axis.groupId as 'left' | 'right')}
|
||||
style={getYAxesStyle(axis.groupId)}
|
||||
domain={getYAxisDomain(axis)}
|
||||
ticks={5}
|
||||
/>
|
||||
|
@ -623,7 +590,7 @@ export function XYChart({
|
|||
baseDomain={rawXDomain}
|
||||
extendedDomain={xDomain}
|
||||
darkMode={darkMode}
|
||||
histogramMode={filteredLayers.every(
|
||||
histogramMode={dataLayers.every(
|
||||
(layer) =>
|
||||
layer.isHistogram &&
|
||||
(layer.seriesType.includes('stacked') || !layer.splitAccessor) &&
|
||||
|
@ -634,282 +601,28 @@ export function XYChart({
|
|||
/>
|
||||
)}
|
||||
|
||||
{filteredLayers.flatMap((layer, layerIndex) =>
|
||||
layer.accessors.map((accessor, accessorIndex) => {
|
||||
const {
|
||||
splitAccessor,
|
||||
seriesType,
|
||||
accessors,
|
||||
xAccessor,
|
||||
layerId,
|
||||
columnToLabel,
|
||||
yScaleType,
|
||||
xScaleType,
|
||||
isHistogram,
|
||||
palette,
|
||||
} = layer;
|
||||
const columnToLabelMap: Record<string, string> = columnToLabel
|
||||
? JSON.parse(columnToLabel)
|
||||
: {};
|
||||
|
||||
const table = data.tables[layerId];
|
||||
|
||||
const formatterPerColumn = new Map<DatatableColumn, FieldFormat>();
|
||||
for (const column of table.columns) {
|
||||
formatterPerColumn.set(column, formatFactory(column.meta.params));
|
||||
}
|
||||
|
||||
// what if row values are not primitive? That is the case of, for instance, Ranges
|
||||
// remaps them to their serialized version with the formatHint metadata
|
||||
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
|
||||
const tableConverted: Datatable = {
|
||||
...table,
|
||||
rows: table.rows.map((row: DatatableRow) => {
|
||||
const newRow = { ...row };
|
||||
for (const column of table.columns) {
|
||||
const record = newRow[column.id];
|
||||
if (
|
||||
record != null &&
|
||||
// pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level
|
||||
(!isPrimitive(record) || (column.id === xAccessor && xScaleType === 'ordinal'))
|
||||
) {
|
||||
newRow[column.id] = formatterPerColumn.get(column)!.convert(record);
|
||||
}
|
||||
}
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
|
||||
// save the id of the layer with the custom table
|
||||
table.columns.reduce<Record<string, boolean>>(
|
||||
(alreadyFormatted: Record<string, boolean>, { id }) => {
|
||||
if (alreadyFormatted[id]) {
|
||||
return alreadyFormatted;
|
||||
}
|
||||
alreadyFormatted[id] = table.rows.some(
|
||||
(row, i) => row[id] !== tableConverted.rows[i][id]
|
||||
);
|
||||
return alreadyFormatted;
|
||||
},
|
||||
layersAlreadyFormatted
|
||||
);
|
||||
|
||||
const isStacked = seriesType.includes('stacked');
|
||||
const isPercentage = seriesType.includes('percentage');
|
||||
const isBarChart = seriesType.includes('bar');
|
||||
const enableHistogramMode =
|
||||
isHistogram &&
|
||||
(isStacked || !splitAccessor) &&
|
||||
(isStacked || !isBarChart || !chartHasMoreThanOneBarSeries);
|
||||
|
||||
// For date histogram chart type, we're getting the rows that represent intervals without data.
|
||||
// To not display them in the legend, they need to be filtered out.
|
||||
const rows = tableConverted.rows.filter(
|
||||
(row) =>
|
||||
!(xAccessor && typeof row[xAccessor] === 'undefined') &&
|
||||
!(
|
||||
splitAccessor &&
|
||||
typeof row[splitAccessor] === 'undefined' &&
|
||||
typeof row[accessor] === 'undefined'
|
||||
)
|
||||
);
|
||||
|
||||
if (!xAccessor) {
|
||||
rows.forEach((row) => {
|
||||
row.unifiedX = i18n.translate('expressionXY.xyChart.emptyXLabel', {
|
||||
defaultMessage: '(empty)',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
|
||||
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
|
||||
);
|
||||
|
||||
const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params;
|
||||
const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params;
|
||||
const splitFormatter = formatFactory(splitHint);
|
||||
|
||||
const seriesProps: SeriesSpec = {
|
||||
splitSeriesAccessors: splitAccessor ? [splitAccessor] : [],
|
||||
stackAccessors: isStacked ? [xAccessor as string] : [],
|
||||
id: `${splitAccessor}-${accessor}`,
|
||||
xAccessor: xAccessor || 'unifiedX',
|
||||
yAccessors: [accessor],
|
||||
data: rows,
|
||||
xScaleType: xAccessor ? xScaleType : 'ordinal',
|
||||
yScaleType:
|
||||
formatter?.id === 'bytes' && yScaleType === ScaleType.Linear
|
||||
? ScaleType.LinearBinary
|
||||
: yScaleType,
|
||||
color: ({ yAccessor, seriesKeys }) => {
|
||||
const overwriteColor = getSeriesColor(layer, accessor);
|
||||
if (overwriteColor !== null) {
|
||||
return overwriteColor;
|
||||
}
|
||||
const colorAssignment = colorAssignments[palette.name];
|
||||
const seriesLayers: SeriesLayer[] = [
|
||||
{
|
||||
name: splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
|
||||
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
|
||||
rankAtDepth: colorAssignment.getRank(
|
||||
layer,
|
||||
String(seriesKeys[0]),
|
||||
String(yAccessor)
|
||||
),
|
||||
},
|
||||
];
|
||||
return paletteService.get(palette.name).getCategoricalColor(
|
||||
seriesLayers,
|
||||
{
|
||||
maxDepth: 1,
|
||||
behindText: false,
|
||||
totalSeries: colorAssignment.totalSeriesCount,
|
||||
syncColors,
|
||||
},
|
||||
palette.params
|
||||
);
|
||||
},
|
||||
groupId: yAxis?.groupId,
|
||||
enableHistogramMode,
|
||||
stackMode: isPercentage ? StackMode.Percentage : undefined,
|
||||
timeZone,
|
||||
areaSeriesStyle: {
|
||||
point: {
|
||||
visible: !xAccessor,
|
||||
radius: xAccessor && !emphasizeFitting ? 5 : 0,
|
||||
},
|
||||
...(args.fillOpacity && { area: { opacity: args.fillOpacity } }),
|
||||
...(emphasizeFitting && {
|
||||
fit: {
|
||||
area: {
|
||||
opacity: args.fillOpacity || 0.5,
|
||||
},
|
||||
line: {
|
||||
visible: true,
|
||||
stroke: ColorVariant.Series,
|
||||
opacity: 1,
|
||||
dash: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
lineSeriesStyle: {
|
||||
point: {
|
||||
visible: !xAccessor,
|
||||
radius: xAccessor && !emphasizeFitting ? 5 : 0,
|
||||
},
|
||||
...(emphasizeFitting && {
|
||||
fit: {
|
||||
line: {
|
||||
visible: true,
|
||||
stroke: ColorVariant.Series,
|
||||
opacity: 1,
|
||||
dash: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
name(d) {
|
||||
// For multiple y series, the name of the operation is used on each, either:
|
||||
// * Key - Y name
|
||||
// * Formatted value - Y name
|
||||
if (accessors.length > 1) {
|
||||
const result = d.seriesKeys
|
||||
.map((key: string | number, i) => {
|
||||
if (
|
||||
i === 0 &&
|
||||
splitHint &&
|
||||
splitAccessor &&
|
||||
!layersAlreadyFormatted[splitAccessor]
|
||||
) {
|
||||
return splitFormatter.convert(key);
|
||||
}
|
||||
return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? '';
|
||||
})
|
||||
.join(' - ');
|
||||
return result;
|
||||
}
|
||||
|
||||
// For formatted split series, format the key
|
||||
// This handles splitting by dates, for example
|
||||
if (splitHint) {
|
||||
if (splitAccessor && layersAlreadyFormatted[splitAccessor]) {
|
||||
return d.seriesKeys[0];
|
||||
}
|
||||
return splitFormatter.convert(d.seriesKeys[0]);
|
||||
}
|
||||
// This handles both split and single-y cases:
|
||||
// * If split series without formatting, show the value literally
|
||||
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
|
||||
return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? '';
|
||||
},
|
||||
};
|
||||
|
||||
const index = `${layerIndex}-${accessorIndex}`;
|
||||
|
||||
const curveType = args.curveType ? CurveType[args.curveType] : undefined;
|
||||
|
||||
switch (seriesType) {
|
||||
case 'line':
|
||||
return (
|
||||
<LineSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
{dataLayers.length && (
|
||||
<DataLayers
|
||||
layers={dataLayers}
|
||||
endValue={endValue}
|
||||
timeZone={timeZone}
|
||||
curveType={args.curveType}
|
||||
syncColors={syncColors}
|
||||
valueLabels={valueLabels}
|
||||
fillOpacity={args.fillOpacity}
|
||||
formatFactory={formatFactory}
|
||||
paletteService={paletteService}
|
||||
fittingFunction={fittingFunction}
|
||||
emphasizeFitting={emphasizeFitting}
|
||||
yAxesConfiguration={yAxesConfiguration}
|
||||
shouldShowValueLabels={shouldShowValueLabels}
|
||||
formattedDatatables={formattedDatatables}
|
||||
chartHasMoreThanOneBarSeries={chartHasMoreThanOneBarSeries}
|
||||
/>
|
||||
);
|
||||
case 'bar':
|
||||
case 'bar_stacked':
|
||||
case 'bar_percentage_stacked':
|
||||
case 'bar_horizontal':
|
||||
case 'bar_horizontal_stacked':
|
||||
case 'bar_horizontal_percentage_stacked':
|
||||
const valueLabelsSettings = {
|
||||
displayValueSettings: {
|
||||
// This format double fixes two issues in elastic-chart
|
||||
// * when rotating the chart, the formatter is not correctly picked
|
||||
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
|
||||
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
|
||||
showValueLabel: shouldShowValueLabels && valueLabels !== 'hide',
|
||||
isValueContainedInElement: false,
|
||||
isAlternatingValueLabel: false,
|
||||
overflowConstraints: [
|
||||
LabelOverflowConstraint.ChartEdges,
|
||||
LabelOverflowConstraint.BarGeometry,
|
||||
],
|
||||
},
|
||||
};
|
||||
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
|
||||
case 'area_stacked':
|
||||
case 'area_percentage_stacked':
|
||||
return (
|
||||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
/>
|
||||
);
|
||||
case 'area':
|
||||
return (
|
||||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return assertNever(seriesType);
|
||||
}
|
||||
})
|
||||
)}
|
||||
{referenceLineLayers.length ? (
|
||||
<ReferenceLineAnnotations
|
||||
layers={referenceLineLayers}
|
||||
data={data}
|
||||
formatters={{
|
||||
left: yAxesMap.left?.formatter,
|
||||
right: yAxesMap.right?.formatter,
|
||||
|
@ -946,7 +659,3 @@ export function XYChart({
|
|||
</Chart>
|
||||
);
|
||||
}
|
||||
|
||||
function assertNever(x: never): never {
|
||||
throw new Error('Unexpected series type: ' + x);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import { ExpressionRenderDefinition } from '@kbn/expressions-plugin';
|
|||
import { FormatFactory } from '@kbn/field-formats-plugin/common';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { XYChartProps } from '../../common';
|
||||
import { calculateMinInterval } from '../helpers/interval';
|
||||
import type { BrushEvent, FilterEvent } from '../types';
|
||||
|
||||
export type GetStartDepsFn = () => Promise<{
|
||||
|
@ -56,7 +55,10 @@ export const getXyChartRenderer = ({
|
|||
};
|
||||
const deps = await getStartDeps();
|
||||
|
||||
const { XYChartReportable } = await import('../components/xy_chart');
|
||||
const [{ XYChartReportable }, { calculateMinInterval }] = await Promise.all([
|
||||
import('../components/xy_chart'),
|
||||
import('../helpers/interval'),
|
||||
]);
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={deps.kibanaTheme.theme$}>
|
||||
|
|
|
@ -9,19 +9,32 @@ import React from 'react';
|
|||
import { Position } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui';
|
||||
import classnames from 'classnames';
|
||||
import type { IconPosition, YAxisMode, YConfig } from '../../common/types';
|
||||
import type {
|
||||
IconPosition,
|
||||
YAxisMode,
|
||||
ExtendedYConfig,
|
||||
CollectiveConfig,
|
||||
} from '../../common/types';
|
||||
import { getBaseIconPlacement } from '../components';
|
||||
import { hasIcon } from './icon';
|
||||
import { annotationsIconSet } from './annotations_icon_set';
|
||||
import { hasIcon, iconSet } from './icon';
|
||||
|
||||
export const LINES_MARKER_SIZE = 20;
|
||||
|
||||
// Note: it does not take into consideration whether the reference line is in view or not
|
||||
type PartialExtendedYConfig = Pick<
|
||||
ExtendedYConfig,
|
||||
'axisMode' | 'icon' | 'iconPosition' | 'textVisibility'
|
||||
>;
|
||||
|
||||
type PartialCollectiveConfig = Pick<CollectiveConfig, 'axisMode' | 'icon' | 'textVisibility'>;
|
||||
|
||||
const isExtendedYConfig = (
|
||||
config: PartialExtendedYConfig | PartialCollectiveConfig | undefined
|
||||
): config is PartialExtendedYConfig =>
|
||||
(config as PartialExtendedYConfig)?.iconPosition ? true : false;
|
||||
|
||||
// Note: it does not take into consideration whether the reference line is in view or not
|
||||
export const getLinesCausedPaddings = (
|
||||
visualConfigs: Array<
|
||||
Pick<YConfig, 'axisMode' | 'icon' | 'iconPosition' | 'textVisibility'> | undefined
|
||||
>,
|
||||
visualConfigs: Array<PartialExtendedYConfig | PartialCollectiveConfig | undefined>,
|
||||
axesMap: Record<'left' | 'right', unknown>
|
||||
) => {
|
||||
// collect all paddings for the 4 axis: if any text is detected double it.
|
||||
|
@ -31,7 +44,9 @@ export const getLinesCausedPaddings = (
|
|||
if (!config) {
|
||||
return;
|
||||
}
|
||||
const { axisMode, icon, iconPosition, textVisibility } = config;
|
||||
const { axisMode, icon, textVisibility } = config;
|
||||
const iconPosition = isExtendedYConfig(config) ? config.iconPosition : undefined;
|
||||
|
||||
if (axisMode && (hasIcon(icon) || textVisibility)) {
|
||||
const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode);
|
||||
paddings[placement] = Math.max(
|
||||
|
@ -48,6 +63,7 @@ export const getLinesCausedPaddings = (
|
|||
paddings[placement] = LINES_MARKER_SIZE;
|
||||
}
|
||||
});
|
||||
|
||||
return paddings;
|
||||
};
|
||||
|
||||
|
@ -138,7 +154,7 @@ export const AnnotationIcon = ({
|
|||
if (isNumericalString(type)) {
|
||||
return <NumberIcon number={Number(type)} />;
|
||||
}
|
||||
const iconConfig = annotationsIconSet.find((i) => i.value === type);
|
||||
const iconConfig = iconSet.find((i) => i.value === type);
|
||||
if (!iconConfig) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,101 +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 { i18n } from '@kbn/i18n';
|
||||
import { TriangleIcon, CircleIcon } from '../icons';
|
||||
|
||||
export const annotationsIconSet = [
|
||||
{
|
||||
value: 'asterisk',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.asteriskIconLabel', {
|
||||
defaultMessage: 'Asterisk',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'alert',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.alertIconLabel', {
|
||||
defaultMessage: 'Alert',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'bell',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.bellIconLabel', {
|
||||
defaultMessage: 'Bell',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'bolt',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.boltIconLabel', {
|
||||
defaultMessage: 'Bolt',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'bug',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.bugIconLabel', {
|
||||
defaultMessage: 'Bug',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'circle',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.circleIconLabel', {
|
||||
defaultMessage: 'Circle',
|
||||
}),
|
||||
icon: CircleIcon,
|
||||
canFill: true,
|
||||
},
|
||||
|
||||
{
|
||||
value: 'editorComment',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.commentIconLabel', {
|
||||
defaultMessage: 'Comment',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'flag',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.flagIconLabel', {
|
||||
defaultMessage: 'Flag',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'heart',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.heartLabel', {
|
||||
defaultMessage: 'Heart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'mapMarker',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.mapMarkerLabel', {
|
||||
defaultMessage: 'Map Marker',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'pinFilled',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.mapPinLabel', {
|
||||
defaultMessage: 'Map Pin',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'starEmpty',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.starLabel', { defaultMessage: 'Star' }),
|
||||
},
|
||||
{
|
||||
value: 'tag',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.tagIconLabel', {
|
||||
defaultMessage: 'Tag',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'triangle',
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.triangleIconLabel', {
|
||||
defaultMessage: 'Triangle',
|
||||
}),
|
||||
icon: TriangleIcon,
|
||||
shouldRotate: true,
|
||||
canFill: true,
|
||||
},
|
||||
];
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DataLayerConfigResult } from '../../common';
|
||||
import { DataLayerConfig } from '../../common';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { getAxesConfiguration } from './axes_configuration';
|
||||
|
@ -220,9 +220,9 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const sampleLayer: DataLayerConfigResult = {
|
||||
type: 'dataLayer',
|
||||
const sampleLayer: DataLayerConfig = {
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'c',
|
||||
|
@ -233,11 +233,12 @@ describe('axes_configuration', () => {
|
|||
yScaleType: 'linear',
|
||||
isHistogram: false,
|
||||
palette: { type: 'palette', name: 'default' },
|
||||
table: tables.first,
|
||||
};
|
||||
|
||||
it('should map auto series to left axis', () => {
|
||||
const formatFactory = jest.fn();
|
||||
const groups = getAxesConfiguration([sampleLayer], false, tables, formatFactory);
|
||||
const groups = getAxesConfiguration([sampleLayer], false, formatFactory);
|
||||
expect(groups.length).toEqual(1);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
|
||||
|
@ -247,7 +248,7 @@ describe('axes_configuration', () => {
|
|||
it('should map auto series to right axis if formatters do not match', () => {
|
||||
const formatFactory = jest.fn();
|
||||
const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] };
|
||||
const groups = getAxesConfiguration([twoSeriesLayer], false, tables, formatFactory);
|
||||
const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory);
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[1].position).toEqual('right');
|
||||
|
@ -261,7 +262,7 @@ describe('axes_configuration', () => {
|
|||
...sampleLayer,
|
||||
accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'],
|
||||
};
|
||||
const groups = getAxesConfiguration([threeSeriesLayer], false, tables, formatFactory);
|
||||
const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory);
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[1].position).toEqual('right');
|
||||
|
@ -280,7 +281,6 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
tables,
|
||||
formatFactory
|
||||
);
|
||||
expect(groups.length).toEqual(1);
|
||||
|
@ -300,7 +300,6 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
tables,
|
||||
formatFactory
|
||||
);
|
||||
expect(groups.length).toEqual(2);
|
||||
|
@ -324,7 +323,6 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
tables,
|
||||
formatFactory
|
||||
);
|
||||
expect(formatFactory).toHaveBeenCalledTimes(2);
|
||||
|
|
|
@ -6,22 +6,25 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { FormatFactory } from '../types';
|
||||
import { AxisExtentConfig, DataLayerConfigResult } from '../../common';
|
||||
import { AxisExtentConfig, CommonXYDataLayerConfig, ExtendedYConfig, YConfig } from '../../common';
|
||||
import { isDataLayer } from './visualization';
|
||||
|
||||
interface FormattedMetric {
|
||||
export interface Series {
|
||||
layer: string;
|
||||
accessor: string;
|
||||
}
|
||||
|
||||
interface FormattedMetric extends Series {
|
||||
fieldFormat: SerializedFieldFormat;
|
||||
}
|
||||
|
||||
export type GroupsConfiguration = Array<{
|
||||
groupId: string;
|
||||
groupId: 'left' | 'right';
|
||||
position: 'left' | 'right' | 'bottom' | 'top';
|
||||
formatter?: IFieldFormat;
|
||||
series: Array<{ layer: string; accessor: string }>;
|
||||
series: Series[];
|
||||
}>;
|
||||
|
||||
export function isFormatterCompatible(
|
||||
|
@ -31,10 +34,7 @@ export function isFormatterCompatible(
|
|||
return formatter1.id === formatter2.id;
|
||||
}
|
||||
|
||||
export function groupAxesByType(
|
||||
layers: DataLayerConfigResult[],
|
||||
tables?: Record<string, Datatable>
|
||||
) {
|
||||
export function groupAxesByType(layers: CommonXYDataLayerConfig[]) {
|
||||
const series: {
|
||||
auto: FormattedMetric[];
|
||||
left: FormattedMetric[];
|
||||
|
@ -47,15 +47,19 @@ export function groupAxesByType(
|
|||
bottom: [],
|
||||
};
|
||||
|
||||
layers?.forEach((layer) => {
|
||||
const table = tables?.[layer.layerId];
|
||||
layers.forEach((layer) => {
|
||||
const { table } = layer;
|
||||
layer.accessors.forEach((accessor) => {
|
||||
const yConfig: Array<YConfig | ExtendedYConfig> | undefined = layer.yConfig;
|
||||
const mode =
|
||||
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode ||
|
||||
'auto';
|
||||
let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor)
|
||||
yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto';
|
||||
let formatter: SerializedFieldFormat = table.columns?.find((column) => column.id === accessor)
|
||||
?.meta?.params || { id: 'number' };
|
||||
if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') {
|
||||
if (
|
||||
isDataLayer(layer) &&
|
||||
layer.seriesType.includes('percentage') &&
|
||||
formatter.id !== 'percent'
|
||||
) {
|
||||
formatter = {
|
||||
id: 'percent',
|
||||
params: {
|
||||
|
@ -71,10 +75,12 @@ export function groupAxesByType(
|
|||
});
|
||||
});
|
||||
|
||||
const tablesExist = layers.filter(({ table }) => Boolean(table)).length > 0;
|
||||
|
||||
series.auto.forEach((currentSeries) => {
|
||||
if (
|
||||
series.left.length === 0 ||
|
||||
(tables &&
|
||||
(tablesExist &&
|
||||
series.left.every((leftSeries) =>
|
||||
isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat)
|
||||
))
|
||||
|
@ -82,7 +88,7 @@ export function groupAxesByType(
|
|||
series.left.push(currentSeries);
|
||||
} else if (
|
||||
series.right.length === 0 ||
|
||||
(tables &&
|
||||
(tablesExist &&
|
||||
series.left.every((leftSeries) =>
|
||||
isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat)
|
||||
))
|
||||
|
@ -98,12 +104,11 @@ export function groupAxesByType(
|
|||
}
|
||||
|
||||
export function getAxesConfiguration(
|
||||
layers: DataLayerConfigResult[],
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
shouldRotate: boolean,
|
||||
tables?: Record<string, Datatable>,
|
||||
formatFactory?: FormatFactory
|
||||
): GroupsConfiguration {
|
||||
const series = groupAxesByType(layers, tables);
|
||||
const series = groupAxesByType(layers);
|
||||
|
||||
const axisGroups: GroupsConfiguration = [];
|
||||
|
||||
|
|
|
@ -7,41 +7,13 @@
|
|||
*/
|
||||
|
||||
import { getColorAssignments } from './color_assignment';
|
||||
import type { DataLayerConfigResult, LensMultiTable } from '../../common';
|
||||
import type { DataLayerConfig } from '../../common';
|
||||
import type { FormatFactory } from '../types';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import { Datatable } from '@kbn/expressions-plugin';
|
||||
|
||||
describe('color_assignment', () => {
|
||||
const layers: DataLayerConfigResult[] = [
|
||||
{
|
||||
type: 'dataLayer',
|
||||
yScaleType: 'linear',
|
||||
xScaleType: 'linear',
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
palette: { type: 'palette', name: 'palette1' },
|
||||
layerId: '1',
|
||||
layerType: LayerTypes.DATA,
|
||||
splitAccessor: 'split1',
|
||||
accessors: ['y1', 'y2'],
|
||||
},
|
||||
{
|
||||
type: 'dataLayer',
|
||||
yScaleType: 'linear',
|
||||
xScaleType: 'linear',
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
palette: { type: 'palette', name: 'palette2' },
|
||||
layerId: '2',
|
||||
layerType: LayerTypes.DATA,
|
||||
splitAccessor: 'split2',
|
||||
accessors: ['y3', 'y4'],
|
||||
},
|
||||
];
|
||||
|
||||
const data: LensMultiTable = {
|
||||
type: 'lens_multitable',
|
||||
tables: {
|
||||
const tables: Record<string, Datatable> = {
|
||||
'1': {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -74,9 +46,37 @@ describe('color_assignment', () => {
|
|||
{ split2: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const layers: DataLayerConfig[] = [
|
||||
{
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
yScaleType: 'linear',
|
||||
xScaleType: 'linear',
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
palette: { type: 'palette', name: 'palette1' },
|
||||
layerType: LayerTypes.DATA,
|
||||
splitAccessor: 'split1',
|
||||
accessors: ['y1', 'y2'],
|
||||
table: tables['1'],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
type: 'dataLayer',
|
||||
yScaleType: 'linear',
|
||||
xScaleType: 'linear',
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
palette: { type: 'palette', name: 'palette2' },
|
||||
layerType: LayerTypes.DATA,
|
||||
splitAccessor: 'split2',
|
||||
accessors: ['y3', 'y4'],
|
||||
table: tables['2'],
|
||||
},
|
||||
];
|
||||
|
||||
const formatFactory = (() =>
|
||||
({
|
||||
convert(x: unknown) {
|
||||
|
@ -86,7 +86,7 @@ describe('color_assignment', () => {
|
|||
|
||||
describe('totalSeriesCount', () => {
|
||||
it('should calculate total number of series per palette', () => {
|
||||
const assignments = getColorAssignments(layers, data, formatFactory);
|
||||
const assignments = getColorAssignments(layers, formatFactory);
|
||||
// two y accessors, with 3 splitted series
|
||||
expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3);
|
||||
expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3);
|
||||
|
@ -95,7 +95,6 @@ describe('color_assignment', () => {
|
|||
it('should calculate total number of series spanning multible layers', () => {
|
||||
const assignments = getColorAssignments(
|
||||
[layers[0], { ...layers[1], palette: layers[0].palette }],
|
||||
data,
|
||||
formatFactory
|
||||
);
|
||||
// two y accessors, with 3 splitted series, two times
|
||||
|
@ -106,7 +105,6 @@ describe('color_assignment', () => {
|
|||
it('should calculate total number of series for non split series', () => {
|
||||
const assignments = getColorAssignments(
|
||||
[layers[0], { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }],
|
||||
data,
|
||||
formatFactory
|
||||
);
|
||||
// two y accessors, with 3 splitted series for the first layer, 2 non splitted y accessors for the second layer
|
||||
|
@ -117,15 +115,16 @@ describe('color_assignment', () => {
|
|||
it('should format non-primitive values and count them correctly', () => {
|
||||
const complexObject = { aProp: 123 };
|
||||
const formatMock = jest.fn((x) => 'formatted');
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
const newLayers = [
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': { ...data.tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] },
|
||||
},
|
||||
...layers[0],
|
||||
table: { ...tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] },
|
||||
},
|
||||
layers[1],
|
||||
];
|
||||
|
||||
const assignments = getColorAssignments(
|
||||
newLayers,
|
||||
(() =>
|
||||
({
|
||||
convert: formatMock,
|
||||
|
@ -137,26 +136,18 @@ describe('color_assignment', () => {
|
|||
});
|
||||
|
||||
it('should handle missing tables', () => {
|
||||
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory);
|
||||
const assignments = getColorAssignments(
|
||||
layers.map((l) => ({ ...l, table: {} as any })),
|
||||
formatFactory
|
||||
);
|
||||
// if there is no data, just assume a single split
|
||||
expect(assignments.palette1.totalSeriesCount).toEqual(2);
|
||||
});
|
||||
|
||||
it('should handle missing columns', () => {
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': {
|
||||
...data.tables['1'],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
formatFactory
|
||||
);
|
||||
const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]];
|
||||
const assignments = getColorAssignments(newLayers, formatFactory);
|
||||
|
||||
// if the split column is missing, just assume a single split
|
||||
expect(assignments.palette1.totalSeriesCount).toEqual(2);
|
||||
});
|
||||
|
@ -164,7 +155,7 @@ describe('color_assignment', () => {
|
|||
|
||||
describe('getRank', () => {
|
||||
it('should return the correct rank for a series key', () => {
|
||||
const assignments = getColorAssignments(layers, data, formatFactory);
|
||||
const assignments = getColorAssignments(layers, formatFactory);
|
||||
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
|
||||
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(3);
|
||||
// 1 series in front of 1/y4 - 1/y3
|
||||
|
@ -173,7 +164,7 @@ describe('color_assignment', () => {
|
|||
|
||||
it('should return the correct rank for a series key spanning multiple layers', () => {
|
||||
const newLayers = [layers[0], { ...layers[1], palette: layers[0].palette }];
|
||||
const assignments = getColorAssignments(newLayers, data, formatFactory);
|
||||
const assignments = getColorAssignments(newLayers, formatFactory);
|
||||
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
|
||||
expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3);
|
||||
// 2 series in front for the current layer (1/y3, 1/y4), plus all 6 series from the first layer
|
||||
|
@ -185,7 +176,7 @@ describe('color_assignment', () => {
|
|||
layers[0],
|
||||
{ ...layers[1], palette: layers[0].palette, splitAccessor: undefined },
|
||||
];
|
||||
const assignments = getColorAssignments(newLayers, data, formatFactory);
|
||||
const assignments = getColorAssignments(newLayers, formatFactory);
|
||||
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
|
||||
expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3);
|
||||
// 1 series in front for the current layer (y3), plus all 6 series from the first layer
|
||||
|
@ -193,15 +184,16 @@ describe('color_assignment', () => {
|
|||
});
|
||||
|
||||
it('should return the correct rank for a series with a non-primitive value', () => {
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
const newLayers = [
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': { ...data.tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] },
|
||||
},
|
||||
...layers[0],
|
||||
table: { ...tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] },
|
||||
},
|
||||
layers[1],
|
||||
];
|
||||
|
||||
const assignments = getColorAssignments(
|
||||
newLayers,
|
||||
(() =>
|
||||
({
|
||||
convert: () => 'formatted',
|
||||
|
@ -212,26 +204,19 @@ describe('color_assignment', () => {
|
|||
});
|
||||
|
||||
it('should handle missing tables', () => {
|
||||
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory);
|
||||
const assignments = getColorAssignments(
|
||||
layers.map((l) => ({ ...l, table: {} as any })),
|
||||
formatFactory
|
||||
);
|
||||
// if there is no data, assume it is the first splitted series. One series in front - 0/y1
|
||||
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should handle missing columns', () => {
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': {
|
||||
...data.tables['1'],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
formatFactory
|
||||
);
|
||||
const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]];
|
||||
|
||||
const assignments = getColorAssignments(newLayers, formatFactory);
|
||||
|
||||
// if the split column is missing, assume it is the first splitted series. One series in front - 0/y1
|
||||
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
|
||||
});
|
||||
|
|
|
@ -8,10 +8,9 @@
|
|||
|
||||
import { uniq, mapValues } from 'lodash';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import type { Datatable } from '@kbn/expressions-plugin';
|
||||
import { FormatFactory } from '../types';
|
||||
import { isDataLayer } from './visualization';
|
||||
import { DataLayerConfigResult, XYLayerConfigResult } from '../../common';
|
||||
import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common';
|
||||
|
||||
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
|
||||
|
||||
|
@ -21,20 +20,21 @@ export type ColorAssignments = Record<
|
|||
string,
|
||||
{
|
||||
totalSeriesCount: number;
|
||||
getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string): number;
|
||||
getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string): number;
|
||||
}
|
||||
>;
|
||||
|
||||
export function getColorAssignments(
|
||||
layers: XYLayerConfigResult[],
|
||||
data: { tables: Record<string, Datatable> },
|
||||
layers: CommonXYLayerConfig[],
|
||||
formatFactory: FormatFactory
|
||||
): ColorAssignments {
|
||||
const layersPerPalette: Record<string, DataLayerConfigResult[]> = {};
|
||||
const layersPerPalette: Record<string, CommonXYDataLayerConfig[]> = {};
|
||||
|
||||
layers.forEach((layer) => {
|
||||
if (!isDataLayer(layer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
layers
|
||||
.filter((layer): layer is DataLayerConfigResult => isDataLayer(layer))
|
||||
.forEach((layer) => {
|
||||
const palette = layer.palette?.name || 'default';
|
||||
if (!layersPerPalette[palette]) {
|
||||
layersPerPalette[palette] = [];
|
||||
|
@ -43,18 +43,18 @@ export function getColorAssignments(
|
|||
});
|
||||
|
||||
return mapValues(layersPerPalette, (paletteLayers) => {
|
||||
const seriesPerLayer = paletteLayers.map((layer, layerIndex) => {
|
||||
const seriesPerLayer = paletteLayers.map((layer) => {
|
||||
if (!layer.splitAccessor) {
|
||||
return { numberOfSeries: layer.accessors.length, splits: [] };
|
||||
}
|
||||
const splitAccessor = layer.splitAccessor;
|
||||
const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor);
|
||||
const column = layer.table.columns?.find(({ id }) => id === splitAccessor);
|
||||
const columnFormatter = column && formatFactory(column.meta.params);
|
||||
const splits =
|
||||
!column || !data.tables[layer.layerId]
|
||||
!column || !layer.table
|
||||
? []
|
||||
: uniq(
|
||||
data.tables[layer.layerId].rows.map((row) => {
|
||||
layer.table.rows.map((row) => {
|
||||
let value = row[splitAccessor];
|
||||
if (value && !isPrimitive(value)) {
|
||||
value = columnFormatter?.convert(value) ?? value;
|
||||
|
@ -72,8 +72,10 @@ export function getColorAssignments(
|
|||
);
|
||||
return {
|
||||
totalSeriesCount,
|
||||
getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string) {
|
||||
const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId);
|
||||
getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string) {
|
||||
const layerIndex = paletteLayers.findIndex(
|
||||
(layer) => sortedLayer.layerId === layer.layerId
|
||||
);
|
||||
const currentSeriesPerLayer = seriesPerLayer[layerIndex];
|
||||
const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey);
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* 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 {
|
||||
AreaSeriesProps,
|
||||
BarSeriesProps,
|
||||
ColorVariant,
|
||||
LineSeriesProps,
|
||||
ScaleType,
|
||||
SeriesName,
|
||||
StackMode,
|
||||
XYChartSeriesIdentifier,
|
||||
} from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
FieldFormat,
|
||||
FieldFormatParams,
|
||||
IFieldFormat,
|
||||
SerializedFieldFormat,
|
||||
} from '@kbn/field-formats-plugin/common';
|
||||
import { Datatable } from '@kbn/expressions-plugin';
|
||||
import { PaletteRegistry, SeriesLayer } from '@kbn/coloring';
|
||||
import { CommonXYDataLayerConfig, XScaleType } from '../../common';
|
||||
import { FormatFactory } from '../types';
|
||||
import { getSeriesColor } from './state';
|
||||
import { ColorAssignments } from './color_assignment';
|
||||
import { GroupsConfiguration } from './axes_configuration';
|
||||
|
||||
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
|
||||
|
||||
type GetSeriesPropsFn = (config: {
|
||||
layer: CommonXYDataLayerConfig;
|
||||
accessor: string;
|
||||
chartHasMoreThanOneBarSeries?: boolean;
|
||||
formatFactory: FormatFactory;
|
||||
colorAssignments: ColorAssignments;
|
||||
columnToLabelMap: Record<string, string>;
|
||||
paletteService: PaletteRegistry;
|
||||
syncColors?: boolean;
|
||||
yAxis?: GroupsConfiguration[number];
|
||||
timeZone?: string;
|
||||
emphasizeFitting?: boolean;
|
||||
fillOpacity?: number;
|
||||
formattedDatatableInfo: DatatableWithFormatInfo;
|
||||
}) => SeriesSpec;
|
||||
|
||||
type GetSeriesNameFn = (
|
||||
data: XYChartSeriesIdentifier,
|
||||
config: {
|
||||
layer: CommonXYDataLayerConfig;
|
||||
splitHint: SerializedFieldFormat<FieldFormatParams> | undefined;
|
||||
splitFormatter: FieldFormat;
|
||||
alreadyFormattedColumns: Record<string, boolean>;
|
||||
columnToLabelMap: Record<string, string>;
|
||||
}
|
||||
) => SeriesName;
|
||||
|
||||
type GetColorFn = (
|
||||
seriesIdentifier: XYChartSeriesIdentifier,
|
||||
config: {
|
||||
layer: CommonXYDataLayerConfig;
|
||||
accessor: string;
|
||||
colorAssignments: ColorAssignments;
|
||||
columnToLabelMap: Record<string, string>;
|
||||
paletteService: PaletteRegistry;
|
||||
syncColors?: boolean;
|
||||
}
|
||||
) => string | null;
|
||||
|
||||
export interface DatatableWithFormatInfo {
|
||||
table: Datatable;
|
||||
formattedColumns: Record<string, true>;
|
||||
}
|
||||
|
||||
export type DatatablesWithFormatInfo = Record<string, DatatableWithFormatInfo>;
|
||||
|
||||
export type FormattedDatatables = Record<string, Datatable>;
|
||||
|
||||
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
|
||||
|
||||
export const getFormattedRow = (
|
||||
row: Datatable['rows'][number],
|
||||
columns: Datatable['columns'],
|
||||
columnsFormatters: Record<string, IFieldFormat>,
|
||||
xAccessor: string | undefined,
|
||||
xScaleType: XScaleType
|
||||
): { row: Datatable['rows'][number]; formattedColumns: Record<string, true> } =>
|
||||
columns.reduce(
|
||||
(formattedInfo, { id }) => {
|
||||
const record = formattedInfo.row[id];
|
||||
if (
|
||||
record != null &&
|
||||
// pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level
|
||||
(!isPrimitive(record) || (id === xAccessor && xScaleType === 'ordinal'))
|
||||
) {
|
||||
return {
|
||||
row: { ...formattedInfo.row, [id]: columnsFormatters[id]!.convert(record) },
|
||||
formattedColumns: { ...formattedInfo.formattedColumns, [id]: true },
|
||||
};
|
||||
}
|
||||
return formattedInfo;
|
||||
},
|
||||
{ row, formattedColumns: {} }
|
||||
);
|
||||
|
||||
export const getFormattedTable = (
|
||||
table: Datatable,
|
||||
formatFactory: FormatFactory,
|
||||
xAccessor: string | undefined,
|
||||
xScaleType: XScaleType
|
||||
): { table: Datatable; formattedColumns: Record<string, true> } => {
|
||||
const columnsFormatters = table.columns.reduce<Record<string, IFieldFormat>>(
|
||||
(formatters, { id, meta }) => ({ ...formatters, [id]: formatFactory(meta.params) }),
|
||||
{}
|
||||
);
|
||||
|
||||
const formattedTableInfo = table.rows.reduce<{
|
||||
rows: Datatable['rows'];
|
||||
formattedColumns: Record<string, true>;
|
||||
}>(
|
||||
({ rows: formattedRows, formattedColumns }, row) => {
|
||||
const formattedRowInfo = getFormattedRow(
|
||||
row,
|
||||
table.columns,
|
||||
columnsFormatters,
|
||||
xAccessor,
|
||||
xScaleType
|
||||
);
|
||||
return {
|
||||
rows: [...formattedRows, formattedRowInfo.row],
|
||||
formattedColumns: { ...formattedColumns, ...formattedRowInfo.formattedColumns },
|
||||
};
|
||||
},
|
||||
{
|
||||
rows: [],
|
||||
formattedColumns: {},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
table: { ...table, rows: formattedTableInfo.rows },
|
||||
formattedColumns: formattedTableInfo.formattedColumns,
|
||||
};
|
||||
};
|
||||
|
||||
export const getFormattedTablesByLayers = (
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
formatFactory: FormatFactory
|
||||
): DatatablesWithFormatInfo =>
|
||||
layers.reduce(
|
||||
(formattedDatatables, { layerId, table, xAccessor, xScaleType }) => ({
|
||||
...formattedDatatables,
|
||||
[layerId]: getFormattedTable(table, formatFactory, xAccessor, xScaleType),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const getSeriesName: GetSeriesNameFn = (
|
||||
data,
|
||||
{ layer, splitHint, splitFormatter, alreadyFormattedColumns, columnToLabelMap }
|
||||
) => {
|
||||
// For multiple y series, the name of the operation is used on each, either:
|
||||
// * Key - Y name
|
||||
// * Formatted value - Y name
|
||||
if (layer.splitAccessor && layer.accessors.length > 1) {
|
||||
const formatted = alreadyFormattedColumns[layer.splitAccessor];
|
||||
const result = data.seriesKeys
|
||||
.map((key: string | number, i) => {
|
||||
if (i === 0 && splitHint && layer.splitAccessor && !formatted) {
|
||||
return splitFormatter.convert(key);
|
||||
}
|
||||
return layer.splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? null;
|
||||
})
|
||||
.join(' - ');
|
||||
return result;
|
||||
}
|
||||
|
||||
// For formatted split series, format the key
|
||||
// This handles splitting by dates, for example
|
||||
if (splitHint) {
|
||||
if (layer.splitAccessor && alreadyFormattedColumns[layer.splitAccessor]) {
|
||||
return data.seriesKeys[0];
|
||||
}
|
||||
return splitFormatter.convert(data.seriesKeys[0]);
|
||||
}
|
||||
// This handles both split and single-y cases:
|
||||
// * If split series without formatting, show the value literally
|
||||
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
|
||||
return layer.splitAccessor ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null;
|
||||
};
|
||||
|
||||
const getPointConfig = (xAccessor?: string, emphasizeFitting?: boolean) => ({
|
||||
visible: !xAccessor,
|
||||
radius: xAccessor && !emphasizeFitting ? 5 : 0,
|
||||
});
|
||||
|
||||
const getLineConfig = () => ({ visible: true, stroke: ColorVariant.Series, opacity: 1, dash: [] });
|
||||
|
||||
const getColor: GetColorFn = (
|
||||
{ yAccessor, seriesKeys },
|
||||
{ layer, accessor, colorAssignments, columnToLabelMap, paletteService, syncColors }
|
||||
) => {
|
||||
const overwriteColor = getSeriesColor(layer, accessor);
|
||||
if (overwriteColor !== null) {
|
||||
return overwriteColor;
|
||||
}
|
||||
const colorAssignment = colorAssignments[layer.palette.name];
|
||||
const seriesLayers: SeriesLayer[] = [
|
||||
{
|
||||
name: layer.splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
|
||||
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
|
||||
rankAtDepth: colorAssignment.getRank(layer, String(seriesKeys[0]), String(yAccessor)),
|
||||
},
|
||||
];
|
||||
return paletteService.get(layer.palette.name).getCategoricalColor(
|
||||
seriesLayers,
|
||||
{
|
||||
maxDepth: 1,
|
||||
behindText: false,
|
||||
totalSeries: colorAssignment.totalSeriesCount,
|
||||
syncColors,
|
||||
},
|
||||
layer.palette.params
|
||||
);
|
||||
};
|
||||
|
||||
export const getSeriesProps: GetSeriesPropsFn = ({
|
||||
layer,
|
||||
accessor,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
colorAssignments,
|
||||
formatFactory,
|
||||
columnToLabelMap,
|
||||
paletteService,
|
||||
syncColors,
|
||||
yAxis,
|
||||
timeZone,
|
||||
emphasizeFitting,
|
||||
fillOpacity,
|
||||
formattedDatatableInfo,
|
||||
}): SeriesSpec => {
|
||||
const { table } = layer;
|
||||
const isStacked = layer.seriesType.includes('stacked');
|
||||
const isPercentage = layer.seriesType.includes('percentage');
|
||||
const isBarChart = layer.seriesType.includes('bar');
|
||||
const enableHistogramMode =
|
||||
layer.isHistogram &&
|
||||
(isStacked || !layer.splitAccessor) &&
|
||||
(isStacked || !isBarChart || !chartHasMoreThanOneBarSeries);
|
||||
|
||||
const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params;
|
||||
const splitHint = table?.columns.find((col) => col.id === layer.splitAccessor)?.meta?.params;
|
||||
const splitFormatter = formatFactory(splitHint);
|
||||
|
||||
// what if row values are not primitive? That is the case of, for instance, Ranges
|
||||
// remaps them to their serialized version with the formatHint metadata
|
||||
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
|
||||
const { table: formattedTable, formattedColumns } = formattedDatatableInfo;
|
||||
|
||||
// For date histogram chart type, we're getting the rows that represent intervals without data.
|
||||
// To not display them in the legend, they need to be filtered out.
|
||||
let rows = formattedTable.rows.filter(
|
||||
(row) =>
|
||||
!(layer.xAccessor && typeof row[layer.xAccessor] === 'undefined') &&
|
||||
!(
|
||||
layer.splitAccessor &&
|
||||
typeof row[layer.splitAccessor] === 'undefined' &&
|
||||
typeof row[accessor] === 'undefined'
|
||||
)
|
||||
);
|
||||
|
||||
if (!layer.xAccessor) {
|
||||
rows = rows.map((row) => ({
|
||||
...row,
|
||||
unifiedX: i18n.translate('expressionXY.xyChart.emptyXLabel', {
|
||||
defaultMessage: '(empty)',
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
splitSeriesAccessors: layer.splitAccessor ? [layer.splitAccessor] : [],
|
||||
stackAccessors: isStacked ? [layer.xAccessor as string] : [],
|
||||
id: layer.splitAccessor ? `${layer.splitAccessor}-${accessor}` : `${accessor}`,
|
||||
xAccessor: layer.xAccessor || 'unifiedX',
|
||||
yAccessors: [accessor],
|
||||
data: rows,
|
||||
xScaleType: layer.xAccessor ? layer.xScaleType : 'ordinal',
|
||||
yScaleType:
|
||||
formatter?.id === 'bytes' && layer.yScaleType === ScaleType.Linear
|
||||
? ScaleType.LinearBinary
|
||||
: layer.yScaleType,
|
||||
color: (series) =>
|
||||
getColor(series, {
|
||||
layer,
|
||||
accessor,
|
||||
colorAssignments,
|
||||
columnToLabelMap,
|
||||
paletteService,
|
||||
syncColors,
|
||||
}),
|
||||
groupId: yAxis?.groupId,
|
||||
enableHistogramMode,
|
||||
stackMode: isPercentage ? StackMode.Percentage : undefined,
|
||||
timeZone,
|
||||
areaSeriesStyle: {
|
||||
point: getPointConfig(layer.xAccessor, emphasizeFitting),
|
||||
...(fillOpacity && { area: { opacity: fillOpacity } }),
|
||||
...(emphasizeFitting && {
|
||||
fit: { area: { opacity: fillOpacity || 0.5 }, line: getLineConfig() },
|
||||
}),
|
||||
},
|
||||
lineSeriesStyle: {
|
||||
point: getPointConfig(layer.xAccessor, emphasizeFitting),
|
||||
...(emphasizeFitting && { fit: { line: getLineConfig() } }),
|
||||
},
|
||||
name(d) {
|
||||
return getSeriesName(d, {
|
||||
layer,
|
||||
splitHint,
|
||||
splitFormatter,
|
||||
alreadyFormattedColumns: formattedColumns,
|
||||
columnToLabelMap,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { Fit } from '@elastic/charts';
|
||||
import { EndValue, FittingFunction } from '../../common';
|
||||
import { EndValues } from '../../common/constants';
|
||||
|
||||
export function getFitEnum(fittingFunction?: FittingFunction | EndValue) {
|
||||
if (fittingFunction) {
|
||||
|
@ -17,10 +18,10 @@ export function getFitEnum(fittingFunction?: FittingFunction | EndValue) {
|
|||
}
|
||||
|
||||
export function getEndValue(endValue?: EndValue) {
|
||||
if (endValue === 'Nearest') {
|
||||
if (endValue === EndValues.NEAREST) {
|
||||
return Fit[endValue];
|
||||
}
|
||||
if (endValue === 'Zero') {
|
||||
if (endValue === EndValues.ZERO) {
|
||||
return 0;
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
@ -6,6 +6,107 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TriangleIcon, CircleIcon } from '../icons';
|
||||
import { AvailableReferenceLineIcons } from '../../common/constants';
|
||||
|
||||
export function hasIcon(icon: string | undefined): icon is string {
|
||||
return icon != null && icon !== 'empty';
|
||||
}
|
||||
|
||||
export const iconSet = [
|
||||
{
|
||||
value: AvailableReferenceLineIcons.EMPTY,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.noIconLabel', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.ASTERISK,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.asteriskIconLabel', {
|
||||
defaultMessage: 'Asterisk',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.ALERT,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.alertIconLabel', {
|
||||
defaultMessage: 'Alert',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.BELL,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.bellIconLabel', {
|
||||
defaultMessage: 'Bell',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.BOLT,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.boltIconLabel', {
|
||||
defaultMessage: 'Bolt',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.BUG,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.bugIconLabel', {
|
||||
defaultMessage: 'Bug',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.CIRCLE,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.circleIconLabel', {
|
||||
defaultMessage: 'Circle',
|
||||
}),
|
||||
icon: CircleIcon,
|
||||
canFill: true,
|
||||
},
|
||||
|
||||
{
|
||||
value: AvailableReferenceLineIcons.EDITOR_COMMENT,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.commentIconLabel', {
|
||||
defaultMessage: 'Comment',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.FLAG,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.flagIconLabel', {
|
||||
defaultMessage: 'Flag',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.HEART,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.heartLabel', {
|
||||
defaultMessage: 'Heart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.MAP_MARKER,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.mapMarkerLabel', {
|
||||
defaultMessage: 'Map Marker',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.PIN_FILLED,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.mapPinLabel', {
|
||||
defaultMessage: 'Map Pin',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.STAR_EMPTY,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.starLabel', { defaultMessage: 'Star' }),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.TAG,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.tagIconLabel', {
|
||||
defaultMessage: 'Tag',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: AvailableReferenceLineIcons.TRIANGLE,
|
||||
label: i18n.translate('expressionXY.xyChart.iconSelect.triangleIconLabel', {
|
||||
defaultMessage: 'Triangle',
|
||||
}),
|
||||
icon: TriangleIcon,
|
||||
shouldRotate: true,
|
||||
canFill: true,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -14,5 +14,5 @@ export * from './fitting_functions';
|
|||
export * from './axes_configuration';
|
||||
export * from './icon';
|
||||
export * from './color_assignment';
|
||||
export * from './annotations_icon_set';
|
||||
export * from './annotations';
|
||||
export * from './data_layers';
|
||||
|
|
|
@ -6,32 +6,36 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DataLayerConfigResult, XYChartProps } from '../../common';
|
||||
import { DataLayerConfig, XYChartProps } from '../../common';
|
||||
import { sampleArgs } from '../../common/__mocks__';
|
||||
import { calculateMinInterval } from './interval';
|
||||
|
||||
describe('calculateMinInterval', () => {
|
||||
let xyProps: XYChartProps;
|
||||
|
||||
let layer: DataLayerConfig;
|
||||
beforeEach(() => {
|
||||
xyProps = sampleArgs();
|
||||
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'time';
|
||||
const { layers, ...restArgs } = sampleArgs().args;
|
||||
|
||||
xyProps = { args: { ...restArgs, layers } };
|
||||
layer = xyProps.args.layers[0] as DataLayerConfig;
|
||||
layer.xScaleType = 'time';
|
||||
});
|
||||
it('should use first valid layer and determine interval', async () => {
|
||||
xyProps.data.tables.first.columns[2].meta.source = 'esaggs';
|
||||
xyProps.data.tables.first.columns[2].meta.sourceParams = {
|
||||
layer.table.columns[2].meta.source = 'esaggs';
|
||||
layer.table.columns[2].meta.sourceParams = {
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
used_interval: '5m',
|
||||
},
|
||||
};
|
||||
xyProps.args.layers[0] = layer;
|
||||
const result = await calculateMinInterval(xyProps);
|
||||
expect(result).toEqual(5 * 60 * 1000);
|
||||
});
|
||||
|
||||
it('should return interval of number histogram if available on first x axis columns', async () => {
|
||||
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'linear';
|
||||
xyProps.data.tables.first.columns[2].meta = {
|
||||
layer.xScaleType = 'linear';
|
||||
layer.table.columns[2].meta = {
|
||||
source: 'esaggs',
|
||||
type: 'number',
|
||||
field: 'someField',
|
||||
|
@ -43,19 +47,22 @@ describe('calculateMinInterval', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
xyProps.args.layers[0] = layer;
|
||||
const result = await calculateMinInterval(xyProps);
|
||||
expect(result).toEqual(5);
|
||||
});
|
||||
|
||||
it('should return undefined if data table is empty', async () => {
|
||||
xyProps.data.tables.first.rows = [];
|
||||
xyProps.data.tables.first.columns[2].meta.source = 'esaggs';
|
||||
xyProps.data.tables.first.columns[2].meta.sourceParams = {
|
||||
layer.table.rows = [];
|
||||
layer.table.columns[2].meta.source = 'esaggs';
|
||||
layer.table.columns[2].meta.sourceParams = {
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
used_interval: '5m',
|
||||
},
|
||||
};
|
||||
|
||||
xyProps.args.layers[0] = layer;
|
||||
const result = await calculateMinInterval(xyProps);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
@ -66,14 +73,16 @@ describe('calculateMinInterval', () => {
|
|||
});
|
||||
|
||||
it('should return undefined if date column is not found', async () => {
|
||||
xyProps.data.tables.first.columns.splice(2, 1);
|
||||
layer.table.columns.splice(2, 1);
|
||||
xyProps.args.layers[0] = layer;
|
||||
const result = await calculateMinInterval(xyProps);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if x axis is not a date', async () => {
|
||||
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'ordinal';
|
||||
xyProps.data.tables.first.columns.splice(2, 1);
|
||||
layer.xScaleType = 'ordinal';
|
||||
xyProps.args.layers[0] = layer;
|
||||
xyProps.args.layers[0].table.columns.splice(2, 1);
|
||||
const result = await calculateMinInterval(xyProps);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
|
|
@ -11,11 +11,11 @@ import { XYChartProps } from '../../common';
|
|||
import { getFilteredLayers } from './layers';
|
||||
import { isDataLayer } from './visualization';
|
||||
|
||||
export function calculateMinInterval({ args: { layers }, data }: XYChartProps) {
|
||||
const filteredLayers = getFilteredLayers(layers, data);
|
||||
export function calculateMinInterval({ args: { layers } }: XYChartProps) {
|
||||
const filteredLayers = getFilteredLayers(layers);
|
||||
if (filteredLayers.length === 0) return;
|
||||
const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time');
|
||||
const xColumn = data.tables[filteredLayers[0].layerId].columns.find(
|
||||
const xColumn = filteredLayers[0].table.columns.find(
|
||||
(column) => isDataLayer(filteredLayers[0]) && column.id === filteredLayers[0].xAccessor
|
||||
);
|
||||
|
||||
|
|
|
@ -6,24 +6,42 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LensMultiTable } from '../../common';
|
||||
import { DataLayerConfigResult, XYLayerConfigResult } from '../../common/types';
|
||||
import { getDataLayers } from './visualization';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import {
|
||||
CommonXYDataLayerConfig,
|
||||
CommonXYLayerConfig,
|
||||
CommonXYReferenceLineLayerConfig,
|
||||
} from '../../common/types';
|
||||
import { isDataLayer, isReferenceLayer } from './visualization';
|
||||
|
||||
export function getFilteredLayers(layers: CommonXYLayerConfig[]) {
|
||||
return layers.filter<CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig>(
|
||||
(layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => {
|
||||
let table: Datatable | undefined;
|
||||
let accessors: string[] = [];
|
||||
let xAccessor: undefined | string | number;
|
||||
let splitAccessor: undefined | string | number;
|
||||
|
||||
if (isDataLayer(layer)) {
|
||||
xAccessor = layer.xAccessor;
|
||||
splitAccessor = layer.splitAccessor;
|
||||
}
|
||||
|
||||
if (isDataLayer(layer) || isReferenceLayer(layer)) {
|
||||
table = layer.table;
|
||||
accessors = layer.accessors;
|
||||
}
|
||||
|
||||
export function getFilteredLayers(layers: XYLayerConfigResult[], data: LensMultiTable) {
|
||||
return getDataLayers(layers).filter<DataLayerConfigResult>(
|
||||
(layer): layer is DataLayerConfigResult => {
|
||||
const { layerId, xAccessor, accessors, splitAccessor } = layer;
|
||||
return !(
|
||||
!accessors.length ||
|
||||
!data.tables[layerId] ||
|
||||
data.tables[layerId].rows.length === 0 ||
|
||||
!table ||
|
||||
table.rows.length === 0 ||
|
||||
(xAccessor &&
|
||||
data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) ||
|
||||
table.rows.every((row) => xAccessor && typeof row[xAccessor] === 'undefined')) ||
|
||||
// stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty
|
||||
(!xAccessor &&
|
||||
splitAccessor &&
|
||||
data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined'))
|
||||
table.rows.every((row) => splitAccessor && typeof row[splitAccessor] === 'undefined'))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { SeriesType, XYLayerConfigResult, YConfig } from '../../common';
|
||||
import type { CommonXYLayerConfig, SeriesType, ExtendedYConfig, YConfig } from '../../common';
|
||||
import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization';
|
||||
|
||||
export function isHorizontalSeries(seriesType: SeriesType) {
|
||||
|
@ -21,16 +21,14 @@ export function isStackedChart(seriesType: SeriesType) {
|
|||
return seriesType.includes('stacked');
|
||||
}
|
||||
|
||||
export function isHorizontalChart(layers: XYLayerConfigResult[]) {
|
||||
export function isHorizontalChart(layers: CommonXYLayerConfig[]) {
|
||||
return getDataLayers(layers).every((l) => isHorizontalSeries(l.seriesType));
|
||||
}
|
||||
|
||||
export const getSeriesColor = (layer: XYLayerConfigResult, accessor: string) => {
|
||||
export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => {
|
||||
if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null
|
||||
);
|
||||
const yConfig: Array<YConfig | ExtendedYConfig> | undefined = layer?.yConfig;
|
||||
return yConfig?.find((yConf) => yConf.forAccessor === accessor)?.color || null;
|
||||
};
|
||||
|
|
|
@ -6,38 +6,40 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataLayerConfigResult,
|
||||
ReferenceLineLayerConfigResult,
|
||||
XYLayerConfigResult,
|
||||
AnnotationLayerConfigResult,
|
||||
} from '../../common/types';
|
||||
import { LayerTypes } from '../../common/constants';
|
||||
import {
|
||||
CommonXYLayerConfig,
|
||||
CommonXYDataLayerConfig,
|
||||
CommonXYReferenceLineLayerConfig,
|
||||
CommonXYAnnotationLayerConfig,
|
||||
} from '../../common/types';
|
||||
|
||||
export const isDataLayer = (layer: XYLayerConfigResult): layer is DataLayerConfigResult =>
|
||||
export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig =>
|
||||
layer.layerType === LayerTypes.DATA || !layer.layerType;
|
||||
|
||||
export const getDataLayers = (layers: XYLayerConfigResult[]) =>
|
||||
(layers || []).filter((layer): layer is DataLayerConfigResult => isDataLayer(layer));
|
||||
export const getDataLayers = (layers: CommonXYLayerConfig[]) =>
|
||||
(layers || []).filter((layer): layer is CommonXYDataLayerConfig => isDataLayer(layer));
|
||||
|
||||
export const isReferenceLayer = (
|
||||
layer: XYLayerConfigResult
|
||||
): layer is ReferenceLineLayerConfigResult => layer.layerType === LayerTypes.REFERENCELINE;
|
||||
layer: CommonXYLayerConfig
|
||||
): layer is CommonXYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE;
|
||||
|
||||
export const getReferenceLayers = (layers: XYLayerConfigResult[]) =>
|
||||
(layers || []).filter((layer): layer is ReferenceLineLayerConfigResult =>
|
||||
export const getReferenceLayers = (layers: CommonXYLayerConfig[]) =>
|
||||
(layers || []).filter((layer): layer is CommonXYReferenceLineLayerConfig =>
|
||||
isReferenceLayer(layer)
|
||||
);
|
||||
|
||||
const isAnnotationLayerCommon = (
|
||||
layer: XYLayerConfigResult
|
||||
): layer is AnnotationLayerConfigResult => layer.layerType === LayerTypes.ANNOTATIONS;
|
||||
layer: CommonXYLayerConfig
|
||||
): layer is CommonXYAnnotationLayerConfig => layer.layerType === LayerTypes.ANNOTATIONS;
|
||||
|
||||
export const isAnnotationsLayer = (
|
||||
layer: XYLayerConfigResult
|
||||
): layer is AnnotationLayerConfigResult => isAnnotationLayerCommon(layer);
|
||||
layer: CommonXYLayerConfig
|
||||
): layer is CommonXYAnnotationLayerConfig => isAnnotationLayerCommon(layer);
|
||||
|
||||
export const getAnnotationsLayers = (
|
||||
layers: XYLayerConfigResult[]
|
||||
): AnnotationLayerConfigResult[] =>
|
||||
(layers || []).filter((layer): layer is AnnotationLayerConfigResult => isAnnotationsLayer(layer));
|
||||
layers: CommonXYLayerConfig[]
|
||||
): CommonXYAnnotationLayerConfig[] =>
|
||||
(layers || []).filter((layer): layer is CommonXYAnnotationLayerConfig =>
|
||||
isAnnotationsLayer(layer)
|
||||
);
|
||||
|
|
|
@ -16,17 +16,22 @@ import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public'
|
|||
import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types';
|
||||
import {
|
||||
xyVisFunction,
|
||||
layeredXyVisFunction,
|
||||
dataLayerFunction,
|
||||
extendedDataLayerFunction,
|
||||
yAxisConfigFunction,
|
||||
extendedYAxisConfigFunction,
|
||||
legendConfigFunction,
|
||||
gridlinesConfigFunction,
|
||||
dataLayerConfigFunction,
|
||||
axisExtentConfigFunction,
|
||||
tickLabelsConfigFunction,
|
||||
annotationLayerConfigFunction,
|
||||
referenceLineLayerFunction,
|
||||
extendedReferenceLineLayerFunction,
|
||||
annotationLayerFunction,
|
||||
labelsOrientationConfigFunction,
|
||||
referenceLineLayerConfigFunction,
|
||||
axisTitlesVisibilityConfigFunction,
|
||||
} from '../common';
|
||||
extendedAnnotationLayerFunction,
|
||||
} from '../common/expression_functions';
|
||||
import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers';
|
||||
|
||||
export interface XYPluginStartDependencies {
|
||||
|
@ -51,16 +56,21 @@ export class ExpressionXyPlugin {
|
|||
{ expressions, charts }: SetupDeps
|
||||
): ExpressionXyPluginSetup {
|
||||
expressions.registerFunction(yAxisConfigFunction);
|
||||
expressions.registerFunction(extendedYAxisConfigFunction);
|
||||
expressions.registerFunction(legendConfigFunction);
|
||||
expressions.registerFunction(gridlinesConfigFunction);
|
||||
expressions.registerFunction(dataLayerConfigFunction);
|
||||
expressions.registerFunction(dataLayerFunction);
|
||||
expressions.registerFunction(extendedDataLayerFunction);
|
||||
expressions.registerFunction(axisExtentConfigFunction);
|
||||
expressions.registerFunction(tickLabelsConfigFunction);
|
||||
expressions.registerFunction(annotationLayerConfigFunction);
|
||||
expressions.registerFunction(annotationLayerFunction);
|
||||
expressions.registerFunction(extendedAnnotationLayerFunction);
|
||||
expressions.registerFunction(labelsOrientationConfigFunction);
|
||||
expressions.registerFunction(referenceLineLayerConfigFunction);
|
||||
expressions.registerFunction(referenceLineLayerFunction);
|
||||
expressions.registerFunction(extendedReferenceLineLayerFunction);
|
||||
expressions.registerFunction(axisTitlesVisibilityConfigFunction);
|
||||
expressions.registerFunction(xyVisFunction);
|
||||
expressions.registerFunction(layeredXyVisFunction);
|
||||
|
||||
const getStartDeps: GetStartDepsFn = async () => {
|
||||
const [coreStart, deps] = await core.getStartServices();
|
||||
|
|
|
@ -12,16 +12,21 @@ import { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types';
|
|||
import {
|
||||
xyVisFunction,
|
||||
yAxisConfigFunction,
|
||||
extendedYAxisConfigFunction,
|
||||
legendConfigFunction,
|
||||
gridlinesConfigFunction,
|
||||
dataLayerConfigFunction,
|
||||
dataLayerFunction,
|
||||
axisExtentConfigFunction,
|
||||
tickLabelsConfigFunction,
|
||||
annotationLayerConfigFunction,
|
||||
annotationLayerFunction,
|
||||
labelsOrientationConfigFunction,
|
||||
referenceLineLayerConfigFunction,
|
||||
referenceLineLayerFunction,
|
||||
axisTitlesVisibilityConfigFunction,
|
||||
} from '../common';
|
||||
extendedDataLayerFunction,
|
||||
extendedReferenceLineLayerFunction,
|
||||
layeredXyVisFunction,
|
||||
extendedAnnotationLayerFunction,
|
||||
} from '../common/expression_functions';
|
||||
import { SetupDeps } from './types';
|
||||
|
||||
export class ExpressionXyPlugin
|
||||
|
@ -29,16 +34,21 @@ export class ExpressionXyPlugin
|
|||
{
|
||||
public setup(core: CoreSetup, { expressions }: SetupDeps) {
|
||||
expressions.registerFunction(yAxisConfigFunction);
|
||||
expressions.registerFunction(extendedYAxisConfigFunction);
|
||||
expressions.registerFunction(legendConfigFunction);
|
||||
expressions.registerFunction(gridlinesConfigFunction);
|
||||
expressions.registerFunction(dataLayerConfigFunction);
|
||||
expressions.registerFunction(dataLayerFunction);
|
||||
expressions.registerFunction(extendedDataLayerFunction);
|
||||
expressions.registerFunction(axisExtentConfigFunction);
|
||||
expressions.registerFunction(tickLabelsConfigFunction);
|
||||
expressions.registerFunction(annotationLayerConfigFunction);
|
||||
expressions.registerFunction(annotationLayerFunction);
|
||||
expressions.registerFunction(extendedAnnotationLayerFunction);
|
||||
expressions.registerFunction(labelsOrientationConfigFunction);
|
||||
expressions.registerFunction(referenceLineLayerConfigFunction);
|
||||
expressions.registerFunction(referenceLineLayerFunction);
|
||||
expressions.registerFunction(extendedReferenceLineLayerFunction);
|
||||
expressions.registerFunction(axisTitlesVisibilityConfigFunction);
|
||||
expressions.registerFunction(xyVisFunction);
|
||||
expressions.registerFunction(layeredXyVisFunction);
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {}
|
||||
|
|
24
src/plugins/event_annotation/common/constants.ts
Normal file
24
src/plugins/event_annotation/common/constants.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 const AvailableAnnotationIcons = {
|
||||
ASTERISK: 'asterisk',
|
||||
ALERT: 'alert',
|
||||
BELL: 'bell',
|
||||
BOLT: 'bolt',
|
||||
BUG: 'bug',
|
||||
CIRCLE: 'circle',
|
||||
EDITOR_COMMENT: 'editorComment',
|
||||
FLAG: 'flag',
|
||||
HEART: 'heart',
|
||||
MAP_MARKER: 'mapMarker',
|
||||
PIN_FILLED: 'pinFilled',
|
||||
STAR_EMPTY: 'starEmpty',
|
||||
TAG: 'tag',
|
||||
TRIANGLE: 'triangle',
|
||||
} as const;
|
|
@ -17,4 +17,8 @@ export type {
|
|||
export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation';
|
||||
export { eventAnnotationGroup } from './event_annotation_group';
|
||||
export type { EventAnnotationGroupArgs } from './event_annotation_group';
|
||||
export type { EventAnnotationConfig, RangeEventAnnotationConfig } from './types';
|
||||
export type {
|
||||
EventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
AvailableAnnotationIcon,
|
||||
} from './types';
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AvailableAnnotationIcons } from '../constants';
|
||||
|
||||
import type {
|
||||
ManualRangeEventAnnotationArgs,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
|
@ -65,6 +67,8 @@ export const manualPointEventAnnotation: ExpressionFunctionDefinition<
|
|||
help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', {
|
||||
defaultMessage: 'An optional icon used for annotation lines',
|
||||
}),
|
||||
options: [...Object.values(AvailableAnnotationIcons)],
|
||||
strict: true,
|
||||
},
|
||||
textVisibility: {
|
||||
types: ['boolean'],
|
||||
|
|
|
@ -6,15 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
import { AvailableAnnotationIcons } from './constants';
|
||||
|
||||
export type LineStyle = 'solid' | 'dashed' | 'dotted';
|
||||
export type Fill = 'inside' | 'outside' | 'none';
|
||||
export type AnnotationType = 'manual';
|
||||
export type KeyType = 'point_in_time' | 'range';
|
||||
|
||||
export type AvailableAnnotationIcon = $Values<typeof AvailableAnnotationIcons>;
|
||||
export interface PointStyleProps {
|
||||
label: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
icon?: AvailableAnnotationIcon;
|
||||
lineWidth?: number;
|
||||
lineStyle?: LineStyle;
|
||||
textVisibility?: boolean;
|
||||
|
|
|
@ -714,7 +714,7 @@ describe('Execution', () => {
|
|||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: '[requiredArg] > requiredArg requires an argument',
|
||||
message: '[requiredArg] > requiredArg requires the "arg" argument',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -725,7 +725,7 @@ describe('Execution', () => {
|
|||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: '[var_set] > var_set requires an "name" argument',
|
||||
message: '[var_set] > var_set requires the "name" argument',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -481,7 +481,7 @@ export class Execution<
|
|||
);
|
||||
|
||||
// Check for missing required arguments.
|
||||
for (const { aliases, default: argDefault, name, required } of Object.values(argDefs)) {
|
||||
for (const { default: argDefault, name, required } of Object.values(argDefs)) {
|
||||
if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') {
|
||||
dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')];
|
||||
}
|
||||
|
@ -490,13 +490,7 @@ export class Execution<
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!aliases?.length) {
|
||||
throw new Error(`${fnDef.name} requires an argument`);
|
||||
}
|
||||
|
||||
// use an alias if _ is the missing arg
|
||||
const errorArg = name === '_' ? aliases[0] : name;
|
||||
throw new Error(`${fnDef.name} requires an "${errorArg}" argument`);
|
||||
throw new Error(`${fnDef.name} requires the "${name}" argument`);
|
||||
}
|
||||
|
||||
// Create the functions to resolve the argument ASTs into values
|
||||
|
|
|
@ -57,8 +57,7 @@ export type CustomPaletteParamsConfig = CustomPaletteParams & {
|
|||
|
||||
export type LayerType = typeof layerTypes[keyof typeof layerTypes];
|
||||
|
||||
// Shared by XY Chart and Heatmap as for now
|
||||
export type ValueLabelConfig = 'hide' | 'inside' | 'outside';
|
||||
export type ValueLabelConfig = 'hide' | 'show';
|
||||
|
||||
export type PieChartType = $Values<typeof PieChartTypes>;
|
||||
export type CategoryDisplayType = $Values<typeof CategoryDisplay>;
|
||||
|
|
|
@ -145,8 +145,6 @@ export function LayerPanel(
|
|||
const isEmptyLayer = !groups.some((d) => d.accessors.length > 0);
|
||||
const { activeId, activeGroup } = activeDimension;
|
||||
|
||||
const { setDimension, removeDimension } = activeVisualization;
|
||||
|
||||
const allAccessors = groups.flatMap((group) =>
|
||||
group.accessors.map((accessor) => accessor.columnId)
|
||||
);
|
||||
|
@ -209,7 +207,7 @@ export function LayerPanel(
|
|||
previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined;
|
||||
}
|
||||
}
|
||||
const newVisState = setDimension({
|
||||
const newVisState = activeVisualization.setDimension({
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
|
@ -221,7 +219,7 @@ export function LayerPanel(
|
|||
if (typeof dropResult === 'object') {
|
||||
// When a column is moved, we delete the reference to the old
|
||||
updateVisualization(
|
||||
removeDimension({
|
||||
activeVisualization.removeDimension({
|
||||
columnId: dropResult.deleted,
|
||||
layerId: targetLayerId,
|
||||
prevState: newVisState,
|
||||
|
@ -234,7 +232,7 @@ export function LayerPanel(
|
|||
}
|
||||
} else {
|
||||
if (dropType === 'duplicate_compatible' || dropType === 'reorder') {
|
||||
const newVisState = setDimension({
|
||||
const newVisState = activeVisualization.setDimension({
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
|
@ -247,16 +245,15 @@ export function LayerPanel(
|
|||
}
|
||||
};
|
||||
}, [
|
||||
framePublicAPI,
|
||||
layerDatasource,
|
||||
setNextFocusedButtonId,
|
||||
groups,
|
||||
layerDatasourceOnDrop,
|
||||
props.visualizationState,
|
||||
updateVisualization,
|
||||
setDimension,
|
||||
removeDimension,
|
||||
layerDatasourceDropProps,
|
||||
setNextFocusedButtonId,
|
||||
layerDatasource,
|
||||
activeVisualization,
|
||||
props.visualizationState,
|
||||
framePublicAPI,
|
||||
updateVisualization,
|
||||
]);
|
||||
|
||||
const isDimensionPanelOpen = Boolean(activeId);
|
||||
|
|
|
@ -9,11 +9,10 @@ import { Ast, AstFunction, fromExpression } from '@kbn/interpreter';
|
|||
import { DatasourceStates } from '../../state_management';
|
||||
import { Visualization, DatasourceMap, DatasourceLayers } from '../../types';
|
||||
|
||||
export function prependDatasourceExpression(
|
||||
visualizationExpression: Ast | string | null,
|
||||
export function getDatasourceExpressionsByLayers(
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: DatasourceStates
|
||||
): Ast | null {
|
||||
): null | Record<string, Ast> {
|
||||
const datasourceExpressions: Array<[string, Ast | string]> = [];
|
||||
|
||||
Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => {
|
||||
|
@ -28,19 +27,41 @@ export function prependDatasourceExpression(
|
|||
});
|
||||
});
|
||||
|
||||
if (datasourceExpressions.length === 0 || visualizationExpression === null) {
|
||||
if (datasourceExpressions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const parsedDatasourceExpressions: Array<[string, Ast]> = datasourceExpressions.map(
|
||||
([layerId, expr]) => [layerId, typeof expr === 'string' ? fromExpression(expr) : expr]
|
||||
|
||||
return datasourceExpressions.reduce(
|
||||
(exprs, [layerId, expr]) => ({
|
||||
...exprs,
|
||||
[layerId]: typeof expr === 'string' ? fromExpression(expr) : expr,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function prependDatasourceExpression(
|
||||
visualizationExpression: Ast | string | null,
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: DatasourceStates
|
||||
): Ast | null {
|
||||
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
|
||||
datasourceMap,
|
||||
datasourceStates
|
||||
);
|
||||
|
||||
if (datasourceExpressionsByLayers === null || visualizationExpression === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedDatasourceExpressions = Object.entries(datasourceExpressionsByLayers);
|
||||
|
||||
const datafetchExpression: AstFunction = {
|
||||
type: 'function',
|
||||
function: 'lens_merge_tables',
|
||||
arguments: {
|
||||
layerIds: parsedDatasourceExpressions.map(([id]) => id),
|
||||
tables: parsedDatasourceExpressions.map(([id, expr]) => expr),
|
||||
tables: parsedDatasourceExpressions.map(([, expr]) => expr),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -79,16 +100,32 @@ export function buildExpression({
|
|||
if (visualization === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
|
||||
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
|
||||
datasourceMap,
|
||||
datasourceStates
|
||||
);
|
||||
|
||||
const visualizationExpression = visualization.toExpression(
|
||||
visualizationState,
|
||||
datasourceLayers,
|
||||
{
|
||||
title,
|
||||
description,
|
||||
},
|
||||
datasourceExpressionsByLayers ?? undefined
|
||||
);
|
||||
|
||||
return typeof visualizationExpression === 'string'
|
||||
? fromExpression(visualizationExpression)
|
||||
: visualizationExpression;
|
||||
}
|
||||
|
||||
const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers, {
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
const completeExpression = prependDatasourceExpression(
|
||||
visualizationExpression,
|
||||
datasourceMap,
|
||||
datasourceStates
|
||||
);
|
||||
|
||||
return completeExpression;
|
||||
return prependDatasourceExpression(visualizationExpression, datasourceMap, datasourceStates);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { IconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { Ast, toExpression } from '@kbn/interpreter';
|
||||
import { Ast, fromExpression, toExpression } from '@kbn/interpreter';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { ExecutionContextSearch } from '@kbn/data-plugin/public';
|
||||
|
@ -39,7 +39,10 @@ import {
|
|||
DatasourceLayers,
|
||||
} from '../../types';
|
||||
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
|
||||
import { prependDatasourceExpression } from './expression_helpers';
|
||||
import {
|
||||
getDatasourceExpressionsByLayers,
|
||||
prependDatasourceExpression,
|
||||
} from './expression_helpers';
|
||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import {
|
||||
getMissingIndexPattern,
|
||||
|
@ -485,6 +488,7 @@ function getPreviewExpression(
|
|||
visualizableState: VisualizableState,
|
||||
visualization: Visualization,
|
||||
datasources: Record<string, Datasource>,
|
||||
datasourceStates: DatasourceStates,
|
||||
frame: FramePublicAPI
|
||||
) {
|
||||
if (!visualization.toPreviewExpression) {
|
||||
|
@ -518,6 +522,19 @@ function getPreviewExpression(
|
|||
});
|
||||
}
|
||||
|
||||
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
|
||||
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
|
||||
datasources,
|
||||
datasourceStates
|
||||
);
|
||||
|
||||
return visualization.toPreviewExpression(
|
||||
visualizableState.visualizationState,
|
||||
suggestionFrameApi.datasourceLayers,
|
||||
datasourceExpressionsByLayers ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
return visualization.toPreviewExpression(
|
||||
visualizableState.visualizationState,
|
||||
suggestionFrameApi.datasourceLayers
|
||||
|
@ -534,21 +551,7 @@ function preparePreviewExpression(
|
|||
const suggestionDatasourceId = visualizableState.datasourceId;
|
||||
const suggestionDatasourceState = visualizableState.datasourceState;
|
||||
|
||||
const expression = getPreviewExpression(
|
||||
visualizableState,
|
||||
visualization,
|
||||
datasourceMap,
|
||||
framePublicAPI
|
||||
);
|
||||
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expressionWithDatasource = prependDatasourceExpression(
|
||||
expression,
|
||||
datasourceMap,
|
||||
suggestionDatasourceId
|
||||
const datasourceStatesWithSuggestions = suggestionDatasourceId
|
||||
? {
|
||||
...datasourceStates,
|
||||
[suggestionDatasourceId]: {
|
||||
|
@ -556,8 +559,27 @@ function preparePreviewExpression(
|
|||
state: suggestionDatasourceState,
|
||||
},
|
||||
}
|
||||
: datasourceStates
|
||||
: datasourceStates;
|
||||
|
||||
const previewExprDatasourcesStates = visualization.shouldBuildDatasourceExpressionManually?.()
|
||||
? datasourceStatesWithSuggestions
|
||||
: datasourceStates;
|
||||
|
||||
const expression = getPreviewExpression(
|
||||
visualizableState,
|
||||
visualization,
|
||||
datasourceMap,
|
||||
previewExprDatasourcesStates,
|
||||
framePublicAPI
|
||||
);
|
||||
|
||||
return expressionWithDatasource;
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
|
||||
return typeof expression === 'string' ? fromExpression(expression) : expression;
|
||||
}
|
||||
|
||||
return prependDatasourceExpression(expression, datasourceMap, datasourceStatesWithSuggestions);
|
||||
}
|
||||
|
|
|
@ -348,6 +348,7 @@ export class Embeddable
|
|||
if (!this.savedVis || !this.savedVis.visualizationType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || [];
|
||||
}
|
||||
|
||||
|
@ -458,6 +459,7 @@ export class Embeddable
|
|||
this.embeddableTitle = this.getTitle();
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
return isDirty;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,11 +62,11 @@ export const HeatmapToolbar = memo(
|
|||
buttonDataTestSubj="lnsVisualOptionsButton"
|
||||
>
|
||||
<ValueLabelsSettings
|
||||
valueLabels={state?.gridConfig.isCellLabelVisible ? 'inside' : 'hide'}
|
||||
valueLabels={state?.gridConfig.isCellLabelVisible ? 'show' : 'hide'}
|
||||
onValueLabelChange={(newMode) => {
|
||||
setState({
|
||||
...state,
|
||||
gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'inside' },
|
||||
gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'show' },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,7 @@ export type {
|
|||
XYState,
|
||||
XYReferenceLineLayerConfig,
|
||||
XYLayerConfig,
|
||||
ValidLayer,
|
||||
XYDataLayerConfig,
|
||||
XYAnnotationLayerConfig,
|
||||
} from './xy_visualization/types';
|
||||
|
@ -70,7 +71,7 @@ export type {
|
|||
} from './indexpattern_datasource/types';
|
||||
export type {
|
||||
XYArgs,
|
||||
YConfig,
|
||||
ExtendedYConfig,
|
||||
XYRender,
|
||||
LayerType,
|
||||
YAxisMode,
|
||||
|
@ -80,28 +81,27 @@ export type {
|
|||
YScaleType,
|
||||
XScaleType,
|
||||
AxisConfig,
|
||||
ValidLayer,
|
||||
XYCurveType,
|
||||
XYChartProps,
|
||||
LegendConfig,
|
||||
IconPosition,
|
||||
YConfigResult,
|
||||
ExtendedYConfigResult,
|
||||
DataLayerArgs,
|
||||
LensMultiTable,
|
||||
ValueLabelMode,
|
||||
AxisExtentMode,
|
||||
DataLayerConfig,
|
||||
FittingFunction,
|
||||
AxisExtentConfig,
|
||||
LegendConfigResult,
|
||||
AxesSettingsConfig,
|
||||
GridlinesConfigResult,
|
||||
DataLayerConfigResult,
|
||||
TickLabelsConfigResult,
|
||||
AxisExtentConfigResult,
|
||||
ReferenceLineLayerArgs,
|
||||
LabelsOrientationConfig,
|
||||
ReferenceLineLayerConfig,
|
||||
LabelsOrientationConfigResult,
|
||||
ReferenceLineLayerConfigResult,
|
||||
AxisTitlesVisibilityConfigResult,
|
||||
} from '@kbn/expression-xy-plugin/common';
|
||||
export type { LensEmbeddableInput } from './embeddable';
|
||||
|
|
|
@ -235,6 +235,7 @@ export function getIndexPatternDatasource({
|
|||
if (staticValue == null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
|
|
|
@ -30,11 +30,11 @@ export interface LegendLocationSettingsProps {
|
|||
/**
|
||||
* Sets the vertical alignment for legend inside chart
|
||||
*/
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
|
||||
/**
|
||||
* Sets the vertical alignment for legend inside chart
|
||||
*/
|
||||
horizontalAlignment?: HorizontalAlignment;
|
||||
horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right;
|
||||
/**
|
||||
* Callback on horizontal alignment option change
|
||||
*/
|
||||
|
|
|
@ -58,11 +58,11 @@ export interface LegendSettingsPopoverProps {
|
|||
/**
|
||||
* Sets the vertical alignment for legend inside chart
|
||||
*/
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
|
||||
/**
|
||||
* Sets the vertical alignment for legend inside chart
|
||||
*/
|
||||
horizontalAlignment?: HorizontalAlignment;
|
||||
horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right;
|
||||
/**
|
||||
* Callback on horizontal alignment option change
|
||||
*/
|
||||
|
@ -225,6 +225,7 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
|
|||
position={position}
|
||||
onPositionChange={onPositionChange}
|
||||
/>
|
||||
{location !== 'inside' && (
|
||||
<LegendSizeSettings
|
||||
legendSize={legendSize}
|
||||
onLegendSizeChange={onLegendSizeChange}
|
||||
|
@ -232,6 +233,7 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
|
|||
!position || position === Position.Left || position === Position.Right
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{location && (
|
||||
<ColumnsNumberSetting
|
||||
floatingColumns={floatingColumns}
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('Value labels Settings', () => {
|
|||
});
|
||||
|
||||
it('should render the passed value if given', () => {
|
||||
const component = shallow(<ValueLabelsSettings {...props} valueLabels="inside" />);
|
||||
const component = shallow(<ValueLabelsSettings {...props} valueLabels="show" />);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected')
|
||||
).toEqual(`value_labels_inside`);
|
||||
|
|
|
@ -26,7 +26,7 @@ const valueLabelsOptions: Array<{
|
|||
},
|
||||
{
|
||||
id: `value_labels_inside`,
|
||||
value: 'inside',
|
||||
value: 'show',
|
||||
label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.inside', {
|
||||
defaultMessage: 'Show',
|
||||
}),
|
||||
|
|
|
@ -588,7 +588,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
|||
labels?: { buttonAriaLabel: string; buttonLabel: string };
|
||||
};
|
||||
|
||||
interface VisualizationDimensionChangeProps<T> {
|
||||
export interface VisualizationDimensionChangeProps<T> {
|
||||
layerId: string;
|
||||
columnId: string;
|
||||
prevState: T;
|
||||
|
@ -887,7 +887,8 @@ export interface Visualization<T = unknown> {
|
|||
toExpression: (
|
||||
state: T,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
attributes?: Partial<{ title: string; description: string }>
|
||||
attributes?: Partial<{ title: string; description: string }>,
|
||||
datasourceExpressionsByLayers?: Record<string, Ast>
|
||||
) => ExpressionAstExpression | string | null;
|
||||
/**
|
||||
* Expression to render a preview version of the chart in very constrained space.
|
||||
|
@ -895,7 +896,8 @@ export interface Visualization<T = unknown> {
|
|||
*/
|
||||
toPreviewExpression?: (
|
||||
state: T,
|
||||
datasourceLayers: DatasourceLayers
|
||||
datasourceLayers: DatasourceLayers,
|
||||
datasourceExpressionsByLayers?: Record<string, Ast>
|
||||
) => ExpressionAstExpression | string | null;
|
||||
/**
|
||||
* The frame will call this function on all visualizations at few stages (pre-build/build error) in order
|
||||
|
@ -920,6 +922,12 @@ export interface Visualization<T = unknown> {
|
|||
* On Edit events the frame will call this to know what's going to be the next visualization state
|
||||
*/
|
||||
onEditAction?: (state: T, event: LensEditEvent<LensEditSupportedActions>) => T;
|
||||
|
||||
/**
|
||||
* `datasourceExpressionsByLayers` will be passed to the params of `toExpression` and `toPreviewExpression`
|
||||
* functions and datasource expressions will not be appended to the expression automatically.
|
||||
*/
|
||||
shouldBuildDatasourceExpressionManually?: () => boolean;
|
||||
}
|
||||
|
||||
export interface LensFilterEvent {
|
||||
|
|
|
@ -30,9 +30,6 @@ Object {
|
|||
"curveType": Array [
|
||||
"LINEAR",
|
||||
],
|
||||
"description": Array [
|
||||
"",
|
||||
],
|
||||
"emphasizeFitting": Array [
|
||||
true,
|
||||
],
|
||||
|
@ -135,6 +132,18 @@ Object {
|
|||
"splitAccessor": Array [
|
||||
"d",
|
||||
],
|
||||
"table": Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
],
|
||||
"xAccessor": Array [
|
||||
"a",
|
||||
],
|
||||
|
@ -146,7 +155,7 @@ Object {
|
|||
"linear",
|
||||
],
|
||||
},
|
||||
"function": "dataLayer",
|
||||
"function": "extendedDataLayer",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
|
@ -204,9 +213,6 @@ Object {
|
|||
"type": "expression",
|
||||
},
|
||||
],
|
||||
"title": Array [
|
||||
"",
|
||||
],
|
||||
"valueLabels": Array [
|
||||
"hide",
|
||||
],
|
||||
|
@ -263,7 +269,7 @@ Object {
|
|||
"",
|
||||
],
|
||||
},
|
||||
"function": "xyVis",
|
||||
"function": "layeredXyVis",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -135,6 +135,7 @@ export const setAnnotationsDimension: Visualization<XYState>['setDimension'] = (
|
|||
: undefined;
|
||||
|
||||
let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations'];
|
||||
|
||||
if (!currentConfig) {
|
||||
resultAnnotations.push({
|
||||
label: defaultAnnotationLabel,
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataLayerConfigResult } from '@kbn/expression-xy-plugin/common';
|
||||
import { layerTypes } from '../../common';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { getAxesConfiguration } from './axes_configuration';
|
||||
import { XYDataLayerConfig } from './types';
|
||||
|
||||
describe('axes_configuration', () => {
|
||||
const tables: Record<string, Datatable> = {
|
||||
|
@ -219,8 +219,7 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const sampleLayer: DataLayerConfigResult = {
|
||||
type: 'dataLayer',
|
||||
const sampleLayer: XYDataLayerConfig = {
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
seriesType: 'line',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { groupBy, partition } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { YAxisMode, YConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import type { YAxisMode, ExtendedYConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { layerTypes } from '../../common';
|
||||
import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types';
|
||||
|
@ -34,7 +34,7 @@ export interface ReferenceLineBase {
|
|||
* * what groups are current defined in data layers
|
||||
* * what existing reference line are currently defined in reference layers
|
||||
*/
|
||||
export function getGroupsToShow<T extends ReferenceLineBase & { config?: YConfig[] }>(
|
||||
export function getGroupsToShow<T extends ReferenceLineBase & { config?: ExtendedYConfig[] }>(
|
||||
referenceLayers: T[],
|
||||
state: XYState | undefined,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
|
@ -104,6 +104,7 @@ export function getStaticValue(
|
|||
untouchedDataLayers,
|
||||
accessors,
|
||||
} = getAccessorCriteriaForGroup(groupId, dataLayers, activeData);
|
||||
|
||||
if (
|
||||
groupId === 'x' &&
|
||||
filteredLayers.length &&
|
||||
|
@ -111,6 +112,7 @@ export function getStaticValue(
|
|||
) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const computedValue = computeStaticValueForGroup(
|
||||
filteredLayers,
|
||||
accessors,
|
||||
|
@ -118,6 +120,7 @@ export function getStaticValue(
|
|||
groupId !== 'x', // histogram axis should compute the min based on the current data
|
||||
groupId !== 'x'
|
||||
);
|
||||
|
||||
return computedValue ?? fallbackValue;
|
||||
}
|
||||
|
||||
|
@ -165,6 +168,7 @@ export function computeOverallDataDomain(
|
|||
const accessorMap = new Set(accessorIds);
|
||||
let min: number | undefined;
|
||||
let max: number | undefined;
|
||||
|
||||
const [stacked, unstacked] = partition(
|
||||
dataLayers,
|
||||
({ seriesType }) => isStackedChart(seriesType) && allowStacking
|
||||
|
@ -268,13 +272,17 @@ export const getReferenceSupportedLayer = (
|
|||
label: 'x' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const referenceLineGroups = getGroupsRelatedToData(
|
||||
referenceLineGroupIds,
|
||||
state,
|
||||
frame?.datasourceLayers || {},
|
||||
frame?.activeData
|
||||
);
|
||||
const dataLayers = getDataLayers(state?.layers || []);
|
||||
|
||||
const layers = state?.layers || [];
|
||||
const dataLayers = getDataLayers(layers);
|
||||
|
||||
const filledDataLayers = dataLayers.filter(
|
||||
({ accessors, xAccessor }) => accessors.length || xAccessor
|
||||
);
|
||||
|
@ -289,7 +297,7 @@ export const getReferenceSupportedLayer = (
|
|||
groupId: id,
|
||||
columnId: generateId(),
|
||||
dataType: 'number',
|
||||
label: getAxisName(label, { isHorizontal: isHorizontalChart(state?.layers || []) }),
|
||||
label: getAxisName(label, { isHorizontal: isHorizontalChart(layers) }),
|
||||
staticValue: getStaticValue(
|
||||
dataLayers,
|
||||
label,
|
||||
|
@ -317,6 +325,7 @@ export const getReferenceSupportedLayer = (
|
|||
initialDimensions,
|
||||
};
|
||||
};
|
||||
|
||||
export const setReferenceDimension: Visualization<XYState>['setDimension'] = ({
|
||||
prevState,
|
||||
layerId,
|
||||
|
@ -397,6 +406,7 @@ export const getReferenceConfiguration = ({
|
|||
return axisMode;
|
||||
}
|
||||
);
|
||||
|
||||
const groupsToShow = getGroupsToShow(
|
||||
[
|
||||
// When a reference layer panel is added, a static reference line should automatically be included by default
|
||||
|
@ -422,7 +432,7 @@ export const getReferenceConfiguration = ({
|
|||
],
|
||||
state,
|
||||
frame.datasourceLayers,
|
||||
frame?.activeData
|
||||
frame.activeData
|
||||
);
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
return {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { SeriesType, YConfig, ValidLayer } from '@kbn/expression-xy-plugin/common';
|
||||
import type { SeriesType, ExtendedYConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import type { FramePublicAPI, DatasourcePublicAPI } from '../types';
|
||||
import {
|
||||
visualizationTypes,
|
||||
|
@ -58,7 +58,8 @@ export const getSeriesColor = (layer: XYLayerConfig, accessor: string) => {
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null
|
||||
layer?.yConfig?.find((yConfig: ExtendedYConfig) => yConfig.forAccessor === accessor)?.color ||
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -79,7 +80,7 @@ export const getColumnToLabelMap = (
|
|||
};
|
||||
|
||||
export function hasHistogramSeries(
|
||||
layers: ValidLayer[] = [],
|
||||
layers: XYDataLayerConfig[] = [],
|
||||
datasourceLayers?: FramePublicAPI['datasourceLayers']
|
||||
) {
|
||||
if (!datasourceLayers) {
|
||||
|
@ -87,7 +88,11 @@ export function hasHistogramSeries(
|
|||
}
|
||||
const validLayers = layers.filter(({ accessors }) => accessors.length);
|
||||
|
||||
return validLayers.some(({ layerId, xAccessor }: ValidLayer) => {
|
||||
return validLayers.some(({ layerId, xAccessor }: XYDataLayerConfig) => {
|
||||
if (!xAccessor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const xAxisOperation = datasourceLayers[layerId].getOperationForColumnId(xAccessor);
|
||||
return (
|
||||
xAxisOperation &&
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { Ast, fromExpression } from '@kbn/interpreter';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { getXyVisualization } from './xy_visualization';
|
||||
|
@ -28,6 +28,8 @@ describe('#toExpression', () => {
|
|||
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
||||
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
||||
|
||||
let datasourceExpressionsByLayers: Record<string, Ast>;
|
||||
|
||||
beforeEach(() => {
|
||||
frame = createMockFramePublicAPI();
|
||||
mockDatasource = createMockDatasource('testDatasource');
|
||||
|
@ -46,6 +48,23 @@ describe('#toExpression', () => {
|
|||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
|
||||
const datasourceExpression = mockDatasource.toExpression(
|
||||
frame.datasourceLayers.first,
|
||||
'first'
|
||||
) ?? {
|
||||
type: 'expression',
|
||||
chain: [],
|
||||
};
|
||||
const exprAst =
|
||||
typeof datasourceExpression === 'string'
|
||||
? fromExpression(datasourceExpression)
|
||||
: datasourceExpression;
|
||||
|
||||
datasourceExpressionsByLayers = {
|
||||
first: exprAst,
|
||||
referenceLine: exprAst,
|
||||
};
|
||||
});
|
||||
|
||||
it('should map to a valid AST', () => {
|
||||
|
@ -82,7 +101,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
@ -106,7 +127,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast
|
||||
).chain[0].arguments.fittingFunction[0]
|
||||
).toEqual('None');
|
||||
|
@ -129,7 +152,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect(
|
||||
(expression.chain[0].arguments.axisTitlesVisibilitySettings[0] as Ast).chain[0].arguments
|
||||
|
@ -157,7 +182,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect((expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.xAccessor).toEqual(
|
||||
[]
|
||||
|
@ -182,7 +209,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
@ -204,7 +233,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
)! as Ast;
|
||||
|
||||
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b');
|
||||
|
@ -241,7 +272,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect(
|
||||
(expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments
|
||||
|
@ -269,7 +302,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect((expression.chain[0].arguments.labelsOrientation[0] as Ast).chain[0].arguments).toEqual({
|
||||
x: [0],
|
||||
|
@ -295,7 +330,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect(
|
||||
(expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments
|
||||
|
@ -310,7 +347,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'inside',
|
||||
valueLabels: 'show',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -323,16 +360,18 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('inside');
|
||||
expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('show');
|
||||
});
|
||||
|
||||
it('should compute the correct series color fallback based on the layer type', () => {
|
||||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'inside',
|
||||
valueLabels: 'show',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -352,7 +391,9 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
{ ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock }
|
||||
{ ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock },
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
|
||||
function getYConfigColorForLayer(ast: Ast, index: number) {
|
||||
|
|
|
@ -10,13 +10,15 @@ import { ScaleType } from '@elastic/charts';
|
|||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import type { ValidLayer, YConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import type { AxisExtentConfig, ExtendedYConfig, YConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
|
||||
import {
|
||||
State,
|
||||
XYDataLayerConfig,
|
||||
XYReferenceLineLayerConfig,
|
||||
XYAnnotationLayerConfig,
|
||||
} from './types';
|
||||
import type { ValidXYDataLayerConfig } from './types';
|
||||
import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types';
|
||||
import { getColumnToLabelMap } from './state_helpers';
|
||||
import { hasIcon } from './xy_config_panel/shared/icon_select';
|
||||
|
@ -50,6 +52,7 @@ export const toExpression = (
|
|||
datasourceLayers: DatasourceLayers,
|
||||
paletteService: PaletteRegistry,
|
||||
attributes: Partial<{ title: string; description: string }> = {},
|
||||
datasourceExpressionsByLayers: Record<string, Ast>,
|
||||
eventAnnotationService: EventAnnotationServiceType
|
||||
): Ast | null => {
|
||||
if (!state || !state.layers.length) {
|
||||
|
@ -73,7 +76,7 @@ export const toExpression = (
|
|||
metadata,
|
||||
datasourceLayers,
|
||||
paletteService,
|
||||
attributes,
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
);
|
||||
};
|
||||
|
@ -100,6 +103,7 @@ export function toPreviewExpression(
|
|||
state: State,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
paletteService: PaletteRegistry,
|
||||
datasourceExpressionsByLayers: Record<string, Ast>,
|
||||
eventAnnotationService: EventAnnotationServiceType
|
||||
) {
|
||||
return toExpression(
|
||||
|
@ -116,6 +120,7 @@ export function toPreviewExpression(
|
|||
datasourceLayers,
|
||||
paletteService,
|
||||
{},
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
);
|
||||
}
|
||||
|
@ -151,11 +156,13 @@ export const buildExpression = (
|
|||
metadata: Record<string, Record<string, OperationMetadata | null>>,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
paletteService: PaletteRegistry,
|
||||
attributes: Partial<{ title: string; description: string }> = {},
|
||||
datasourceExpressionsByLayers: Record<string, Ast>,
|
||||
eventAnnotationService: EventAnnotationServiceType
|
||||
): Ast | null => {
|
||||
const validDataLayers = getDataLayers(state.layers)
|
||||
.filter((layer): layer is ValidLayer => Boolean(layer.accessors.length))
|
||||
const validDataLayers: ValidXYDataLayerConfig[] = getDataLayers(state.layers)
|
||||
.filter<ValidXYDataLayerConfig>((layer): layer is ValidXYDataLayerConfig =>
|
||||
Boolean(layer.accessors.length)
|
||||
)
|
||||
.map((layer) => ({
|
||||
...layer,
|
||||
accessors: getSortedAccessors(datasourceLayers[layer.layerId], layer),
|
||||
|
@ -188,10 +195,8 @@ export const buildExpression = (
|
|||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'xyVis',
|
||||
function: 'layeredXyVis',
|
||||
arguments: {
|
||||
title: [attributes?.title || ''],
|
||||
description: [attributes?.description || ''],
|
||||
xTitle: [state.xTitle || ''],
|
||||
yTitle: [state.yTitle || ''],
|
||||
yRightTitle: [state.yRightTitle || ''],
|
||||
|
@ -207,18 +212,24 @@ export const buildExpression = (
|
|||
showSingleSeries: state.legend.showSingleSeries
|
||||
? [state.legend.showSingleSeries]
|
||||
: [],
|
||||
position: [state.legend.position],
|
||||
position: !state.legend.isInside ? [state.legend.position] : [],
|
||||
isInside: state.legend.isInside ? [state.legend.isInside] : [],
|
||||
legendSize: state.legend.legendSize ? [state.legend.legendSize] : [],
|
||||
horizontalAlignment: state.legend.horizontalAlignment
|
||||
legendSize:
|
||||
!state.legend.isInside && state.legend.legendSize
|
||||
? [state.legend.legendSize]
|
||||
: [],
|
||||
horizontalAlignment:
|
||||
state.legend.horizontalAlignment && state.legend.isInside
|
||||
? [state.legend.horizontalAlignment]
|
||||
: [],
|
||||
verticalAlignment: state.legend.verticalAlignment
|
||||
verticalAlignment:
|
||||
state.legend.verticalAlignment && state.legend.isInside
|
||||
? [state.legend.verticalAlignment]
|
||||
: [],
|
||||
// ensure that even if the user types more than 5 columns
|
||||
// we will only show 5
|
||||
floatingColumns: state.legend.floatingColumns
|
||||
floatingColumns:
|
||||
state.legend.floatingColumns && state.legend.isInside
|
||||
? [Math.min(5, state.legend.floatingColumns)]
|
||||
: [],
|
||||
maxLines: state.legend.maxLines ? [state.legend.maxLines] : [],
|
||||
|
@ -236,50 +247,8 @@ export const buildExpression = (
|
|||
emphasizeFitting: [state.emphasizeFitting || false],
|
||||
curveType: [state.curveType || 'LINEAR'],
|
||||
fillOpacity: [state.fillOpacity || 0.3],
|
||||
yLeftExtent: [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'axisExtentConfig',
|
||||
arguments: {
|
||||
mode: [state?.yLeftExtent?.mode || 'full'],
|
||||
lowerBound:
|
||||
state?.yLeftExtent?.lowerBound !== undefined
|
||||
? [state?.yLeftExtent?.lowerBound]
|
||||
: [],
|
||||
upperBound:
|
||||
state?.yLeftExtent?.upperBound !== undefined
|
||||
? [state?.yLeftExtent?.upperBound]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
yRightExtent: [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'axisExtentConfig',
|
||||
arguments: {
|
||||
mode: [state?.yRightExtent?.mode || 'full'],
|
||||
lowerBound:
|
||||
state?.yRightExtent?.lowerBound !== undefined
|
||||
? [state?.yRightExtent?.lowerBound]
|
||||
: [],
|
||||
upperBound:
|
||||
state?.yRightExtent?.upperBound !== undefined
|
||||
? [state?.yRightExtent?.upperBound]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
yLeftExtent: [axisExtentConfigToExpression(state.yLeftExtent, validDataLayers)],
|
||||
yRightExtent: [axisExtentConfigToExpression(state.yRightExtent, validDataLayers)],
|
||||
axisTitlesVisibilitySettings: [
|
||||
{
|
||||
type: 'expression',
|
||||
|
@ -353,13 +322,15 @@ export const buildExpression = (
|
|||
layer,
|
||||
datasourceLayers[layer.layerId],
|
||||
metadata,
|
||||
paletteService
|
||||
paletteService,
|
||||
datasourceExpressionsByLayers[layer.layerId]
|
||||
)
|
||||
),
|
||||
...validReferenceLayers.map((layer) =>
|
||||
referenceLineLayerToExpression(
|
||||
layer,
|
||||
datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId]
|
||||
datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId],
|
||||
datasourceExpressionsByLayers[layer.layerId]
|
||||
)
|
||||
),
|
||||
...validAnnotationsLayers.map((layer) =>
|
||||
|
@ -372,25 +343,32 @@ export const buildExpression = (
|
|||
};
|
||||
};
|
||||
|
||||
const buildTableExpression = (datasourceExpression: Ast): ExpressionAstExpression => ({
|
||||
type: 'expression',
|
||||
chain: [{ type: 'function', function: 'kibana', arguments: {} }, ...datasourceExpression.chain],
|
||||
});
|
||||
|
||||
const referenceLineLayerToExpression = (
|
||||
layer: XYReferenceLineLayerConfig,
|
||||
datasourceLayer: DatasourcePublicAPI
|
||||
datasourceLayer: DatasourcePublicAPI,
|
||||
datasourceExpression: Ast
|
||||
): Ast => {
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'referenceLineLayer',
|
||||
function: 'extendedReferenceLineLayer',
|
||||
arguments: {
|
||||
layerId: [layer.layerId],
|
||||
yConfig: layer.yConfig
|
||||
? layer.yConfig.map((yConfig) =>
|
||||
yConfigToExpression(yConfig, defaultReferenceLineColor)
|
||||
extendedYConfigToExpression(yConfig, defaultReferenceLineColor)
|
||||
)
|
||||
: [],
|
||||
accessors: layer.accessors,
|
||||
columnToLabel: [JSON.stringify(getColumnToLabelMap(layer, datasourceLayer))],
|
||||
...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -406,7 +384,7 @@ const annotationLayerToExpression = (
|
|||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'annotationLayer',
|
||||
function: 'extendedAnnotationLayer',
|
||||
arguments: {
|
||||
hide: [Boolean(layer.hide)],
|
||||
layerId: [layer.layerId],
|
||||
|
@ -420,10 +398,11 @@ const annotationLayerToExpression = (
|
|||
};
|
||||
|
||||
const dataLayerToExpression = (
|
||||
layer: ValidLayer,
|
||||
layer: ValidXYDataLayerConfig,
|
||||
datasourceLayer: DatasourcePublicAPI,
|
||||
metadata: Record<string, Record<string, OperationMetadata | null>>,
|
||||
paletteService: PaletteRegistry
|
||||
paletteService: PaletteRegistry,
|
||||
datasourceExpression: Ast
|
||||
): Ast => {
|
||||
const columnToLabel = getColumnToLabelMap(layer, datasourceLayer);
|
||||
|
||||
|
@ -441,7 +420,7 @@ const dataLayerToExpression = (
|
|||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'dataLayer',
|
||||
function: 'extendedDataLayer',
|
||||
arguments: {
|
||||
layerId: [layer.layerId],
|
||||
hide: [Boolean(layer.hide)],
|
||||
|
@ -458,6 +437,7 @@ const dataLayerToExpression = (
|
|||
seriesType: [layer.seriesType],
|
||||
accessors: layer.accessors,
|
||||
columnToLabel: [JSON.stringify(columnToLabel)],
|
||||
...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}),
|
||||
palette: [
|
||||
{
|
||||
type: 'expression',
|
||||
|
@ -496,6 +476,23 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => {
|
|||
{
|
||||
type: 'function',
|
||||
function: 'yConfig',
|
||||
arguments: {
|
||||
forAccessor: [yConfig.forAccessor],
|
||||
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
|
||||
color: yConfig.color ? [yConfig.color] : defaultColor ? [defaultColor] : [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const extendedYConfigToExpression = (yConfig: ExtendedYConfig, defaultColor?: string): Ast => {
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'extendedYConfig',
|
||||
arguments: {
|
||||
forAccessor: [yConfig.forAccessor],
|
||||
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
|
||||
|
@ -514,3 +511,21 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => {
|
|||
],
|
||||
};
|
||||
};
|
||||
|
||||
const axisExtentConfigToExpression = (
|
||||
extent: AxisExtentConfig | undefined,
|
||||
layers: ValidXYDataLayerConfig[]
|
||||
): Ast => ({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'axisExtentConfig',
|
||||
arguments: {
|
||||
mode: [extent?.mode ?? 'full'],
|
||||
lowerBound: extent?.lowerBound !== undefined ? [extent?.lowerBound] : [],
|
||||
upperBound: extent?.upperBound !== undefined ? [extent?.upperBound] : [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -16,7 +16,10 @@ import type {
|
|||
FittingFunction,
|
||||
LabelsOrientationConfig,
|
||||
EndValue,
|
||||
ExtendedYConfig,
|
||||
YConfig,
|
||||
YScaleType,
|
||||
XScaleType,
|
||||
} from '@kbn/expression-xy-plugin/common';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { LensIconChartArea } from '../assets/chart_area';
|
||||
|
@ -43,12 +46,16 @@ export interface XYDataLayerConfig {
|
|||
yConfig?: YConfig[];
|
||||
splitAccessor?: string;
|
||||
palette?: PaletteOutput;
|
||||
yScaleType?: YScaleType;
|
||||
xScaleType?: XScaleType;
|
||||
isHistogram?: boolean;
|
||||
columnToLabel?: string;
|
||||
}
|
||||
|
||||
export interface XYReferenceLineLayerConfig {
|
||||
layerId: string;
|
||||
accessors: string[];
|
||||
yConfig?: YConfig[];
|
||||
yConfig?: ExtendedYConfig[];
|
||||
layerType: 'referenceLine';
|
||||
}
|
||||
|
||||
|
@ -64,6 +71,13 @@ export type XYLayerConfig =
|
|||
| XYReferenceLineLayerConfig
|
||||
| XYAnnotationLayerConfig;
|
||||
|
||||
export interface ValidXYDataLayerConfig extends XYDataLayerConfig {
|
||||
xAccessor: NonNullable<XYDataLayerConfig['xAccessor']>;
|
||||
layerId: string;
|
||||
}
|
||||
|
||||
export type ValidLayer = ValidXYDataLayerConfig | XYReferenceLineLayerConfig;
|
||||
|
||||
// Persisted parts of the state
|
||||
export interface XYState {
|
||||
preferredSeriesType: SeriesType;
|
||||
|
|
|
@ -16,7 +16,12 @@ import { ThemeServiceStart } from '@kbn/core/public';
|
|||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
||||
import { FillStyle, SeriesType, YAxisMode, YConfig } from '@kbn/expression-xy-plugin/common';
|
||||
import {
|
||||
FillStyle,
|
||||
SeriesType,
|
||||
YAxisMode,
|
||||
ExtendedYConfig,
|
||||
} from '@kbn/expression-xy-plugin/common';
|
||||
import { getSuggestions } from './xy_suggestions';
|
||||
import { XyToolbar } from './xy_config_panel';
|
||||
import { DimensionEditor } from './xy_config_panel/dimension_editor';
|
||||
|
@ -61,7 +66,7 @@ import {
|
|||
} from './visualization_helpers';
|
||||
import { groupAxesByType } from './axes_configuration';
|
||||
import { XYState } from './types';
|
||||
import { ReferenceLinePanel } from './xy_config_panel/reference_line_panel';
|
||||
import { ReferenceLinePanel } from './xy_config_panel/reference_line_config_panel';
|
||||
import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel';
|
||||
import { DimensionTrigger } from '../shared_components/dimension_trigger';
|
||||
import { defaultAnnotationLabel } from './annotations/helpers';
|
||||
|
@ -295,6 +300,7 @@ export const getXyVisualization = ({
|
|||
|
||||
setDimension(props) {
|
||||
const { prevState, layerId, columnId, groupId } = props;
|
||||
|
||||
const foundLayer: XYLayerConfig | undefined = prevState.layers.find(
|
||||
(l) => l.layerId === layerId
|
||||
);
|
||||
|
@ -333,7 +339,7 @@ export const getXyVisualization = ({
|
|||
}
|
||||
const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value');
|
||||
const axisMode = axisPosition as YAxisMode;
|
||||
const yConfig = metrics.map<YConfig>((metric, idx) => {
|
||||
const yConfig = metrics.map<ExtendedYConfig>((metric, idx) => {
|
||||
return {
|
||||
color: metric.color,
|
||||
forAccessor: metric.accessor ?? foundLayer.accessors[idx],
|
||||
|
@ -444,7 +450,7 @@ export const getXyVisualization = ({
|
|||
const groupsAvailable = getGroupsAvailableInData(
|
||||
getDataLayers(prevState.layers),
|
||||
frame.datasourceLayers,
|
||||
frame?.activeData
|
||||
frame.activeData
|
||||
);
|
||||
|
||||
if (
|
||||
|
@ -508,10 +514,26 @@ export const getXyVisualization = ({
|
|||
);
|
||||
},
|
||||
|
||||
toExpression: (state, layers, attributes) =>
|
||||
toExpression(state, layers, paletteService, attributes, eventAnnotationService),
|
||||
toPreviewExpression: (state, layers) =>
|
||||
toPreviewExpression(state, layers, paletteService, eventAnnotationService),
|
||||
shouldBuildDatasourceExpressionManually: () => true,
|
||||
|
||||
toExpression: (state, layers, attributes, datasourceExpressionsByLayers = {}) =>
|
||||
toExpression(
|
||||
state,
|
||||
layers,
|
||||
paletteService,
|
||||
attributes,
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
),
|
||||
|
||||
toPreviewExpression: (state, layers, datasourceExpressionsByLayers = {}) =>
|
||||
toPreviewExpression(
|
||||
state,
|
||||
layers,
|
||||
paletteService,
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
),
|
||||
|
||||
getErrorMessages(state, datasourceLayers) {
|
||||
// Data error handling below here
|
||||
|
@ -592,10 +614,12 @@ export const getXyVisualization = ({
|
|||
...getDataLayers(state.layers),
|
||||
...getReferenceLayers(state.layers),
|
||||
].filter(({ accessors }) => accessors.length > 0);
|
||||
|
||||
const accessorsWithArrayValues = [];
|
||||
|
||||
for (const layer of filteredLayers) {
|
||||
const { layerId, accessors } = layer;
|
||||
const rows = frame.activeData[layerId] && frame.activeData[layerId].rows;
|
||||
const rows = frame.activeData?.[layerId] && frame.activeData[layerId].rows;
|
||||
if (!rows) {
|
||||
break;
|
||||
}
|
||||
|
@ -607,6 +631,7 @@ export const getXyVisualization = ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accessorsWithArrayValues.map((label) => (
|
||||
<FormattedMessage
|
||||
key={label}
|
||||
|
|
|
@ -0,0 +1,505 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import './index.scss';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiDatePicker,
|
||||
EuiFormRow,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiButtonGroup,
|
||||
EuiFormLabel,
|
||||
EuiFormControlLayout,
|
||||
EuiText,
|
||||
transparentize,
|
||||
} from '@elastic/eui';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/common/types';
|
||||
import { pick } from 'lodash';
|
||||
import { search } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import Color from 'color';
|
||||
import { getDataLayers } from '../../visualization_helpers';
|
||||
import { FormatFactory } from '../../../../common';
|
||||
import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components';
|
||||
import { isHorizontalChart } from '../../state_helpers';
|
||||
import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers';
|
||||
import { ColorPicker } from '../color_picker';
|
||||
import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings';
|
||||
import { LineStyleSettings } from '../shared/line_style_settings';
|
||||
import { updateLayer } from '..';
|
||||
import { annotationsIconSet } from './icon_set';
|
||||
import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types';
|
||||
import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types';
|
||||
|
||||
export const toRangeAnnotationColor = (color = defaultAnnotationColor) => {
|
||||
return new Color(transparentize(color, 0.1)).hexa();
|
||||
};
|
||||
|
||||
export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => {
|
||||
return new Color(transparentize(color, 1)).hex();
|
||||
};
|
||||
|
||||
export const getEndTimestamp = (
|
||||
startTime: string,
|
||||
{ activeData, dateRange }: FramePublicAPI,
|
||||
dataLayers: XYDataLayerConfig[]
|
||||
) => {
|
||||
const startTimeNumber = moment(startTime).valueOf();
|
||||
const dateRangeFraction =
|
||||
(moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1;
|
||||
const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString();
|
||||
const dataLayersId = dataLayers.map(({ layerId }) => layerId);
|
||||
if (
|
||||
!dataLayersId.length ||
|
||||
!activeData ||
|
||||
Object.entries(activeData)
|
||||
.filter(([key]) => dataLayersId.includes(key))
|
||||
.every(([, { rows }]) => !rows || !rows.length)
|
||||
) {
|
||||
return fallbackValue;
|
||||
}
|
||||
const xColumn = activeData?.[dataLayersId[0]].columns.find(
|
||||
(column) => column.id === dataLayers[0].xAccessor
|
||||
);
|
||||
if (!xColumn) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval;
|
||||
if (!dateInterval) return fallbackValue;
|
||||
const intervalDuration = search.aggs.parseInterval(dateInterval);
|
||||
if (!intervalDuration) return fallbackValue;
|
||||
return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString();
|
||||
};
|
||||
|
||||
const sanitizeProperties = (annotation: EventAnnotationConfig) => {
|
||||
if (isRangeAnnotation(annotation)) {
|
||||
const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [
|
||||
'label',
|
||||
'key',
|
||||
'id',
|
||||
'isHidden',
|
||||
'color',
|
||||
'outside',
|
||||
]);
|
||||
return rangeAnnotation;
|
||||
} else {
|
||||
const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [
|
||||
'id',
|
||||
'label',
|
||||
'key',
|
||||
'isHidden',
|
||||
'lineStyle',
|
||||
'lineWidth',
|
||||
'color',
|
||||
'icon',
|
||||
'textVisibility',
|
||||
]);
|
||||
return lineAnnotation;
|
||||
}
|
||||
};
|
||||
|
||||
export const AnnotationsPanel = (
|
||||
props: VisualizationDimensionEditorProps<State> & {
|
||||
formatFactory: FormatFactory;
|
||||
paletteService: PaletteRegistry;
|
||||
}
|
||||
) => {
|
||||
const { state, setState, layerId, accessor, frame } = props;
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
|
||||
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
|
||||
value: state,
|
||||
onChange: setState,
|
||||
});
|
||||
|
||||
const index = localState.layers.findIndex((l) => l.layerId === layerId);
|
||||
const localLayer = localState.layers.find(
|
||||
(l) => l.layerId === layerId
|
||||
) as XYAnnotationLayerConfig;
|
||||
|
||||
const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor);
|
||||
|
||||
const isRange = isRangeAnnotation(currentAnnotation);
|
||||
|
||||
const setAnnotations = useCallback(
|
||||
(annotation) => {
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
const newConfigs = [...(localLayer.annotations || [])];
|
||||
const existingIndex = newConfigs.findIndex((c) => c.id === accessor);
|
||||
if (existingIndex !== -1) {
|
||||
newConfigs[existingIndex] = sanitizeProperties({
|
||||
...newConfigs[existingIndex],
|
||||
...annotation,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'should never happen because annotation is created before config panel is opened'
|
||||
);
|
||||
}
|
||||
setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index));
|
||||
},
|
||||
[accessor, index, localState, localLayer, setLocalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DimensionEditorSection
|
||||
title={i18n.translate('xpack.lens.xyChart.placement', {
|
||||
defaultMessage: 'Placement',
|
||||
})}
|
||||
>
|
||||
{isRange ? (
|
||||
<>
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-fromTime"
|
||||
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.from', {
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.timestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
|
||||
if (currentEndTime < date.valueOf()) {
|
||||
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
|
||||
const dif = currentEndTime - currentStartTime;
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
timestamp: date.toISOString(),
|
||||
endTimestamp: moment(date.valueOf() + dif).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
timestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
|
||||
defaultMessage: 'Annotation date',
|
||||
})}
|
||||
/>
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-toTime"
|
||||
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.to', {
|
||||
defaultMessage: 'To',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.endTimestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
|
||||
if (currentStartTime > date.valueOf()) {
|
||||
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
|
||||
const dif = currentEndTime - currentStartTime;
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
endTimestamp: date.toISOString(),
|
||||
timestamp: moment(date.valueOf() - dif).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
endTimestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-time"
|
||||
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
|
||||
defaultMessage: 'Annotation date',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.timestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'point_in_time' }),
|
||||
timestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfigPanelApplyAsRangeSwitch
|
||||
annotation={currentAnnotation}
|
||||
onChange={setAnnotations}
|
||||
frame={frame}
|
||||
state={state}
|
||||
/>
|
||||
</DimensionEditorSection>
|
||||
<DimensionEditorSection
|
||||
title={i18n.translate('xpack.lens.xyChart.appearance', {
|
||||
defaultMessage: 'Appearance',
|
||||
})}
|
||||
>
|
||||
<NameInput
|
||||
value={currentAnnotation?.label || defaultAnnotationLabel}
|
||||
defaultValue={defaultAnnotationLabel}
|
||||
onChange={(value) => {
|
||||
setAnnotations({ label: value });
|
||||
}}
|
||||
/>
|
||||
{!isRange && (
|
||||
<IconSelectSetting
|
||||
setConfig={setAnnotations}
|
||||
defaultIcon="triangle"
|
||||
currentConfig={{
|
||||
axisMode: 'bottom',
|
||||
...currentAnnotation,
|
||||
}}
|
||||
customIconSet={annotationsIconSet}
|
||||
/>
|
||||
)}
|
||||
{!isRange && (
|
||||
<TextDecorationSetting
|
||||
setConfig={setAnnotations}
|
||||
currentConfig={{
|
||||
axisMode: 'bottom',
|
||||
...currentAnnotation,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isRange && (
|
||||
<LineStyleSettings
|
||||
isHorizontal={isHorizontal}
|
||||
setConfig={setAnnotations}
|
||||
currentConfig={currentAnnotation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRange && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.fillStyle', {
|
||||
defaultMessage: 'Fill',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.xyChart.fillStyle', {
|
||||
defaultMessage: 'Fill',
|
||||
})}
|
||||
data-test-subj="lns-xyAnnotation-fillStyle"
|
||||
name="fillStyle"
|
||||
buttonSize="compressed"
|
||||
options={[
|
||||
{
|
||||
id: `lens_xyChart_fillStyle_inside`,
|
||||
label: i18n.translate('xpack.lens.xyChart.fillStyle.inside', {
|
||||
defaultMessage: 'Inside',
|
||||
}),
|
||||
'data-test-subj': 'lnsXY_fillStyle_inside',
|
||||
},
|
||||
{
|
||||
id: `lens_xyChart_fillStyle_outside`,
|
||||
label: i18n.translate('xpack.lens.xyChart.fillStyle.outside', {
|
||||
defaultMessage: 'Outside',
|
||||
}),
|
||||
'data-test-subj': 'lnsXY_fillStyle_inside',
|
||||
},
|
||||
]}
|
||||
idSelected={`lens_xyChart_fillStyle_${
|
||||
Boolean(currentAnnotation?.outside) ? 'outside' : 'inside'
|
||||
}`}
|
||||
onChange={(id) => {
|
||||
setAnnotations({
|
||||
outside: id === `lens_xyChart_fillStyle_outside`,
|
||||
});
|
||||
}}
|
||||
isFullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<ColorPicker
|
||||
{...props}
|
||||
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
|
||||
showAlpha={isRange}
|
||||
setConfig={setAnnotations}
|
||||
disableHelpTooltip
|
||||
label={i18n.translate('xpack.lens.xyChart.lineColor.label', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
/>
|
||||
<ConfigPanelHideSwitch
|
||||
value={Boolean(currentAnnotation?.isHidden)}
|
||||
onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })}
|
||||
/>
|
||||
</DimensionEditorSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelApplyAsRangeSwitch = ({
|
||||
annotation,
|
||||
onChange,
|
||||
frame,
|
||||
state,
|
||||
}: {
|
||||
annotation?: EventAnnotationConfig;
|
||||
onChange: (annotations: Partial<EventAnnotationConfig> | undefined) => void;
|
||||
frame: FramePublicAPI;
|
||||
state: XYState;
|
||||
}) => {
|
||||
const isRange = isRangeAnnotation(annotation);
|
||||
return (
|
||||
<EuiFormRow display="columnCompressed" className="lnsRowCompressedMargin">
|
||||
<EuiSwitch
|
||||
data-test-subj="lns-xyAnnotation-rangeSwitch"
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.xyChart.applyAsRange', {
|
||||
defaultMessage: 'Apply as range',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
checked={isRange}
|
||||
onChange={() => {
|
||||
if (isRange) {
|
||||
const newPointAnnotation: PointInTimeEventAnnotationConfig = {
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: annotation.key.timestamp,
|
||||
},
|
||||
id: annotation.id,
|
||||
label:
|
||||
annotation.label === defaultRangeAnnotationLabel
|
||||
? defaultAnnotationLabel
|
||||
: annotation.label,
|
||||
color: toLineAnnotationColor(annotation.color),
|
||||
isHidden: annotation.isHidden,
|
||||
};
|
||||
onChange(newPointAnnotation);
|
||||
} else if (annotation) {
|
||||
const fromTimestamp = moment(annotation?.key.timestamp);
|
||||
const dataLayers = getDataLayers(state.layers);
|
||||
const newRangeAnnotation: RangeEventAnnotationConfig = {
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: annotation.key.timestamp,
|
||||
endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers),
|
||||
},
|
||||
id: annotation.id,
|
||||
label:
|
||||
annotation.label === defaultAnnotationLabel
|
||||
? defaultRangeAnnotationLabel
|
||||
: annotation.label,
|
||||
color: toRangeAnnotationColor(annotation.color),
|
||||
isHidden: annotation.isHidden,
|
||||
};
|
||||
onChange(newRangeAnnotation);
|
||||
}
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelRangeDatePicker = ({
|
||||
value,
|
||||
label,
|
||||
prependLabel,
|
||||
onChange,
|
||||
dataTestSubj = 'lnsXY_annotation_date_picker',
|
||||
}: {
|
||||
value: moment.Moment;
|
||||
prependLabel?: string;
|
||||
label?: string;
|
||||
onChange: (val: moment.Moment | null) => void;
|
||||
dataTestSubj?: string;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow display="rowCompressed" fullWidth label={label} className="lnsRowCompressedMargin">
|
||||
{prependLabel ? (
|
||||
<EuiFormControlLayout
|
||||
fullWidth
|
||||
className="lnsConfigPanelNoPadding"
|
||||
prepend={
|
||||
<EuiFormLabel className="lnsConfigPanelDate__label">{prependLabel}</EuiFormLabel>
|
||||
}
|
||||
>
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
showTimeSelect
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
) : (
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
showTimeSelect
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelHideSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (event: EuiSwitchEvent) => void;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
|
||||
defaultMessage: 'Hide annotation',
|
||||
})}
|
||||
display="columnCompressedSwitch"
|
||||
fullWidth
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
|
||||
defaultMessage: 'Hide annotation',
|
||||
})}
|
||||
showLabel={false}
|
||||
data-test-subj="lns-annotations-hide-annotation"
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -4,10 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IconTriangle, IconCircle } from '../../../assets/annotation_icons';
|
||||
|
||||
export const annotationsIconSet = [
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AvailableAnnotationIcon } from '@kbn/event-annotation-plugin/common';
|
||||
import { IconTriangle, IconCircle } from '../../../assets/annotation_icons';
|
||||
import { IconSet } from '../shared/icon_select';
|
||||
|
||||
export const annotationsIconSet: IconSet<AvailableAnnotationIcon> = [
|
||||
{
|
||||
value: 'asterisk',
|
||||
label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', {
|
||||
|
|
|
@ -29,7 +29,7 @@ const customLineStaticAnnotation = {
|
|||
id: 'ann1',
|
||||
key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' },
|
||||
label: 'Event',
|
||||
icon: 'triangle',
|
||||
icon: 'triangle' as const,
|
||||
color: 'red',
|
||||
lineStyle: 'dashed' as const,
|
||||
lineWidth: 3,
|
||||
|
|
|
@ -5,501 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import './index.scss';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import {
|
||||
EuiDatePicker,
|
||||
EuiFormRow,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiButtonGroup,
|
||||
EuiFormLabel,
|
||||
EuiFormControlLayout,
|
||||
EuiText,
|
||||
transparentize,
|
||||
} from '@elastic/eui';
|
||||
import { pick } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/common/types';
|
||||
import { search } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import Color from 'color';
|
||||
import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types';
|
||||
import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types';
|
||||
import { FormatFactory } from '../../../../common';
|
||||
import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components';
|
||||
import { isHorizontalChart } from '../../state_helpers';
|
||||
import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers';
|
||||
import { ColorPicker } from '../color_picker';
|
||||
import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings';
|
||||
import { LineStyleSettings } from '../shared/line_style_settings';
|
||||
import { updateLayer } from '..';
|
||||
import { annotationsIconSet } from './icon_set';
|
||||
import { getDataLayers } from '../../visualization_helpers';
|
||||
|
||||
export const toRangeAnnotationColor = (color = defaultAnnotationColor) => {
|
||||
return new Color(transparentize(color, 0.1)).hexa();
|
||||
};
|
||||
|
||||
export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => {
|
||||
return new Color(transparentize(color, 1)).hex();
|
||||
};
|
||||
|
||||
export const getEndTimestamp = (
|
||||
startTime: string,
|
||||
{ activeData, dateRange }: FramePublicAPI,
|
||||
dataLayers: XYDataLayerConfig[]
|
||||
) => {
|
||||
const startTimeNumber = moment(startTime).valueOf();
|
||||
const dateRangeFraction =
|
||||
(moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1;
|
||||
const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString();
|
||||
const dataLayersId = dataLayers.map(({ layerId }) => layerId);
|
||||
if (
|
||||
!dataLayersId.length ||
|
||||
!activeData ||
|
||||
Object.entries(activeData)
|
||||
.filter(([key]) => dataLayersId.includes(key))
|
||||
.every(([, { rows }]) => !rows || !rows.length)
|
||||
) {
|
||||
return fallbackValue;
|
||||
}
|
||||
const xColumn = activeData?.[dataLayersId[0]].columns.find(
|
||||
(column) => column.id === dataLayers[0].xAccessor
|
||||
);
|
||||
if (!xColumn) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval;
|
||||
if (!dateInterval) return fallbackValue;
|
||||
const intervalDuration = search.aggs.parseInterval(dateInterval);
|
||||
if (!intervalDuration) return fallbackValue;
|
||||
return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString();
|
||||
};
|
||||
|
||||
const sanitizeProperties = (annotation: EventAnnotationConfig) => {
|
||||
if (isRangeAnnotation(annotation)) {
|
||||
const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [
|
||||
'label',
|
||||
'key',
|
||||
'id',
|
||||
'isHidden',
|
||||
'color',
|
||||
'outside',
|
||||
]);
|
||||
return rangeAnnotation;
|
||||
} else {
|
||||
const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [
|
||||
'id',
|
||||
'label',
|
||||
'key',
|
||||
'isHidden',
|
||||
'lineStyle',
|
||||
'lineWidth',
|
||||
'color',
|
||||
'icon',
|
||||
'textVisibility',
|
||||
]);
|
||||
return lineAnnotation;
|
||||
}
|
||||
};
|
||||
|
||||
export const AnnotationsPanel = (
|
||||
props: VisualizationDimensionEditorProps<State> & {
|
||||
formatFactory: FormatFactory;
|
||||
paletteService: PaletteRegistry;
|
||||
}
|
||||
) => {
|
||||
const { state, setState, layerId, accessor, frame } = props;
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
|
||||
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
|
||||
value: state,
|
||||
onChange: setState,
|
||||
});
|
||||
|
||||
const index = localState.layers.findIndex((l) => l.layerId === layerId);
|
||||
const localLayer = localState.layers.find(
|
||||
(l) => l.layerId === layerId
|
||||
) as XYAnnotationLayerConfig;
|
||||
|
||||
const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor);
|
||||
|
||||
const isRange = isRangeAnnotation(currentAnnotation);
|
||||
|
||||
const setAnnotations = useCallback(
|
||||
(annotation) => {
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
const newConfigs = [...(localLayer.annotations || [])];
|
||||
const existingIndex = newConfigs.findIndex((c) => c.id === accessor);
|
||||
if (existingIndex !== -1) {
|
||||
newConfigs[existingIndex] = sanitizeProperties({
|
||||
...newConfigs[existingIndex],
|
||||
...annotation,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'should never happen because annotation is created before config panel is opened'
|
||||
);
|
||||
}
|
||||
setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index));
|
||||
},
|
||||
[accessor, index, localState, localLayer, setLocalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DimensionEditorSection
|
||||
title={i18n.translate('xpack.lens.xyChart.placement', {
|
||||
defaultMessage: 'Placement',
|
||||
})}
|
||||
>
|
||||
{isRange ? (
|
||||
<>
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-fromTime"
|
||||
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.from', {
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.timestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
|
||||
if (currentEndTime < date.valueOf()) {
|
||||
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
|
||||
const dif = currentEndTime - currentStartTime;
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
timestamp: date.toISOString(),
|
||||
endTimestamp: moment(date.valueOf() + dif).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
timestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
|
||||
defaultMessage: 'Annotation date',
|
||||
})}
|
||||
/>
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-toTime"
|
||||
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.to', {
|
||||
defaultMessage: 'To',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.endTimestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
|
||||
if (currentStartTime > date.valueOf()) {
|
||||
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
|
||||
const dif = currentEndTime - currentStartTime;
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
endTimestamp: date.toISOString(),
|
||||
timestamp: moment(date.valueOf() - dif).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'range' }),
|
||||
endTimestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-time"
|
||||
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
|
||||
defaultMessage: 'Annotation date',
|
||||
})}
|
||||
value={moment(currentAnnotation?.key.timestamp)}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
setAnnotations({
|
||||
key: {
|
||||
...(currentAnnotation?.key || { type: 'point_in_time' }),
|
||||
timestamp: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfigPanelApplyAsRangeSwitch
|
||||
annotation={currentAnnotation}
|
||||
onChange={setAnnotations}
|
||||
frame={frame}
|
||||
state={state}
|
||||
/>
|
||||
</DimensionEditorSection>
|
||||
<DimensionEditorSection
|
||||
title={i18n.translate('xpack.lens.xyChart.appearance', {
|
||||
defaultMessage: 'Appearance',
|
||||
})}
|
||||
>
|
||||
<NameInput
|
||||
value={currentAnnotation?.label || defaultAnnotationLabel}
|
||||
defaultValue={defaultAnnotationLabel}
|
||||
onChange={(value) => {
|
||||
setAnnotations({ label: value });
|
||||
}}
|
||||
/>
|
||||
{!isRange && (
|
||||
<IconSelectSetting
|
||||
setConfig={setAnnotations}
|
||||
defaultIcon="triangle"
|
||||
currentConfig={{
|
||||
axisMode: 'bottom',
|
||||
...currentAnnotation,
|
||||
}}
|
||||
customIconSet={annotationsIconSet}
|
||||
/>
|
||||
)}
|
||||
{!isRange && (
|
||||
<TextDecorationSetting
|
||||
setConfig={setAnnotations}
|
||||
currentConfig={{
|
||||
axisMode: 'bottom',
|
||||
...currentAnnotation,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isRange && (
|
||||
<LineStyleSettings
|
||||
isHorizontal={isHorizontal}
|
||||
setConfig={setAnnotations}
|
||||
currentConfig={currentAnnotation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRange && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.fillStyle', {
|
||||
defaultMessage: 'Fill',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.xyChart.fillStyle', {
|
||||
defaultMessage: 'Fill',
|
||||
})}
|
||||
data-test-subj="lns-xyAnnotation-fillStyle"
|
||||
name="fillStyle"
|
||||
buttonSize="compressed"
|
||||
options={[
|
||||
{
|
||||
id: `lens_xyChart_fillStyle_inside`,
|
||||
label: i18n.translate('xpack.lens.xyChart.fillStyle.inside', {
|
||||
defaultMessage: 'Inside',
|
||||
}),
|
||||
'data-test-subj': 'lnsXY_fillStyle_inside',
|
||||
},
|
||||
{
|
||||
id: `lens_xyChart_fillStyle_outside`,
|
||||
label: i18n.translate('xpack.lens.xyChart.fillStyle.outside', {
|
||||
defaultMessage: 'Outside',
|
||||
}),
|
||||
'data-test-subj': 'lnsXY_fillStyle_inside',
|
||||
},
|
||||
]}
|
||||
idSelected={`lens_xyChart_fillStyle_${
|
||||
Boolean(currentAnnotation?.outside) ? 'outside' : 'inside'
|
||||
}`}
|
||||
onChange={(id) => {
|
||||
setAnnotations({
|
||||
outside: id === `lens_xyChart_fillStyle_outside`,
|
||||
});
|
||||
}}
|
||||
isFullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<ColorPicker
|
||||
{...props}
|
||||
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
|
||||
showAlpha={isRange}
|
||||
setConfig={setAnnotations}
|
||||
disableHelpTooltip
|
||||
label={i18n.translate('xpack.lens.xyChart.lineColor.label', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
/>
|
||||
<ConfigPanelHideSwitch
|
||||
value={Boolean(currentAnnotation?.isHidden)}
|
||||
onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })}
|
||||
/>
|
||||
</DimensionEditorSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelApplyAsRangeSwitch = ({
|
||||
annotation,
|
||||
onChange,
|
||||
frame,
|
||||
state,
|
||||
}: {
|
||||
annotation?: EventAnnotationConfig;
|
||||
onChange: (annotations: Partial<EventAnnotationConfig> | undefined) => void;
|
||||
frame: FramePublicAPI;
|
||||
state: XYState;
|
||||
}) => {
|
||||
const isRange = isRangeAnnotation(annotation);
|
||||
return (
|
||||
<EuiFormRow display="columnCompressed" className="lnsRowCompressedMargin">
|
||||
<EuiSwitch
|
||||
data-test-subj="lns-xyAnnotation-rangeSwitch"
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.xyChart.applyAsRange', {
|
||||
defaultMessage: 'Apply as range',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
checked={isRange}
|
||||
onChange={() => {
|
||||
if (isRange) {
|
||||
const newPointAnnotation: PointInTimeEventAnnotationConfig = {
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: annotation.key.timestamp,
|
||||
},
|
||||
id: annotation.id,
|
||||
label:
|
||||
annotation.label === defaultRangeAnnotationLabel
|
||||
? defaultAnnotationLabel
|
||||
: annotation.label,
|
||||
color: toLineAnnotationColor(annotation.color),
|
||||
isHidden: annotation.isHidden,
|
||||
};
|
||||
onChange(newPointAnnotation);
|
||||
} else if (annotation) {
|
||||
const fromTimestamp = moment(annotation?.key.timestamp);
|
||||
const dataLayers = getDataLayers(state.layers);
|
||||
const newRangeAnnotation: RangeEventAnnotationConfig = {
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: annotation.key.timestamp,
|
||||
endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers),
|
||||
},
|
||||
id: annotation.id,
|
||||
label:
|
||||
annotation.label === defaultAnnotationLabel
|
||||
? defaultRangeAnnotationLabel
|
||||
: annotation.label,
|
||||
color: toRangeAnnotationColor(annotation.color),
|
||||
isHidden: annotation.isHidden,
|
||||
};
|
||||
onChange(newRangeAnnotation);
|
||||
}
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelRangeDatePicker = ({
|
||||
value,
|
||||
label,
|
||||
prependLabel,
|
||||
onChange,
|
||||
dataTestSubj = 'lnsXY_annotation_date_picker',
|
||||
}: {
|
||||
value: moment.Moment;
|
||||
prependLabel?: string;
|
||||
label?: string;
|
||||
onChange: (val: moment.Moment | null) => void;
|
||||
dataTestSubj?: string;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow display="rowCompressed" fullWidth label={label} className="lnsRowCompressedMargin">
|
||||
{prependLabel ? (
|
||||
<EuiFormControlLayout
|
||||
fullWidth
|
||||
className="lnsConfigPanelNoPadding"
|
||||
prepend={
|
||||
<EuiFormLabel className="lnsConfigPanelDate__label">{prependLabel}</EuiFormLabel>
|
||||
}
|
||||
>
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
showTimeSelect
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
) : (
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
showTimeSelect
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigPanelHideSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (event: EuiSwitchEvent) => void;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
|
||||
defaultMessage: 'Hide annotation',
|
||||
})}
|
||||
display="columnCompressedSwitch"
|
||||
fullWidth
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
|
||||
defaultMessage: 'Hide annotation',
|
||||
})}
|
||||
showLabel={false}
|
||||
data-test-subj="lns-annotations-hide-annotation"
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
export { AnnotationsPanel } from './annotations_panel';
|
||||
|
|
|
@ -76,10 +76,9 @@ export const ColorPicker = ({
|
|||
frame.datasourceLayers[layer.layerId] ?? layer.accessors,
|
||||
layer
|
||||
);
|
||||
|
||||
const colorAssignments = getColorAssignments(
|
||||
getDataLayers(state.layers),
|
||||
{ tables: frame.activeData },
|
||||
{ tables: frame.activeData ?? {} },
|
||||
formatFactory
|
||||
);
|
||||
const mappedAccessors = getAccessorColorConfig(
|
||||
|
@ -91,7 +90,6 @@ export const ColorPicker = ({
|
|||
},
|
||||
paletteService
|
||||
);
|
||||
|
||||
return mappedAccessors.find((a) => a.columnId === accessor)?.color || null;
|
||||
}
|
||||
}, [
|
||||
|
@ -105,12 +103,12 @@ export const ColorPicker = ({
|
|||
defaultColor,
|
||||
]);
|
||||
|
||||
const [color, setColor] = useState(currentColor);
|
||||
|
||||
useEffect(() => {
|
||||
setColor(currentColor);
|
||||
}, [currentColor]);
|
||||
|
||||
const [color, setColor] = useState(currentColor);
|
||||
|
||||
const handleColor: EuiColorPickerProps['onChange'] = (text, output) => {
|
||||
setColor(text);
|
||||
if (output.isValid || text === '') {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue