[Lens] Multiple y axes (#69911)

This commit is contained in:
Joe Reuter 2020-07-01 09:57:23 +02:00 committed by GitHub
parent c1dc53c6fb
commit e1665e8b27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1141 additions and 186 deletions

View file

@ -186,7 +186,7 @@ export function LayerPanel(
},
];
if (activeVisualization.renderDimensionEditor) {
if (activeVisualization.renderDimensionEditor && group.enableDimensionEditor) {
tabs.push({
id: 'visualization',
name: i18n.translate('xpack.lens.editorFrame.formatStyleLabel', {

View file

@ -38,6 +38,7 @@ Object {
"xScaleType": Array [
"linear",
],
"yConfig": Array [],
"yScaleType": Array [
"linear",
],

View file

@ -12,6 +12,11 @@ exports[`xy_expression XYChart component it renders area 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -21,11 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="left"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -45,8 +52,9 @@ exports[`xy_expression XYChart component it renders area 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -60,6 +68,43 @@ exports[`xy_expression XYChart component it renders area 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={Array []}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -80,6 +125,11 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -89,11 +139,13 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="left"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -113,8 +165,9 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -128,6 +181,43 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={Array []}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -148,6 +238,11 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -157,11 +252,13 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="bottom"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -181,8 +278,9 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -196,6 +294,43 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={Array []}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -216,6 +351,11 @@ exports[`xy_expression XYChart component it renders line 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -225,11 +365,13 @@ exports[`xy_expression XYChart component it renders line 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="left"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -249,8 +391,9 @@ exports[`xy_expression XYChart component it renders line 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -264,6 +407,43 @@ exports[`xy_expression XYChart component it renders line 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={Array []}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -284,6 +464,11 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -293,11 +478,13 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="left"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -317,8 +504,9 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -336,6 +524,47 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={
Array [
"c",
]
}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -356,6 +585,11 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -365,11 +599,13 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="left"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -389,8 +625,9 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -408,6 +645,47 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={
Array [
"c",
]
}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}
@ -428,6 +706,11 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
showLegend={false}
showLegendExtra={false}
theme={Object {}}
tooltip={
Object {
"headerFormatter": [Function],
}
}
/>
<Connect(SpecInstance)
id="x"
@ -437,11 +720,13 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
title="c"
/>
<Connect(SpecInstance)
id="y"
groupId="left"
id="left"
key="left"
position="bottom"
showGridLines={false}
tickFormat={[Function]}
title=""
title="a"
/>
<Connect(SpecInstance)
data={
@ -461,8 +746,9 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
]
}
enableHistogramMode={false}
id="d"
key="0"
groupId="left"
id="d-a"
key="0-0"
name={[Function]}
splitSeriesAccessors={
Array [
@ -480,6 +766,47 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
yAccessors={
Array [
"a",
]
}
yScaleType="linear"
/>
<Connect(SpecInstance)
data={
Array [
Object {
"a": 1,
"b": 2,
"c": "I",
"d": "Foo",
},
Object {
"a": 1,
"b": 5,
"c": "J",
"d": "Bar",
},
]
}
enableHistogramMode={false}
groupId="left"
id="d-b"
key="0-1"
name={[Function]}
splitSeriesAccessors={
Array [
"d",
]
}
stackAccessors={
Array [
"c",
]
}
timeZone="UTC"
xAccessor="c"
xScaleType="ordinal"
yAccessors={
Array [
"b",
]
}

View file

@ -0,0 +1,295 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LayerArgs } from './types';
import { KibanaDatatable } from '../../../../../src/plugins/expressions/public';
import { getAxesConfiguration } from './axes_configuration';
describe('axes_configuration', () => {
const tables: Record<string, KibanaDatatable> = {
first: {
type: 'kibana_datatable',
rows: [
{
xAccessorId: 1585758120000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585758360000,
splitAccessorId: "Women's Accessories",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585758360000,
splitAccessorId: "Women's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Women's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585760700000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585760760000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585760760000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
{
xAccessorId: 1585761120000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
yAccessorId2: 1,
yAccessorId3: 1,
yAccessorId4: 4,
},
],
columns: [
{
id: 'xAccessorId',
name: 'order_date per minute',
meta: {
type: 'date_histogram',
indexPatternId: 'indexPatternId',
aggConfigParams: {
field: 'order_date',
timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' },
useNormalizedEsInterval: true,
scaleMetricValues: false,
interval: '1m',
drop_partials: false,
min_doc_count: 0,
extended_bounds: {},
},
},
formatHint: { id: 'date', params: { pattern: 'HH:mm' } },
},
{
id: 'splitAccessorId',
name: 'Top values of category.keyword',
meta: {
type: 'terms',
indexPatternId: 'indexPatternId',
aggConfigParams: {
field: 'category.keyword',
orderBy: 'yAccessorId',
order: 'desc',
size: 3,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
},
formatHint: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
parsedUrl: {
origin: 'http://localhost:5601',
pathname: '/jiy/app/kibana',
basePath: '/jiy',
},
},
},
},
{
id: 'yAccessorId',
name: 'Count of records',
meta: {
type: 'count',
indexPatternId: 'indexPatternId',
aggConfigParams: {},
},
formatHint: { id: 'number' },
},
{
id: 'yAccessorId2',
name: 'Other column',
meta: {
type: 'average',
indexPatternId: 'indexPatternId',
aggConfigParams: {},
},
formatHint: { id: 'bytes' },
},
{
id: 'yAccessorId3',
name: 'Other column',
meta: {
type: 'average',
indexPatternId: 'indexPatternId',
aggConfigParams: {},
},
formatHint: { id: 'currency' },
},
{
id: 'yAccessorId4',
name: 'Other column',
meta: {
type: 'average',
indexPatternId: 'indexPatternId',
aggConfigParams: {},
},
formatHint: { id: 'currency' },
},
],
},
};
const sampleLayer: LayerArgs = {
layerId: 'first',
seriesType: 'line',
xAccessor: 'c',
accessors: ['yAccessorId'],
splitAccessor: 'd',
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
xScaleType: 'ordinal',
yScaleType: 'linear',
isHistogram: false,
};
it('should map auto series to left axis', () => {
const formatFactory = jest.fn();
const groups = getAxesConfiguration([sampleLayer], tables, formatFactory, false);
expect(groups.length).toEqual(1);
expect(groups[0].position).toEqual('left');
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
expect(groups[0].series[0].layer).toEqual('first');
});
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], tables, formatFactory, false);
expect(groups.length).toEqual(2);
expect(groups[0].position).toEqual('left');
expect(groups[1].position).toEqual('right');
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
expect(groups[1].series[0].accessor).toEqual('yAccessorId2');
});
it('should map auto series to left if left and right are already filled with non-matching series', () => {
const formatFactory = jest.fn();
const threeSeriesLayer = {
...sampleLayer,
accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'],
};
const groups = getAxesConfiguration([threeSeriesLayer], tables, formatFactory, false);
expect(groups.length).toEqual(2);
expect(groups[0].position).toEqual('left');
expect(groups[1].position).toEqual('right');
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
expect(groups[0].series[1].accessor).toEqual('yAccessorId3');
expect(groups[1].series[0].accessor).toEqual('yAccessorId2');
});
it('should map right series to right axis', () => {
const formatFactory = jest.fn();
const groups = getAxesConfiguration(
[{ ...sampleLayer, yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }] }],
tables,
formatFactory,
false
);
expect(groups.length).toEqual(1);
expect(groups[0].position).toEqual('right');
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
expect(groups[0].series[0].layer).toEqual('first');
});
it('should map series with matching formatters to same axis', () => {
const formatFactory = jest.fn();
const groups = getAxesConfiguration(
[
{
...sampleLayer,
accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'],
yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }],
},
],
tables,
formatFactory,
false
);
expect(groups.length).toEqual(2);
expect(groups[0].position).toEqual('left');
expect(groups[0].series[0].accessor).toEqual('yAccessorId3');
expect(groups[0].series[1].accessor).toEqual('yAccessorId4');
expect(groups[1].position).toEqual('right');
expect(groups[1].series[0].accessor).toEqual('yAccessorId');
expect(formatFactory).toHaveBeenCalledWith({ id: 'number' });
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' });
});
it('should create one formatter per series group', () => {
const formatFactory = jest.fn();
getAxesConfiguration(
[
{
...sampleLayer,
accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'],
yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }],
},
],
tables,
formatFactory,
false
);
expect(formatFactory).toHaveBeenCalledTimes(2);
expect(formatFactory).toHaveBeenCalledWith({ id: 'number' });
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' });
});
});

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LayerConfig } from './types';
import {
KibanaDatatable,
SerializedFieldFormat,
} from '../../../../../src/plugins/expressions/public';
import { IFieldFormat } from '../../../../../src/plugins/data/public';
interface FormattedMetric {
layer: string;
accessor: string;
fieldFormat: SerializedFieldFormat;
}
type GroupsConfiguration = Array<{
groupId: string;
position: 'left' | 'right' | 'bottom' | 'top';
formatter: IFieldFormat;
series: Array<{ layer: string; accessor: string }>;
}>;
export function isFormatterCompatible(
formatter1: SerializedFieldFormat,
formatter2: SerializedFieldFormat
) {
return formatter1.id === formatter2.id;
}
export function getAxesConfiguration(
layers: LayerConfig[],
tables: Record<string, KibanaDatatable>,
formatFactory: (mapping: SerializedFieldFormat) => IFieldFormat,
shouldRotate: boolean
): GroupsConfiguration {
const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = {
auto: [],
left: [],
right: [],
};
layers.forEach((layer) => {
const table = tables[layer.layerId];
layer.accessors.forEach((accessor) => {
const mode =
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode ||
'auto';
const formatter: SerializedFieldFormat = table.columns.find(
(column) => column.id === accessor
)?.formatHint || { id: 'number' };
series[mode].push({
layer: layer.layerId,
accessor,
fieldFormat: formatter,
});
});
});
series.auto.forEach((currentSeries) => {
if (
series.left.length === 0 ||
series.left.every((leftSeries) =>
isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat)
)
) {
series.left.push(currentSeries);
} else if (
series.right.length === 0 ||
series.right.every((rightSeries) =>
isFormatterCompatible(rightSeries.fieldFormat, currentSeries.fieldFormat)
)
) {
series.right.push(currentSeries);
} else if (series.right.length >= series.left.length) {
series.left.push(currentSeries);
} else {
series.right.push(currentSeries);
}
});
const axisGroups: GroupsConfiguration = [];
if (series.left.length > 0) {
axisGroups.push({
groupId: 'left',
position: shouldRotate ? 'bottom' : 'left',
formatter: formatFactory(series.left[0].fieldFormat),
series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries),
});
}
if (series.right.length > 0) {
axisGroups.push({
groupId: 'right',
position: shouldRotate ? 'top' : 'right',
formatter: formatFactory(series.right[0].fieldFormat),
series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries),
});
}
return axisGroups;
}

View file

@ -11,7 +11,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
import { xyVisualization } from './xy_visualization';
import { xyChart, getXyChartRenderer } from './xy_expression';
import { legendConfig, xConfig, layerConfig } from './types';
import { legendConfig, layerConfig, yAxisConfig } from './types';
import { EditorFrameSetup, FormatFactory } from '../types';
export interface XyVisualizationPluginSetupPlugins {
@ -37,7 +37,7 @@ export class XyVisualization {
{ expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => legendConfig);
expressions.registerFunction(() => xConfig);
expressions.registerFunction(() => yAxisConfig);
expressions.registerFunction(() => layerConfig);
expressions.registerFunction(() => xyChart);

View file

@ -179,6 +179,21 @@ export const buildExpression = (
],
isHistogram: [isHistogramDimension],
splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [],
yConfig: layer.yConfig
? layer.yConfig.map((yConfig) => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'lens_xy_yConfig',
arguments: {
forAccessor: [yConfig.forAccessor],
axisMode: [yConfig.axisMode],
},
},
],
}))
: [],
seriesType: [layer.seriesType],
accessors: layer.accessors,
columnToLabel: [JSON.stringify(columnToLabel)],

View file

@ -77,37 +77,33 @@ const axisConfig: { [key in keyof AxisConfig]: ArgumentType<AxisConfig[key]> } =
},
};
export interface YState extends AxisConfig {
accessors: string[];
}
type YConfigResult = YConfig & { type: 'lens_xy_yConfig' };
export interface XConfig extends AxisConfig {
accessor: string;
}
type XConfigResult = XConfig & { type: 'lens_xy_xConfig' };
export const xConfig: ExpressionFunctionDefinition<
'lens_xy_xConfig',
export const yAxisConfig: ExpressionFunctionDefinition<
'lens_xy_yConfig',
null,
XConfig,
XConfigResult
YConfig,
YConfigResult
> = {
name: 'lens_xy_xConfig',
name: 'lens_xy_yConfig',
aliases: [],
type: 'lens_xy_xConfig',
help: `Configure the xy chart's x axis`,
type: 'lens_xy_yConfig',
help: `Configure the behavior of a xy chart's y axis metric`,
inputTypes: ['null'],
args: {
...axisConfig,
accessor: {
forAccessor: {
types: ['string'],
help: 'The column to display on the x axis.',
help: 'The accessor this configuration is for',
},
axisMode: {
types: ['string'],
options: ['auto', 'left', 'right'],
help: 'The axis mode of the metric',
},
},
fn: function fn(input: unknown, args: XConfig) {
fn: function fn(input: unknown, args: YConfig) {
return {
type: 'lens_xy_xConfig',
type: 'lens_xy_yConfig',
...args,
};
},
@ -166,6 +162,12 @@ export const layerConfig: ExpressionFunctionDefinition<
help: 'The columns to display on the y axis.',
multi: true,
},
yConfig: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
types: ['lens_xy_yConfig' as any],
help: 'Additional configuration for y axes',
multi: true,
},
columnToLabel: {
types: ['string'],
help: 'JSON key-value pairs of column ID to label',
@ -188,11 +190,19 @@ export type SeriesType =
| 'bar_horizontal_stacked'
| 'area_stacked';
export type YAxisMode = 'auto' | 'left' | 'right';
export interface YConfig {
forAccessor: string;
axisMode?: YAxisMode;
}
export interface LayerConfig {
hide?: boolean;
layerId: string;
xAccessor?: string;
accessors: string[];
yConfig?: YConfig[];
seriesType: SeriesType;
splitAccessor?: string;
}

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonGroup, EuiFormRow } from '@elastic/eui';
import { State, SeriesType, visualizationTypes } from './types';
import { VisualizationLayerWidgetProps } from '../types';
import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui';
import { State, SeriesType, visualizationTypes, YAxisMode } from './types';
import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types';
import { isHorizontalChart, isHorizontalSeries } from './state_helpers';
import { trackUiEvent } from '../lens_ui_telemetry';
@ -68,3 +67,73 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps<State>) {
</EuiFormRow>
);
}
const idPrefix = htmlIdGenerator()();
export function DimensionEditor({
state,
setState,
layerId,
accessor,
}: VisualizationDimensionEditorProps<State>) {
const index = state.layers.findIndex((l) => l.layerId === layerId);
const layer = state.layers[index];
const axisMode =
(layer.yConfig &&
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) ||
'auto';
return (
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.lens.xyChart.axisSide.label', {
defaultMessage: 'Axis side',
})}
>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.xyChart.axisSide.label', {
defaultMessage: 'Axis side',
})}
name="axisSide"
buttonSize="compressed"
className="eui-displayInlineBlock"
options={[
{
id: `${idPrefix}auto`,
label: i18n.translate('xpack.lens.xyChart.axisSide.auto', {
defaultMessage: 'Auto',
}),
},
{
id: `${idPrefix}left`,
label: i18n.translate('xpack.lens.xyChart.axisSide.left', {
defaultMessage: 'Left',
}),
},
{
id: `${idPrefix}right`,
label: i18n.translate('xpack.lens.xyChart.axisSide.right', {
defaultMessage: 'Right',
}),
},
]}
idSelected={`${idPrefix}${axisMode}`}
onChange={(id) => {
const newMode = id.replace(idPrefix, '') as YAxisMode;
const newYAxisConfigs = [...(layer.yConfig || [])];
const existingIndex = newYAxisConfigs.findIndex(
(yAxisConfig) => yAxisConfig.forAccessor === accessor
);
if (existingIndex !== -1) {
newYAxisConfigs[existingIndex].axisMode = newMode;
} else {
newYAxisConfigs.push({
forAccessor: accessor,
axisMode: newMode,
});
}
setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index));
}}
/>
</EuiFormRow>
);
}

View file

@ -280,6 +280,58 @@ describe('xy_expression', () => {
let getFormatSpy: jest.Mock;
let convertSpy: jest.Mock;
const dataWithoutFormats: LensMultiTable = {
type: 'lens_multitable',
tables: {
first: {
type: 'kibana_datatable',
columns: [
{ id: 'a', name: 'a' },
{ id: 'b', name: 'b' },
{ id: 'c', name: 'c' },
{ id: 'd', name: 'd' },
],
rows: [
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
],
},
},
};
const dataWithFormats: LensMultiTable = {
type: 'lens_multitable',
tables: {
first: {
type: 'kibana_datatable',
columns: [
{ id: 'a', name: 'a' },
{ id: 'b', name: 'b' },
{ id: 'c', name: 'c' },
{ id: 'd', name: 'd', formatHint: { id: 'custom' } },
],
rows: [
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
],
},
},
};
const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => {
return shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
};
beforeEach(() => {
convertSpy = jest.fn((x) => x);
getFormatSpy = jest.fn();
@ -302,7 +354,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(LineSeries)).toHaveLength(1);
expect(component.find(LineSeries)).toHaveLength(2);
expect(component.find(LineSeries).at(0).prop('yAccessors')).toEqual(['a']);
expect(component.find(LineSeries).at(1).prop('yAccessors')).toEqual(['b']);
});
describe('date range', () => {
@ -559,7 +613,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(BarSeries)).toHaveLength(1);
expect(component.find(BarSeries)).toHaveLength(2);
expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']);
expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']);
});
test('it renders area', () => {
@ -577,7 +633,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(AreaSeries)).toHaveLength(1);
expect(component.find(AreaSeries)).toHaveLength(2);
expect(component.find(AreaSeries).at(0).prop('yAccessors')).toEqual(['a']);
expect(component.find(AreaSeries).at(1).prop('yAccessors')).toEqual(['b']);
});
test('it renders horizontal bar', () => {
@ -595,7 +653,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(BarSeries)).toHaveLength(1);
expect(component.find(BarSeries)).toHaveLength(2);
expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']);
expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']);
expect(component.find(Settings).prop('rotation')).toEqual(90);
});
@ -705,8 +765,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(BarSeries)).toHaveLength(1);
expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1);
expect(component.find(BarSeries)).toHaveLength(2);
expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1);
expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1);
});
test('it renders stacked area', () => {
@ -724,8 +785,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(AreaSeries)).toHaveLength(1);
expect(component.find(AreaSeries).prop('stackAccessors')).toHaveLength(1);
expect(component.find(AreaSeries)).toHaveLength(2);
expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toHaveLength(1);
expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toHaveLength(1);
});
test('it renders stacked horizontal bar', () => {
@ -746,8 +808,9 @@ describe('xy_expression', () => {
/>
);
expect(component).toMatchSnapshot();
expect(component.find(BarSeries)).toHaveLength(1);
expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1);
expect(component.find(BarSeries)).toHaveLength(2);
expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1);
expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1);
expect(component.find(Settings).prop('rotation')).toEqual(90);
});
@ -765,7 +828,8 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST');
expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST');
expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST');
});
test('it applies histogram mode to the series for single series', () => {
@ -784,7 +848,8 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true);
});
test('it applies histogram mode to the series for stacked series', () => {
@ -810,7 +875,8 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true);
});
test('it does not apply histogram mode for splitted series', () => {
@ -830,47 +896,104 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false);
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false);
});
describe('y axes', () => {
test('single axis if possible', () => {
const args = createArgsWithLayers();
const component = getRenderedComponent(dataWithoutFormats, args);
const axes = component.find(Axis);
expect(axes).toHaveLength(2);
});
test('multiple axes because of config', () => {
const args = createArgsWithLayers();
const newArgs = {
...args,
layers: [
{
...args.layers[0],
accessors: ['a', 'b'],
yConfig: [
{
forAccessor: 'a',
axisMode: 'left',
},
{
forAccessor: 'b',
axisMode: 'right',
},
],
},
],
} as XYArgs;
const component = getRenderedComponent(dataWithoutFormats, newArgs);
const axes = component.find(Axis);
expect(axes).toHaveLength(3);
expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(
axes.at(1).prop('groupId')
);
expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(
axes.at(2).prop('groupId')
);
});
test('multiple axes because of incompatible formatters', () => {
const args = createArgsWithLayers();
const newArgs = {
...args,
layers: [
{
...args.layers[0],
accessors: ['c', 'd'],
},
],
} as XYArgs;
const component = getRenderedComponent(dataWithFormats, newArgs);
const axes = component.find(Axis);
expect(axes).toHaveLength(3);
expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(
axes.at(1).prop('groupId')
);
expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(
axes.at(2).prop('groupId')
);
});
test('single axis despite different formatters if enforced', () => {
const args = createArgsWithLayers();
const newArgs = {
...args,
layers: [
{
...args.layers[0],
accessors: ['c', 'd'],
yConfig: [
{
forAccessor: 'c',
axisMode: 'left',
},
{
forAccessor: 'd',
axisMode: 'left',
},
],
},
],
} as XYArgs;
const component = getRenderedComponent(dataWithoutFormats, newArgs);
const axes = component.find(Axis);
expect(axes).toHaveLength(2);
});
});
describe('provides correct series naming', () => {
const dataWithoutFormats: LensMultiTable = {
type: 'lens_multitable',
tables: {
first: {
type: 'kibana_datatable',
columns: [
{ id: 'a', name: 'a' },
{ id: 'b', name: 'b' },
{ id: 'c', name: 'c' },
{ id: 'd', name: 'd' },
],
rows: [
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
],
},
},
};
const dataWithFormats: LensMultiTable = {
type: 'lens_multitable',
tables: {
first: {
type: 'kibana_datatable',
columns: [
{ id: 'a', name: 'a' },
{ id: 'b', name: 'b' },
{ id: 'c', name: 'c' },
{ id: 'd', name: 'd', formatHint: { id: 'custom' } },
],
rows: [
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
],
},
},
};
const nameFnArgs = {
seriesKeys: [],
key: '',
@ -879,21 +1002,6 @@ describe('xy_expression', () => {
splitAccessors: new Map(),
};
const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => {
return shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
};
test('simplest xy chart without human-readable name', () => {
const args = createArgsWithLayers();
const newArgs = {
@ -973,13 +1081,14 @@ describe('xy_expression', () => {
};
const component = getRenderedComponent(dataWithoutFormats, newArgs);
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn;
// This accessor has a human-readable name
expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A');
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A');
// This accessor does not
expect(nameFn({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual('');
expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual('');
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual('');
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual('');
});
test('split series without formatting and single y accessor', () => {
@ -1039,9 +1148,13 @@ describe('xy_expression', () => {
};
const component = getRenderedComponent(dataWithoutFormats, newArgs);
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
const nameFn2 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual(
'split1 - Label A'
);
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
'split1 - Label B'
);
});
@ -1061,13 +1174,14 @@ describe('xy_expression', () => {
};
const component = getRenderedComponent(dataWithFormats, newArgs);
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn;
convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2');
expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual(
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual(
'formatted1 - Label A'
);
expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
'formatted2 - Label B'
);
});
@ -1088,7 +1202,8 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal);
expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal);
expect(component.find(LineSeries).at(1).prop('xScaleType')).toEqual(ScaleType.Ordinal);
});
test('it set the scale of the y axis according to the args prop', () => {
@ -1106,7 +1221,8 @@ describe('xy_expression', () => {
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt);
expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt);
expect(component.find(LineSeries).at(1).prop('yScaleType')).toEqual(ScaleType.Sqrt);
});
test('it gets the formatter for the x axis', () => {
@ -1128,25 +1244,6 @@ describe('xy_expression', () => {
expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' });
});
test('it gets a default formatter for y if there are multiple y accessors', () => {
const { data, args } = sampleArgs();
shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
chartTheme={{}}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(getFormatSpy).toHaveBeenCalledWith({ id: 'number' });
});
test('it gets the formatter for the y axis if there is only one accessor', () => {
const { data, args } = sampleArgs();

View file

@ -40,6 +40,7 @@ import { isHorizontalChart } from './state_helpers';
import { parseInterval } from '../../../../../src/plugins/data/common';
import { EmptyPlaceholder } from '../shared_components';
import { desanitizeFilterContext } from '../utils';
import { getAxesConfiguration } from './axes_configuration';
type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T;
type SeriesSpec = InferPropType<typeof LineSeries> &
@ -213,23 +214,19 @@ export function XYChart({
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint);
// use default number formatter for y axis and use formatting hint if there is just a single y column
let yAxisFormatter = formatFactory({ id: 'number' });
if (filteredLayers.length === 1 && filteredLayers[0].accessors.length === 1) {
const firstYAxisColumn = Object.values(data.tables)[0].columns.find(
({ id }) => id === filteredLayers[0].accessors[0]
);
if (firstYAxisColumn && firstYAxisColumn.formatHint) {
yAxisFormatter = formatFactory(firstYAxisColumn.formatHint);
}
}
const chartHasMoreThanOneSeries =
filteredLayers.length > 1 ||
filteredLayers.some((layer) => layer.accessors.length > 1) ||
filteredLayers.some((layer) => layer.splitAccessor);
const shouldRotate = isHorizontalChart(filteredLayers);
const yAxesConfiguration = getAxesConfiguration(
filteredLayers,
data.tables,
formatFactory,
shouldRotate
);
const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle;
function calculateMinInterval() {
@ -279,6 +276,9 @@ export function XYChart({
legendPosition={legend.position}
showLegendExtra={false}
theme={chartTheme}
tooltip={{
headerFormatter: (d) => xAxisFormatter.convert(d.value),
}}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
onBrushEnd={({ x }) => {
@ -368,18 +368,30 @@ export function XYChart({
tickFormat={(d) => xAxisFormatter.convert(d)}
/>
<Axis
id="y"
position={shouldRotate ? Position.Bottom : Position.Left}
title={args.yTitle}
showGridLines={false}
hide={filteredLayers[0].hide}
tickFormat={(d) => yAxisFormatter.convert(d)}
/>
{yAxesConfiguration.map((axis, index) => (
<Axis
key={axis.groupId}
id={axis.groupId}
groupId={axis.groupId}
position={axis.position}
title={
axis.series
.map(
(series) =>
data.tables[series.layer].columns.find((column) => column.id === series.accessor)
?.name
)
.filter((name) => Boolean(name))[0] || args.yTitle
}
showGridLines={false}
hide={filteredLayers[0].hide}
tickFormat={(d) => axis.formatter.convert(d)}
/>
))}
{filteredLayers.map(
(
{
{filteredLayers.flatMap((layer, layerIndex) =>
layer.accessors.map((accessor, accessorIndex) => {
const {
splitAccessor,
seriesType,
accessors,
@ -389,9 +401,7 @@ export function XYChart({
yScaleType,
xScaleType,
isHistogram,
},
index
) => {
} = layer;
const columnToLabelMap: Record<string, string> = columnToLabel
? JSON.parse(columnToLabel)
: {};
@ -407,19 +417,22 @@ export function XYChart({
!(
splitAccessor &&
typeof row[splitAccessor] === 'undefined' &&
accessors.every((accessor) => typeof row[accessor] === 'undefined')
typeof row[accessor] === 'undefined'
)
);
const seriesProps: SeriesSpec = {
splitSeriesAccessors: splitAccessor ? [splitAccessor] : [],
stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [],
id: splitAccessor || accessors.join(','),
id: `${splitAccessor}-${accessor}`,
xAccessor,
yAccessors: accessors,
yAccessors: [accessor],
data: rows,
xScaleType,
yScaleType,
groupId: yAxesConfiguration.find((axisConfiguration) =>
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
)?.groupId,
enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor),
timeZone,
name(d) {
@ -451,6 +464,8 @@ export function XYChart({
},
};
const index = `${layerIndex}-${accessorIndex}`;
switch (seriesType) {
case 'line':
return <LineSeries key={index} {...seriesProps} />;
@ -462,7 +477,7 @@ export function XYChart({
default:
return <AreaSeries key={index} {...seriesProps} />;
}
}
})
)}
</Chart>
);

View file

@ -14,7 +14,7 @@ import {
TableSuggestion,
TableChangeType,
} from '../types';
import { State, SeriesType, XYState, visualizationTypes } from './types';
import { State, SeriesType, XYState, visualizationTypes, LayerConfig } from './types';
import { getIconForSeries } from './state_helpers';
const columnSortOrder = {
@ -379,13 +379,19 @@ function buildSuggestion({
changeType: TableChangeType;
keptLayerIds: string[];
}) {
const existingLayer: LayerConfig | {} = getExistingLayer(currentState, layerId) || {};
const accessors = yValues.map((col) => col.columnId);
const newLayer = {
...(getExistingLayer(currentState, layerId) || {}),
...existingLayer,
layerId,
seriesType,
xAccessor: xValue.columnId,
splitAccessor: splitBy?.columnId,
accessors: yValues.map((col) => col.columnId),
accessors,
yConfig:
'yConfig' in existingLayer && existingLayer.yConfig
? existingLayer.yConfig.filter(({ forAccessor }) => accessors.indexOf(forAccessor) !== -1)
: undefined,
};
const keptLayers = currentState

View file

@ -11,13 +11,13 @@ import { Position } from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
import { LayerContextMenu } from './xy_config_panel';
import { DimensionEditor, LayerContextMenu } from './xy_config_panel';
import { Visualization, OperationMetadata, VisualizationType } from '../types';
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
import { toExpression, toPreviewExpression } from './to_expression';
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
import chartMixedSVG from '../assets/chart_mixed_xy.svg';
import { isHorizontalChart } from './state_helpers';
import { toExpression, toPreviewExpression } from './to_expression';
const defaultIcon = chartBarStackedSVG;
const defaultSeriesType = 'bar_stacked';
@ -187,6 +187,7 @@ export const xyVisualization: Visualization<State, PersistableState> = {
supportsMoreColumns: true,
required: true,
dataTestSubj: 'lnsXY_yDimensionPanel',
enableDimensionEditor: true,
},
{
groupId: 'breakdown',
@ -239,6 +240,10 @@ export const xyVisualization: Visualization<State, PersistableState> = {
newLayer.accessors = newLayer.accessors.filter((a) => a !== columnId);
}
if (newLayer.yConfig) {
newLayer.yConfig = newLayer.yConfig.filter(({ forAccessor }) => forAccessor !== columnId);
}
return {
...prevState,
layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)),
@ -259,6 +264,15 @@ export const xyVisualization: Visualization<State, PersistableState> = {
);
},
renderDimensionEditor(domElement, props) {
render(
<I18nProvider>
<DimensionEditor {...props} />
</I18nProvider>,
domElement
);
},
toExpression,
toPreviewExpression,
};