diff --git a/.playground/playground.tsx b/.playground/playground.tsx index aba458f9ad..4c235f1933 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -1,43 +1,39 @@ import React from 'react'; -import { Chart, ScaleType, Position, Axis, getAxisId, timeFormatter, getSpecId, AreaSeries, Settings } from '../src'; -import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; -export class Playground extends React.Component { - chartRef: React.RefObject = React.createRef(); - onBrushEnd = (min: number, max: number) => { - // eslint-disable-next-line no-console - console.log({ min, max }); - }; +import { Chart, LineSeries, ScaleType, Position, Axis } from '../src'; +import { SeededDataGenerator } from '../src/mocks/utils'; + +export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { render() { + const dg = new SeededDataGenerator(); + const data = dg.generateGroupedSeries(10, 2).map((item) => ({ + ...item, + y1: item.y + 100, + })); + return ( <>
- - - + + + + - Number(d).toFixed(2)} - /> - - { - return [...d, d[1] - 10]; - })} +
diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-full-and-sub-series-label-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-full-and-sub-series-label-visually-looks-correct-1-snap.png deleted file mode 100644 index aa0cd39a38..0000000000 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-full-and-sub-series-label-visually-looks-correct-1-snap.png and /dev/null differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-formatting-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-formatting-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..2dc61a89e9 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-formatting-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..92752b6c79 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-series-name-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-sub-series-label-formatting-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-sub-series-label-formatting-visually-looks-correct-1-snap.png deleted file mode 100644 index e779acf24f..0000000000 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-add-custom-sub-series-label-formatting-visually-looks-correct-1-snap.png and /dev/null differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-naming-config-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-naming-config-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..53139dec02 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-naming-config-visually-looks-correct-1-snap.png differ diff --git a/src/chart_types/xy_chart/legend/legend.test.ts b/src/chart_types/xy_chart/legend/legend.test.ts index ea36a3497e..80b0fd13bd 100644 --- a/src/chart_types/xy_chart/legend/legend.test.ts +++ b/src/chart_types/xy_chart/legend/legend.test.ts @@ -1,7 +1,7 @@ import { getAxisId, getGroupId, getSpecId } from '../../../utils/ids'; import { ScaleType } from '../../../scales'; import { computeLegend } from './legend'; -import { SeriesCollectionValue, getSeriesLabel } from '../utils/series'; +import { SeriesCollectionValue, getSeriesName } from '../utils/series'; import { AxisSpec, BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { Position } from '../../../utils/commons'; import { ChartTypes } from '../..'; @@ -118,7 +118,7 @@ describe('Legends', () => { const expected = [ { color: 'red', - label: 'Spec 1 title', + name: 'Spec 1 title', seriesIdentifier: { seriesKeys: ['y1'], specId: 'spec1', @@ -142,7 +142,7 @@ describe('Legends', () => { const expected = [ { color: 'red', - label: 'Spec 1 title', + name: 'Spec 1 title', seriesIdentifier: { seriesKeys: ['y1'], specId: 'spec1', @@ -158,7 +158,7 @@ describe('Legends', () => { }, { color: 'blue', - label: 'a - b', + name: 'a - b', seriesIdentifier: { seriesKeys: ['a', 'b', 'y1'], specId: 'spec1', @@ -182,7 +182,7 @@ describe('Legends', () => { const expected = [ { color: 'red', - label: 'Spec 1 title', + name: 'Spec 1 title', seriesIdentifier: { seriesKeys: ['y1'], specId: 'spec1', @@ -198,7 +198,7 @@ describe('Legends', () => { }, { color: 'green', - label: 'spec2', + name: 'spec2', seriesIdentifier: { seriesKeys: ['y1'], specId: 'spec2', @@ -227,7 +227,7 @@ describe('Legends', () => { const expected = [ { color: 'violet', - label: 'Spec 1 title', + name: 'Spec 1 title', banded: undefined, seriesIdentifier: { seriesKeys: ['y1'], @@ -272,7 +272,7 @@ describe('Legends', () => { const visibility = [...legend.values()].map((item) => item.isSeriesVisible); expect(visibility).toEqual([false, false, true]); }); - it('returns the right series label for a color series', () => { + it('returns the right series name for a color series', () => { const seriesIdentifier1 = { specId: getSpecId(''), yAccessor: 'y1', @@ -289,38 +289,38 @@ describe('Legends', () => { }; // null removed, seriesIdentifier has to be at least an empty array - let label = getSeriesLabel(seriesIdentifier1, true, false); - expect(label).toBe(''); - label = getSeriesLabel(seriesIdentifier1, true, false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier1, true, false, spec2); - expect(label).toBe('spec2'); - label = getSeriesLabel(seriesIdentifier2, true, false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier2, true, false, spec2); - expect(label).toBe('spec2'); + let name = getSeriesName(seriesIdentifier1, true, false); + expect(name).toBe(''); + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier2, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier2, true, false, spec2); + expect(name).toBe('spec2'); - label = getSeriesLabel(seriesIdentifier1, false, false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier1, false, false, spec2); - expect(label).toBe('spec2'); - label = getSeriesLabel(seriesIdentifier2, false, false, spec1); - expect(label).toBe('a - b'); - label = getSeriesLabel(seriesIdentifier2, false, false, spec2); - expect(label).toBe('a - b'); + name = getSeriesName(seriesIdentifier1, false, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, false, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier2, false, false, spec1); + expect(name).toBe('a - b'); + name = getSeriesName(seriesIdentifier2, false, false, spec2); + expect(name).toBe('a - b'); - label = getSeriesLabel(seriesIdentifier1, true, false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier1, true, false, spec2); - expect(label).toBe('spec2'); - label = getSeriesLabel(seriesIdentifier1, true, false); - expect(label).toBe(''); - label = getSeriesLabel(seriesIdentifier1, true, false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier1, true, false, spec2); - expect(label).toBe('spec2'); + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier1, true, false); + expect(name).toBe(''); + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); }); - it('use the splitted value as label if has a single series and splitSeries is used', () => { + it('use the splitted value as name if has a single series and splitSeries is used', () => { const seriesIdentifier1 = { specId: getSpecId(''), yAccessor: 'y1', @@ -347,22 +347,22 @@ describe('Legends', () => { ...spec1, splitSeriesAccessors: ['g'], }; - let label = getSeriesLabel(seriesIdentifier1, true, false, specWithSplit); - expect(label).toBe('Spec 1 title'); + let name = getSeriesName(seriesIdentifier1, true, false, specWithSplit); + expect(name).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier3, true, false, specWithSplit); - expect(label).toBe('a'); + name = getSeriesName(seriesIdentifier3, true, false, specWithSplit); + expect(name).toBe('a'); // happens when we have multiple values in splitSeriesAccessor // or we have also multiple yAccessors - label = getSeriesLabel(seriesIdentifier2, true, false, specWithSplit); - expect(label).toBe('a - b'); + name = getSeriesName(seriesIdentifier2, true, false, specWithSplit); + expect(name).toBe('a - b'); // happens when the value of a splitSeriesAccessor is null - label = getSeriesLabel(seriesIdentifier1, true, false, specWithSplit); - expect(label).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, specWithSplit); + expect(name).toBe('Spec 1 title'); - label = getSeriesLabel(seriesIdentifier1, false, false, specWithSplit); - expect(label).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, false, false, specWithSplit); + expect(name).toBe('Spec 1 title'); }); }); diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index 324a00edc6..defdb8ca8b 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -4,7 +4,7 @@ import { SeriesCollectionValue, getSeriesIndex, getSortedDataSeriesColorsValuesMap, - getSeriesLabel, + getSeriesName, XYChartSeriesIdentifier, } from '../utils/series'; import { AxisSpec, BasicSeriesSpec, Postfixes, isAreaSeriesSpec, isBarSeriesSpec } from '../utils/specs'; @@ -19,7 +19,7 @@ interface FormattedLastValues { export type LegendItem = Postfixes & { key: string; color: string; - label: string; + name: string; seriesIdentifier: XYChartSeriesIdentifier; isSeriesVisible?: boolean; banded?: boolean; @@ -43,14 +43,14 @@ function getPostfix(spec: BasicSeriesSpec): Postfixes { } export function getItemLabel( - { banded, label, y1AccessorFormat, y0AccessorFormat }: LegendItem, + { banded, name, y1AccessorFormat, y0AccessorFormat }: LegendItem, yAccessor: BandedAccessorType, ) { if (!banded) { - return label; + return name; } - return yAccessor === BandedAccessorType.Y1 ? `${label}${y1AccessorFormat}` : `${label}${y0AccessorFormat}`; + return yAccessor === BandedAccessorType.Y1 ? `${name}${y1AccessorFormat}` : `${name}${y0AccessorFormat}`; } export function computeLegend( @@ -69,10 +69,10 @@ export function computeLegend( const spec = getSpecsById(specs, seriesIdentifier.specId); const color = seriesColors.get(key) || defaultColor; const hasSingleSeries = seriesCollection.size === 1; - const label = getSeriesLabel(seriesIdentifier, hasSingleSeries, false, spec); + const name = getSeriesName(seriesIdentifier, hasSingleSeries, false, spec); const isSeriesVisible = deselectedDataSeries ? getSeriesIndex(deselectedDataSeries, seriesIdentifier) < 0 : true; - if (label === '' || !spec) { + if (name === '' || !spec) { return; } @@ -84,7 +84,7 @@ export function computeLegend( const legendItem: LegendItem = { key, color, - label, + name, banded, seriesIdentifier, isSeriesVisible, diff --git a/src/chart_types/xy_chart/rendering/rendering.test.ts b/src/chart_types/xy_chart/rendering/rendering.test.ts index 4586163712..f378b9f48b 100644 --- a/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -105,7 +105,7 @@ describe('Rendering utils', () => { const highlightedLegendItem: LegendItem = { key: 'somekey', color: '', - label: '', + name: '', seriesIdentifier, isSeriesVisible: true, isLegendItemVisible: true, diff --git a/src/chart_types/xy_chart/state/chart_state.specs.test.ts b/src/chart_types/xy_chart/state/chart_state.specs.test.ts index be8dd55cf6..0a00ea6b09 100644 --- a/src/chart_types/xy_chart/state/chart_state.specs.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.specs.test.ts @@ -25,8 +25,8 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); const legendItems = getLegendItemsSelector(store.getState()); - const labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['A', 'B', 'C']); + const names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['A', 'B', 'C']); }); it('the legend respect the insert order [B, A, C]', () => { store.dispatch(specParsing()); @@ -35,8 +35,8 @@ describe('XYChart - specs ordering', () => { store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); store.dispatch(specParsed()); const legendItems = getLegendItemsSelector(store.getState()); - const labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['B', 'A', 'C']); + const names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['B', 'A', 'C']); }); it('the legend respect the order when changing properties of existing specs', () => { store.dispatch(specParsing()); @@ -46,8 +46,8 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); let legendItems = getLegendItemsSelector(store.getState()); - let labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['A', 'B', 'C']); + let names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['A', 'B', 'C']); store.dispatch(specParsing()); store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); @@ -56,8 +56,8 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); legendItems = getLegendItemsSelector(store.getState()); - labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['A', 'B updated', 'C']); + names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['A', 'B updated', 'C']); }); it('the legend respect the order when changing the order of the specs', () => { store.dispatch(specParsing()); @@ -67,8 +67,8 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); let legendItems = getLegendItemsSelector(store.getState()); - let labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['A', 'B', 'C']); + let names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['A', 'B', 'C']); store.dispatch(specParsing()); store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); @@ -77,7 +77,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); legendItems = getLegendItemsSelector(store.getState()); - labels = [...legendItems.values()].map((item) => item.label); - expect(labels).toEqual(['B', 'A', 'C']); + names = [...legendItems.values()].map((item) => item.name); + expect(names).toEqual(['B', 'A', 'C']); }); }); diff --git a/src/chart_types/xy_chart/state/chart_state.test.ts b/src/chart_types/xy_chart/state/chart_state.test.ts index eadbb35557..c3d5d5f1bd 100644 --- a/src/chart_types/xy_chart/state/chart_state.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.test.ts @@ -45,7 +45,7 @@ describe.skip('Chart Store', () => { const firstLegendItem: LegendItem = { key: 'color1', color: 'foo', - label: 'bar', + name: 'bar', seriesIdentifier: { specId: SPEC_ID, yAccessor: 'y1', @@ -68,7 +68,7 @@ describe.skip('Chart Store', () => { const secondLegendItem: LegendItem = { key: 'color2', color: 'baz', - label: 'qux', + name: 'qux', seriesIdentifier: { specId: SPEC_ID, yAccessor: '', diff --git a/src/chart_types/xy_chart/state/utils.test.ts b/src/chart_types/xy_chart/state/utils.test.ts index 723049fff1..843b9100cb 100644 --- a/src/chart_types/xy_chart/state/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils.test.ts @@ -1379,7 +1379,7 @@ describe('Chart State utils', () => { legendItems1.set('specId:{bars},colors:{a}', { key: 'specId:{bars},colors:{a}', color: '#1EA593', - label: 'a', + name: 'a', seriesIdentifier: { specId: 'bars', seriesKeys: ['a'], @@ -1393,7 +1393,7 @@ describe('Chart State utils', () => { legendItems1.set('specId:{bars},colors:{b}', { key: 'specId:{bars},colors:{b}', color: '#2B70F7', - label: 'b', + name: 'b', seriesIdentifier: { specId: 'bars', seriesKeys: ['b'], @@ -1411,7 +1411,7 @@ describe('Chart State utils', () => { legendItems2.set('specId:{bars},colors:{a}', { key: 'specId:{bars},colors:{a}', color: '#1EA593', - label: 'a', + name: 'a', seriesIdentifier: { specId: 'bars', seriesKeys: ['a'], @@ -1425,7 +1425,7 @@ describe('Chart State utils', () => { legendItems2.set('specId:{bars},colors:{b}', { key: 'specId:{bars},colors:{b}', color: '#2B70F7', - label: 'b', + name: 'b', seriesIdentifier: { specId: 'bars', seriesKeys: ['b'], diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index 87cb5bf711..76ee34ea9c 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -8,7 +8,7 @@ import { } from '../utils/specs'; import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; import { getAccessorFormatLabel } from '../../../utils/accessor'; -import { getSeriesLabel } from '../utils/series'; +import { getSeriesName } from '../utils/series'; import { TooltipValue } from '../../../specs'; export interface TooltipLegendValue { @@ -48,7 +48,7 @@ export function formatTooltip( hasSingleSeries: boolean, axisSpec?: AxisSpec, ): TooltipValue { - let label = getSeriesLabel(seriesIdentifier, hasSingleSeries, true, spec); + let label = getSeriesName(seriesIdentifier, hasSingleSeries, true, spec); if (isBandedSpec(spec.y0Accessors) && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) { const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec; diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index a671414d68..4cb2d3d6cb 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -10,12 +10,18 @@ import { splitSeries, XYChartSeriesIdentifier, cleanDatum, + getSeriesName, } from './series'; -import { BasicSeriesSpec, LineSeriesSpec, SeriesTypes } from './specs'; +import { BasicSeriesSpec, LineSeriesSpec, SeriesTypes, AreaSeriesSpec } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; import * as TestDataset from '../../../utils/data_samples/test_dataset'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; +import { MockSeriesSpec } from '../../../mocks/specs'; +import { SeededDataGenerator } from '../../../mocks/utils'; +import { MockSeriesIdentifier } from '../../../mocks/series/seriesIdentifiers'; + +const dg = new SeededDataGenerator(); describe('Series', () => { test('Can split dataset into 1Y0G series', () => { @@ -661,4 +667,219 @@ describe('Series', () => { expect(datum.y1).toBe(null); expect(datum.y0).toBe(null); }); + describe('#getSeriesNameKeys', () => { + const data = dg.generateGroupedSeries(50, 2).map((d) => ({ ...d, y2: d.y })); + const spec = MockSeriesSpec.area({ + data, + yAccessors: ['y', 'y2'], + splitSeriesAccessors: ['g'], + }); + const indentifiers = MockSeriesIdentifier.fromSpecs([spec]); + + it('should get series label from spec', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, spec); + expect(actual).toBe('a - y'); + }); + + it('should not show y value with single yAccessor', () => { + const specSingleY: AreaSeriesSpec = { + ...spec, + yAccessors: ['y'], + }; + const [identifier] = MockSeriesIdentifier.fromSpecs([spec]); + const actual = getSeriesName(identifier, false, false, specSingleY); + + expect(actual).toBe('a'); + }); + + describe('Custom labeling', () => { + it('should replace full label', () => { + const label = 'My custom new label'; + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: ({ yAccessor, splitAccessors }) => + yAccessor === identifier.yAccessor && splitAccessors.get('g') === 'a' ? label : null, + }); + + expect(actual).toBe(label); + }); + + it('should have access to all accessors with single y', () => { + const specSingleY: AreaSeriesSpec = { + ...spec, + yAccessors: ['y'], + name: ({ seriesKeys }) => seriesKeys.join(' - '), + }; + const [identifier] = MockSeriesIdentifier.fromSpecs([spec]); + const actual = getSeriesName(identifier, false, false, specSingleY); + + expect(actual).toBe('a - y'); + }); + + it('should replace yAccessor sub label with map', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + }, + { + accessor: 'y', + name: 'Yuuuup', + }, + ], + }, + }); + expect(actual).toBe('a - Yuuuup'); + }); + + it('should join with custom delimiter', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + }, + { + accessor: 'y', + }, + ], + delimiter: ' ¯\\_(ツ)_/¯ ', + }, + }); + expect(actual).toBe('a ¯\\_(ツ)_/¯ y'); + }); + + it('should replace splitAccessor sub label with map', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + { + accessor: 'y', + }, + ], + }, + }); + expect(actual).toBe('Apple - y'); + }); + + it('should mind order of names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + ], + }, + }); + expect(actual).toBe('Yuuum - Apple'); + }); + + it('should mind sortIndex of names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + sortIndex: 2, + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + sortIndex: 0, + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should allow undefined sortIndex', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + sortIndex: 0, + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should ignore missing names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + { + accessor: 'g', + value: 'Not a mapping', + name: 'No Value', + }, + { + accessor: 'y', + name: 'Yuuum', + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should return fallback label if empty string', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [], + }, + }); + expect(actual).toBe('a - y'); + }); + }); + }); }); diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 4c4dd068d0..582c130f11 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -3,12 +3,14 @@ import { Accessor } from '../../../utils/accessor'; import { GroupId, SpecId } from '../../../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from '../domains/y_domain'; import { formatNonStackedDataSeriesValues } from './nonstacked_series_utils'; -import { BasicSeriesSpec, SubSeriesStringPredicate, SeriesTypes, SeriesSpecs } from './specs'; +import { BasicSeriesSpec, SeriesTypes, SeriesSpecs, SeriesNameConfigOptions } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; import { ScaleType } from '../../../scales'; import { LastValues } from '../state/utils'; import { Datum } from '../../../utils/commons'; +export const SERIES_DELIMITER = ' - '; + export interface FilledValues { /** the x value */ x?: number | string; @@ -361,10 +363,11 @@ export function getSplittedSeries( const banded = spec.y0Accessors && spec.y0Accessors.length > 0; dataSeries.rawDataSeries.forEach((series) => { + const { data, ...seriesIdentifier } = series; seriesCollection.set(series.key, { banded, specSortIndex: spec.sortIndex, - seriesIdentifier: series as XYChartSeriesIdentifier, + seriesIdentifier, }); }); @@ -381,85 +384,79 @@ export function getSplittedSeries( }; } -/** - * Get custom series sub-name - */ -const getCustomSubSeriesName = (() => { - const cache = new Map(); - - return (customSubSeriesLabel: SubSeriesStringPredicate, isTooltip: boolean) => ( - args: [string | number | null, string | number], - ): string | number => { - const [accessorKey, accessorLabel] = args; - const key = [args, isTooltip].join('~~~'); - - if (cache.has(key)) { - return cache.get(key); - } else { - const label = customSubSeriesLabel(accessorLabel, accessorKey, isTooltip) || accessorLabel; - cache.set(key, label); - - return label; - } - }; -})(); - -const getSeriesLabelKeys = ( - spec: BasicSeriesSpec, - seriesIdentifier: XYChartSeriesIdentifier, - isTooltip: boolean, -): (string | number)[] => { - const isMultipleY = spec.yAccessors.length > 1; - - if (spec.customSubSeriesLabel) { - const { yAccessor, splitAccessors } = seriesIdentifier; - const fullKeyPairs: [string | number | null, string | number][] = [...splitAccessors.entries(), [null, yAccessor]]; - const labelKeys = fullKeyPairs.map(getCustomSubSeriesName(spec.customSubSeriesLabel, isTooltip)); - - return isMultipleY ? labelKeys : labelKeys.slice(0, -1); +export function getSeriesNameFromOptions( + options: SeriesNameConfigOptions, + { yAccessor, splitAccessors }: XYChartSeriesIdentifier, + delimiter: string, +): string | null { + if (!options.names) { + return null; } - const { seriesKeys } = seriesIdentifier; + return ( + options.names + .slice() + .sort(({ sortIndex: a = Infinity }, { sortIndex: b = Infinity }) => a - b) + .map(({ accessor, value, name }) => { + const accessorValue = splitAccessors.get(accessor) ?? null; + if (accessorValue === value) { + return name ?? value; + } - return isMultipleY ? seriesKeys : seriesKeys.slice(0, -1); -}; + if (yAccessor === accessor) { + return name ?? accessor; + } + return null; + }) + .filter((d) => Boolean(d) || d === 0) + .join(delimiter) || null + ); +} /** - * Get series label based on `SeriesIdentifier` + * Get series name based on `SeriesIdentifier` */ -export function getSeriesLabel( +export function getSeriesName( seriesIdentifier: XYChartSeriesIdentifier, hasSingleSeries: boolean, isTooltip: boolean, spec?: BasicSeriesSpec, ): string { - if (spec && spec.customSeriesLabel) { - const customLabel = spec.customSeriesLabel(seriesIdentifier, isTooltip); + let delimiter = SERIES_DELIMITER; + if (spec && spec.name && typeof spec.name !== 'string') { + let customLabel: string | number | null = null; + if (typeof spec.name === 'function') { + customLabel = spec.name(seriesIdentifier, isTooltip); + } else { + delimiter = spec.name.delimiter ?? delimiter; + customLabel = getSeriesNameFromOptions(spec.name, seriesIdentifier, delimiter); + } if (customLabel !== null) { - return customLabel; + return customLabel.toString(); } } - let label = ''; - const labelKeys = spec ? getSeriesLabelKeys(spec, seriesIdentifier, isTooltip) : seriesIdentifier.seriesKeys; + let name = ''; + const nameKeys = + spec && spec.yAccessors.length > 1 ? seriesIdentifier.seriesKeys : seriesIdentifier.seriesKeys.slice(0, -1); // there is one series, the is only one yAccessor, the first part is not null - if (hasSingleSeries || labelKeys.length === 0 || labelKeys[0] == null) { + if (hasSingleSeries || nameKeys.length === 0 || nameKeys[0] == null) { if (!spec) { return ''; } - if (spec.splitSeriesAccessors && labelKeys.length > 0 && labelKeys[0] != null) { - label = labelKeys.join(' - '); + if (spec.splitSeriesAccessors && nameKeys.length > 0 && nameKeys[0] != null) { + name = nameKeys.join(delimiter); } else { - label = spec.name || `${spec.id}`; + name = typeof spec.name === 'string' ? spec.name : `${spec.id}`; } } else { - label = labelKeys.join(' - '); + name = nameKeys.join(delimiter); } - return label; + return name; } function getSortIndex({ specSortIndex }: SeriesCollectionValue, total: number): number { diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index c85724ecc8..6a5973cde3 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -56,12 +56,58 @@ export type PointStyleAccessor = ( export const DEFAULT_GLOBAL_ID = '__global__'; export type FilterPredicate = (series: XYChartSeriesIdentifier) => boolean; -export type SeriesStringPredicate = (series: XYChartSeriesIdentifier, isTooltip: boolean) => string | null; -export type SubSeriesStringPredicate = ( - accessorLabel: string | number, - accessorKey: string | number | null, - isTooltip: boolean, -) => string | number | null; +export type SeriesName = string | number | null; +/** + * Function to create custom series name for a given series + */ +export type SeriesNameFn = (series: XYChartSeriesIdentifier, isTooltip: boolean) => SeriesName; +/** + * Accessor mapping to replace names + */ +export interface SeriesNameConfig { + /** + * accessor key (i.e. `yAccessors` and `seriesSplitAccessors`) + */ + accessor: string | number; + /** + * Accessor value (i.e. values from `seriesSplitAccessors`) + */ + value?: string | number; + /** + * New name for Accessor value + * + * If not provided, the original value will be used + */ + name?: string | number; + /** + * Sort order of name, overrides order listed in array. + * + * lower values - left-most + * higher values - right-most + */ + sortIndex?: number; +} +export interface SeriesNameConfigOptions { + /** + * Array of accessor naming configs to replace series names + * + * Only provided configs will be included + * (i.e. if you only provide a single mapping for `yAccessor`, all other series accessor names will be ignored) + * + * The order of configs is the order in which the resulting names will + * be joined, if no `sortIndex` is specified. + * + * If no values are found for a giving mapping in a series, the mapping will be ignored. + */ + names?: SeriesNameConfig[]; + /** + * Delimiter to join values/names + * + * @default ' - ' + */ + delimiter?: string; +} +export type SeriesNameAccessor = string | SeriesNameFn | SeriesNameConfigOptions; /** * The fit function type @@ -203,8 +249,10 @@ export interface DisplayValueSpec { export interface SeriesSpec extends Spec { specType: typeof SpecTypes.Series; chartType: typeof ChartTypes.XYAxis; - /** The name or label of the spec */ - name?: string; + /** + * The name of the spec. Also a mechanism to provide custom series names. + */ + name?: SeriesNameAccessor; /** The ID of the spec group, generated via getGroupId method * @default __global__ */ @@ -240,21 +288,6 @@ export interface SeriesSpec extends Spec { * Hide series in tooltip */ filterSeriesInTooltip?: FilterPredicate; - /** - * Custom series naming predicate function. Values are unaffected by `customSubSeriesLabel` changes. - * - * This takes precedence over `customSubSeriesLabel` - * - * @param series - `XYChartSeriesIdentifier` - * @param isTooltip - true if tooltip label, otherwise legend label - */ - customSeriesLabel?: SeriesStringPredicate; - /** - * Custom sub series naming predicate function. - * - * `customSeriesLabel` takes precedence - */ - customSubSeriesLabel?: SubSeriesStringPredicate; } export interface Postfixes { diff --git a/src/mocks/series/seriesIdentifiers.ts b/src/mocks/series/seriesIdentifiers.ts index f471994f51..24a91ea9a2 100644 --- a/src/mocks/series/seriesIdentifiers.ts +++ b/src/mocks/series/seriesIdentifiers.ts @@ -1,5 +1,4 @@ import { BasicSeriesSpec } from '../../chart_types/xy_chart/utils/specs'; -import { getSpecId } from '../..'; import { SeriesCollectionValue, getSplittedSeries, @@ -23,7 +22,7 @@ export class MockSeriesCollection { export class MockSeriesIdentifier { private static readonly base: XYChartSeriesIdentifier = { - specId: getSpecId('bars'), + specId: 'bars', yAccessor: 'y', seriesKeys: ['a'], splitAccessors: new Map().set('g', 'a'), @@ -35,4 +34,10 @@ export class MockSeriesIdentifier { mergeOptionalPartialValues: true, }); } + + static fromSpecs(specs: BasicSeriesSpec[]): XYChartSeriesIdentifier[] { + const { seriesCollection } = getSplittedSeries(specs); + + return [...seriesCollection.values()].map(({ seriesIdentifier }) => seriesIdentifier); + } } diff --git a/stories/styling.tsx b/stories/styling.tsx index a5089830f1..cb27eec69d 100644 --- a/stories/styling.tsx +++ b/stories/styling.tsx @@ -32,8 +32,8 @@ import { palettes } from '../src/utils/themes/colors'; import { BarStyleAccessor, PointStyleAccessor, - SeriesStringPredicate, - SubSeriesStringPredicate, + SeriesNameConfigOptions, + SeriesNameFn, } from '../src/chart_types/xy_chart/utils/specs'; import moment from 'moment'; import { DateTime } from 'luxon'; @@ -937,29 +937,60 @@ customSeriesStylesArea.story = { name: 'custom series styles: area', }; -export const addCustomFullAndSubSeriesLabel = () => { - const customSeriesLabel: SeriesStringPredicate = ({ yAccessor, splitAccessors }) => { +export const addCustomSeriesName = () => { + const customSeriesNamingFn: SeriesNameFn = ({ yAccessor, splitAccessors }) => { // eslint-disable-next-line react/prop-types if (yAccessor === 'y1' && splitAccessors.get('g') === 'a') { - return 'replace full series name'; + return 'Custom full series name'; } return null; }; - const customSubSeriesLabel: SubSeriesStringPredicate = (accessor, key) => { - if (key) { - // split accessor; - if (accessor === 'a') { - return 'replace a(from g)'; - } - } else { - // y accessor; - if (accessor === 'y2') { - return 'replace y2'; - } - } - return null; + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); +}; +addCustomSeriesName.story = { + name: 'Add custom series name', +}; + +export const customSeriesNamingConfig = () => { + const customSeriesNameOptions: SeriesNameConfigOptions = { + names: [ + { + // replace split accessor; + accessor: 'g', + value: 'a', + name: 'replace a(from g)', + }, + { + // replace y accessor; + accessor: 'y2', + name: 'replace y2', + }, + ], + delimiter: ' | ', }; return ( @@ -980,17 +1011,16 @@ export const addCustomFullAndSubSeriesLabel = () => { yAccessors={['y1', 'y2']} splitSeriesAccessors={['g']} data={TestDatasets.BARCHART_2Y1G} - customSeriesLabel={customSeriesLabel} - customSubSeriesLabel={customSubSeriesLabel} + name={customSeriesNameOptions} /> ); }; -addCustomFullAndSubSeriesLabel.story = { - name: 'Add custom full and sub series label', +customSeriesNamingConfig.story = { + name: 'Add custom series naming and delimeter', }; -export const addCustomSubSeriesLabelFormatting = () => { +export const addCustomSeriesNameFormatting = () => { const start = DateTime.fromISO('2019-01-01T00:00:00.000', { zone: 'utc' }); const data = [ { x: 1, y: 3, percent: 0.5, time: start.plus({ month: 1 }).toMillis() }, @@ -1003,26 +1033,30 @@ export const addCustomSubSeriesLabelFormatting = () => { { x: 2, y: 18, percent: 1, time: start.plus({ month: 2 }).toMillis() }, { x: 3, y: 7, percent: 1, time: start.plus({ month: 3 }).toMillis() }, ]; - const customSubSeriesLabel: SubSeriesStringPredicate = (accessor, key, isTooltip) => { - if (key === 'time') { - // Format time group - if (isTooltip) { - // Format tooltip time to be longer - return moment(accessor).format('ll'); - } + const customSeriesNamingFn: SeriesNameFn = ({ yAccessor, splitAccessors }, isTooltip) => + [ + ...[...splitAccessors.entries()].map(([key, value]) => { + if (key === 'time') { + // Format time group + if (isTooltip) { + // Format tooltip time to be longer + return moment(value).format('ll'); + } - // Format legend to be shorter - return moment(accessor).format('M/YYYY'); - } + // Format legend to be shorter + return moment(value).format('M/YYYY'); + } - if (key === 'percent') { - // Format percent group - return `${(accessor as number) * 100}%`; - } + if (key === 'percent') { + // Format percent group + return `${(value as number) * 100}%`; + } - // don't format yAccessor - return null; - }; + return value; + }), + // don't format yAccessor + yAccessor, + ].join(' - '); return ( @@ -1043,13 +1077,13 @@ export const addCustomSubSeriesLabelFormatting = () => { yAccessors={['y']} splitSeriesAccessors={['time', 'percent']} data={data} - customSubSeriesLabel={customSubSeriesLabel} + name={customSeriesNamingFn} /> ); }; -addCustomFullAndSubSeriesLabel.story = { - name: 'Add custom sub-series label formatting [time/date and percent]', +addCustomSeriesNameFormatting.story = { + name: 'Add custom series name formatting (legend/tooltip) [time/date and percent]', }; export const tickLabelPaddingBothPropAndTheme = () => {