diff --git a/src/chart_types/xy_chart/legend/legend.test.ts b/src/chart_types/xy_chart/legend/legend.test.ts index 043e1c9086..9d151888ae 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 { AxisId, getAxisId, getGroupId, getSpecId, SpecId } from '../../../utils/ids'; import { ScaleType } from '../../../utils/scales/scales'; import { computeLegend, getSeriesColorLabel } from './legend'; -import { DataSeriesColorsValues } from '../utils/series'; +import { DataSeriesColorsValues, getColorValuesAsString } from '../utils/series'; import { AxisSpec, BasicSeriesSpec, Position } from '../utils/specs'; const colorValues1a = { @@ -65,6 +65,7 @@ axesSpecs.set(axisSpec.id, axisSpec); describe('Legends', () => { const seriesColor = new Map(); const seriesColorMap = new Map(); + const seriesNameMap = new Map(); const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); @@ -77,7 +78,7 @@ describe('Legends', () => { }); it('compute legend for a single series', () => { seriesColor.set('colorSeries1a', colorValues1a); - const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs); + const legend = computeLegend(seriesColor, seriesColorMap, seriesNameMap, specs, 'violet', axesSpecs); const expected = [ { color: 'red', @@ -94,7 +95,7 @@ describe('Legends', () => { it('compute legend for a single spec but with multiple series', () => { seriesColor.set('colorSeries1a', colorValues1a); seriesColor.set('colorSeries1b', colorValues1b); - const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs); + const legend = computeLegend(seriesColor, seriesColorMap, seriesNameMap, specs, 'violet', axesSpecs); const expected = [ { color: 'red', @@ -120,7 +121,7 @@ describe('Legends', () => { it('compute legend for multiple specs', () => { seriesColor.set('colorSeries1a', colorValues1a); seriesColor.set('colorSeries2a', colorValues2a); - const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs); + const legend = computeLegend(seriesColor, seriesColorMap, seriesNameMap, specs, 'violet', axesSpecs); const expected = [ { color: 'red', @@ -145,13 +146,13 @@ describe('Legends', () => { }); it('empty legend for missing spec', () => { seriesColor.set('colorSeries2b', colorValues2b); - const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs); + const legend = computeLegend(seriesColor, seriesColorMap, seriesNameMap, specs, 'violet', axesSpecs); expect(legend.size).toEqual(0); }); it('compute legend with default color for missing series color', () => { seriesColor.set('colorSeries1a', colorValues1a); const emptyColorMap = new Map(); - const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs); + const legend = computeLegend(seriesColor, emptyColorMap, seriesNameMap, specs, 'violet', axesSpecs); const expected = [ { color: 'violet', @@ -174,7 +175,15 @@ describe('Legends', () => { const emptyColorMap = new Map(); const deselectedDataSeries = null; - const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs, deselectedDataSeries); + const legend = computeLegend( + seriesColor, + emptyColorMap, + seriesNameMap, + specs, + 'violet', + axesSpecs, + deselectedDataSeries, + ); const visibility = [...legend.values()].map((item) => item.isSeriesVisible); @@ -189,32 +198,77 @@ describe('Legends', () => { const emptyColorMap = new Map(); const deselectedDataSeries = [colorValues1a, colorValues1b]; - const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs, deselectedDataSeries); + const legend = computeLegend( + seriesColor, + emptyColorMap, + seriesNameMap, + specs, + 'violet', + axesSpecs, + deselectedDataSeries, + ); const visibility = [...legend.values()].map((item) => item.isSeriesVisible); expect(visibility).toEqual([false, false, true]); }); - it('returns the right series label for a color series', () => { - let label = getSeriesColorLabel([], true); - expect(label).toBeUndefined(); - label = getSeriesColorLabel([], true, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesColorLabel([], true, spec2); - expect(label).toBe('spec2'); - label = getSeriesColorLabel(['a', 'b'], true, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesColorLabel(['a', 'b'], true, spec2); - expect(label).toBe('spec2'); - - label = getSeriesColorLabel([], false); - expect(label).toBeUndefined(); - label = getSeriesColorLabel([], false, spec1); - expect(label).toBe('Spec 1 title'); - label = getSeriesColorLabel([], false, spec2); - expect(label).toBe('spec2'); - label = getSeriesColorLabel(['a', 'b'], false, spec1); - expect(label).toBe('a - b'); - label = getSeriesColorLabel(['a', 'b'], false, spec2); - expect(label).toBe('a - b'); + + describe('getSeriesColorLabel', () => { + it('should return undefined if there is no spec and hasSingleSeries is true', () => { + const label = getSeriesColorLabel([], true, new Map(), ''); + expect(label).toBeUndefined(); + }); + + it('should return undefined if there is no spec and hasSingleSeries is false', () => { + const label = getSeriesColorLabel([], false, new Map(), ''); + expect(label).toBeUndefined(); + }); + + it('should return spec name when hasSingleSeries is true', () => { + const label = getSeriesColorLabel([], true, new Map(), '', spec1); + expect(label).toBe(spec1.name); + }); + + it('should return spec name when hasSingleSeries is false', () => { + const label = getSeriesColorLabel([], false, new Map(), '', spec1); + expect(label).toBe(spec1.name); + }); + + it('should return spec id if no name when hasSingleSeries is true', () => { + const label = getSeriesColorLabel([], true, new Map(), '', spec2); + expect(label).toBe(spec2.id); + }); + + it('should return spec id if no name when hasSingleSeries is false', () => { + const label = getSeriesColorLabel([], false, new Map(), '', spec2); + expect(label).toBe('spec2'); + }); + + it('should return spec name, not colorValues, when hasSingleSeries is true', () => { + const label = getSeriesColorLabel(['a', 'b'], true, new Map(), '', spec1); + expect(label).toBe('Spec 1 title'); + }); + + it('should return colorValues, not spec name, when hasSingleSeries is false', () => { + const label = getSeriesColorLabel(['a', 'b'], false, new Map(), '', spec1); + expect(label).toBe('a - b'); + }); + + it('should return spec id, not colorValues, when hasSingleSeries is true', () => { + const label = getSeriesColorLabel(['a', 'b'], true, new Map(), '', spec2); + expect(label).toBe(spec2.id); + }); + + it('should return colorValues, not spec id, when hasSingleSeries is false', () => { + const label = getSeriesColorLabel(['a', 'b'], false, new Map(), '', spec2); + expect(label).toBe('a - b'); + }); + + it('should return custom name for given series', () => { + const customName = 'Custom series name'; + const seriesKey = getColorValuesAsString(['a', 'b'], spec2.id); + const names = new Map([[seriesKey, customName]]); + const label = getSeriesColorLabel(['a', 'b'], false, names, seriesKey, spec2); + expect(label).toBe(customName); + }); }); }); diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index 511ca1b593..fd3ead74bb 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -24,6 +24,7 @@ export interface LegendItem { export function computeLegend( seriesColor: Map, seriesColorMap: Map, + seriesNameMap: Map, specs: Map, defaultColor: string, axesSpecs: Map, @@ -36,7 +37,7 @@ export function computeLegend( const spec = specs.get(series.specId); const color = seriesColorMap.get(key) || defaultColor; const hasSingleSeries = seriesColor.size === 1; - const label = getSeriesColorLabel(series.colorValues, hasSingleSeries, spec); + const label = getSeriesColorLabel(series.colorValues, hasSingleSeries, seriesNameMap, key, spec); const isSeriesVisible = deselectedDataSeries ? findDataSeriesByColorValues(deselectedDataSeries, series) < 0 : true; if (!label || !spec) { @@ -68,8 +69,16 @@ export function computeLegend( export function getSeriesColorLabel( colorValues: any[], hasSingleSeries: boolean, + seriesNameMap: Map, + seriesKey: string, spec?: BasicSeriesSpec, ): string | undefined { + const customLabel = seriesNameMap.get(seriesKey!); + + if (customLabel) { + return customLabel; + } + let label = ''; if (hasSingleSeries || colorValues.length === 0 || !colorValues[0]) { diff --git a/src/chart_types/xy_chart/store/chart_state.ts b/src/chart_types/xy_chart/store/chart_state.ts index f464807810..40705799e7 100644 --- a/src/chart_types/xy_chart/store/chart_state.ts +++ b/src/chart_types/xy_chart/store/chart_state.ts @@ -27,6 +27,7 @@ import { FormattedDataSeries, getSeriesColorMap, RawDataSeries, + getSeriesNameMap, } from '../utils/series'; import { AnnotationSpec, @@ -167,6 +168,7 @@ export class ChartStore { deselectedDataSeries: DataSeriesColorsValues[] | null = null; customSeriesColors: Map = new Map(); seriesColorMap: Map = new Map(); + seriesNameMap: Map = new Map(); totalBarsInCluster?: number; tooltipData = observable.array([], { deep: false }); @@ -368,13 +370,20 @@ export class ChartStore { // format the tooltip values const yAxisFormatSpec = [0, 180].includes(this.chartRotation) ? yAxis : xAxis; - const formattedTooltip = formatTooltip(indexedGeometry, spec, false, isHighlighted, yAxisFormatSpec); + const formattedTooltip = formatTooltip( + indexedGeometry, + spec, + false, + isHighlighted, + this.seriesNameMap, + yAxisFormatSpec, + ); // format only one time the x value if (!xValueInfo) { // if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter const xAxisFormatSpec = [0, 180].includes(this.chartRotation) ? xAxis : yAxis; const formatterAxis = this.tooltipHeaderFormatter ? undefined : xAxisFormatSpec; - xValueInfo = formatTooltip(indexedGeometry, spec, true, false, formatterAxis); + xValueInfo = formatTooltip(indexedGeometry, spec, true, false, this.seriesNameMap, formatterAxis); return [xValueInfo, ...acc, formattedTooltip]; } @@ -803,9 +812,12 @@ export class ChartStore { this.customSeriesColors, ); + this.seriesNameMap = getSeriesNameMap(this.seriesSpecs); + this.legendItems = computeLegend( this.seriesDomainsAndData.seriesColors, this.seriesColorMap, + this.seriesNameMap, this.seriesSpecs, this.chartTheme.colors.defaultVizColor, this.axesSpecs, diff --git a/src/chart_types/xy_chart/tooltip/tooltip.test.ts b/src/chart_types/xy_chart/tooltip/tooltip.test.ts index aeca41b6bd..10716fbe04 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.test.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.test.ts @@ -3,6 +3,7 @@ import { ScaleType } from '../../../utils/scales/scales'; import { BarGeometry } from '../rendering/rendering'; import { AxisSpec, BarSeriesSpec, Position } from '../utils/specs'; import { formatTooltip } from './tooltip'; +import { getColorValuesAsString } from '../utils/series'; describe('Tooltip formatting', () => { const SPEC_ID_1 = getSpecId('bar_1'); @@ -65,7 +66,7 @@ describe('Tooltip formatting', () => { }; test('format simple tooltip', () => { - const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC); + const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, new Map(), YAXIS_SPEC); expect(tooltipValue).toBeDefined(); expect(tooltipValue.name).toBe('bar_1'); expect(tooltipValue.isXValue).toBe(false); @@ -81,7 +82,7 @@ describe('Tooltip formatting', () => { seriesKey: ['y1'], }, }; - const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, YAXIS_SPEC); + const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, new Map(), YAXIS_SPEC); expect(tooltipValue).toBeDefined(); expect(tooltipValue.name).toBe('y1'); expect(tooltipValue.isXValue).toBe(false); @@ -89,6 +90,22 @@ describe('Tooltip formatting', () => { expect(tooltipValue.color).toBe('blue'); expect(tooltipValue.value).toBe('10'); }); + test('format tooltip with custom series name', () => { + const seriesKey = ['y1']; + const customName = 'My Custom Series'; + const geometry: BarGeometry = { + ...indexedGeometry, + geometryId: { + specId: SPEC_ID_1, + seriesKey, + }, + }; + const seriesKeyAsString = getColorValuesAsString(seriesKey, SPEC_ID_1); + const nameMap = new Map([[seriesKeyAsString, customName]]); + const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, nameMap, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.name).toBe(customName); + }); test('format y0 tooltip', () => { const geometry: BarGeometry = { ...indexedGeometry, @@ -97,7 +114,7 @@ describe('Tooltip formatting', () => { accessor: 'y0', }, }; - const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, YAXIS_SPEC); + const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, new Map(), YAXIS_SPEC); expect(tooltipValue).toBeDefined(); expect(tooltipValue.name).toBe('bar_1'); expect(tooltipValue.isXValue).toBe(false); @@ -113,7 +130,7 @@ describe('Tooltip formatting', () => { accessor: 'y0', }, }; - let tooltipValue = formatTooltip(geometry, SPEC_1, true, false, YAXIS_SPEC); + let tooltipValue = formatTooltip(geometry, SPEC_1, true, false, new Map(), YAXIS_SPEC); expect(tooltipValue).toBeDefined(); expect(tooltipValue.name).toBe('bar_1'); expect(tooltipValue.isXValue).toBe(true); @@ -121,7 +138,7 @@ describe('Tooltip formatting', () => { expect(tooltipValue.color).toBe('blue'); expect(tooltipValue.value).toBe('1'); // disable any highlight on x value - tooltipValue = formatTooltip(geometry, SPEC_1, true, true, YAXIS_SPEC); + tooltipValue = formatTooltip(geometry, SPEC_1, true, true, new Map(), YAXIS_SPEC); expect(tooltipValue.isHighlighted).toBe(false); }); }); diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index 0f429e6598..5b1c388336 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -25,6 +25,7 @@ export function formatTooltip( spec: BasicSeriesSpec, isXValue: boolean, isHighlighted: boolean, + seriesNameMap: Map, axisSpec?: AxisSpec, ): TooltipValue { const { id } = spec; @@ -34,8 +35,12 @@ export function formatTooltip( geometryId: { seriesKey }, } = searchIndexValue; const seriesKeyAsString = getColorValuesAsString(seriesKey, id); + const customName = seriesKey.length > 0 ? seriesNameMap.get(seriesKeyAsString) : ''; let name: string | undefined; - if (seriesKey.length > 0) { + + if (customName) { + name = customName; + } else if (seriesKey.length > 0) { name = seriesKey.join(' - '); } else { name = spec.name || `${spec.id}`; diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 76ffd19189..22e58ed404 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -9,6 +9,8 @@ import { getSplittedSeries, RawDataSeries, splitSeries, + getSeriesNameMap, + getColorValuesAsString, } from './series'; import { BasicSeriesSpec } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; @@ -488,4 +490,39 @@ describe('Series', () => { expect(getSortedDataSeriesColorsValuesMap(colorValuesMap)).toEqual(undefinedSortedColorValues); }); + describe('getSeriesNameMap', () => { + const seriesSpecs = new Map(); + const customName = 'Custom series name'; + const specId = getSpecId('spec'); + const colorValues = ['y1', 'x1']; + const customSeriesNames = new Map([ + [ + { + specId, + colorValues, + }, + customName, + ], + ]); + const spec: BasicSeriesSpec = { + id: specId, + groupId: getGroupId('group'), + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + seriesType: 'bar', + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + stackAccessors: ['x'], + yScaleToDataExtent: false, + data: TestDataset.BARCHART_2Y0G, + customSeriesNames, + }; + seriesSpecs.set(spec.id, spec); + const nameMap = getSeriesNameMap(seriesSpecs); + const expectedMap = new Map([[getColorValuesAsString(colorValues, specId), customName]]); + + it('should return the expected name map', () => { + expect(nameMap).toEqual(expectedMap); + }); + }); }); diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index fe5c2828e6..833610aee9 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -368,3 +368,21 @@ export function getSeriesColorMap( }); return seriesColorMap; } + +export function getSeriesNameMap(seriesSpecs: Map): Map { + const seriesColorMap = new Map(); + + seriesSpecs.forEach(({ customSeriesNames }) => { + if (!customSeriesNames || customSeriesNames.size === 0) { + return seriesColorMap; + } + + customSeriesNames.forEach((seriesNameKey, { colorValues, specId }) => { + const seriesKey = getColorValuesAsString(colorValues, specId); + + seriesColorMap.set(seriesKey, seriesNameKey); + }); + }); + + return seriesColorMap; +} diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index 458375aa2e..f36f87c12d 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -77,6 +77,8 @@ export interface SeriesSpec { data: Datum[]; /** The type of series you are looking to render */ seriesType: 'bar' | 'line' | 'area'; + /** Custom naming for series */ + customSeriesNames?: CustomSeriesNamesMap; /** Custom colors for series */ customSeriesColors?: CustomSeriesColorsMap; /** If the series should appear in the legend @@ -89,6 +91,7 @@ export interface SeriesSpec { } export type CustomSeriesColorsMap = Map; +export type CustomSeriesNamesMap = Map; export interface SeriesAccessors { /** The field name of the x value on Datum object */ diff --git a/stories/styling.tsx b/stories/styling.tsx index 701db422a0..927695abd5 100644 --- a/stories/styling.tsx +++ b/stories/styling.tsx @@ -1,4 +1,4 @@ -import { boolean, color, number, select } from '@storybook/addon-knobs'; +import { boolean, color, number, select, text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { switchTheme } from '../.storybook/theme_service'; @@ -24,6 +24,7 @@ import { } from '../src/'; import * as TestDatasets from '../src/utils/data_samples/test_dataset'; import { palettes } from '../src/utils/themes/colors'; +import { CustomSeriesNamesMap } from '../src/chart_types/xy_chart/utils/specs'; function range(title: string, min: number, max: number, value: number, groupId?: string, step: number = 1) { return number( @@ -431,12 +432,7 @@ storiesOf('Stylings', module) return ( - + ); }) + .add('custom series names', () => { + const colorValues = ['cloudflare.com', 'direct-cdn', 'y2']; + const barCustomSeriesNames: CustomSeriesNamesMap = new Map(); + const barDataSeries: DataSeriesColorsValues = { + colorValues, + specId: getSpecId('bars'), + }; + + const customSeriesText = text(`Custom series name`, 'MY CUSTOM SERIES'); + barCustomSeriesNames.set(barDataSeries, customSeriesText); + + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); + }) .add('custom series styles: bars', () => { const applyBarStyle = boolean('apply bar style (bar 1 series)', true, 'Chart Global Theme');