Skip to content

Commit

Permalink
feat: allow custom series naming
Browse files Browse the repository at this point in the history
Allow user to set custom names for series based on DataSeriesColorsValues

elastic#245
  • Loading branch information
nickofthyme committed Aug 6, 2019
1 parent 759e8b5 commit b432645
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 46 deletions.
114 changes: 84 additions & 30 deletions src/chart_types/xy_chart/legend/legend.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -65,6 +65,7 @@ axesSpecs.set(axisSpec.id, axisSpec);
describe('Legends', () => {
const seriesColor = new Map<string, DataSeriesColorsValues>();
const seriesColorMap = new Map<string, string>();
const seriesNameMap = new Map<string, string>();
const specs = new Map<SpecId, BasicSeriesSpec>();
specs.set(spec1.id, spec1);
specs.set(spec2.id, spec2);
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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<string, string>();
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs);
const legend = computeLegend(seriesColor, emptyColorMap, seriesNameMap, specs, 'violet', axesSpecs);
const expected = [
{
color: 'violet',
Expand All @@ -174,7 +175,15 @@ describe('Legends', () => {
const emptyColorMap = new Map<string, string>();
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);

Expand All @@ -189,32 +198,77 @@ describe('Legends', () => {
const emptyColorMap = new Map<string, string>();
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<string, string>([[seriesKey, customName]]);
const label = getSeriesColorLabel(['a', 'b'], false, names, seriesKey, spec2);
expect(label).toBe(customName);
});
});
});
11 changes: 10 additions & 1 deletion src/chart_types/xy_chart/legend/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface LegendItem {
export function computeLegend(
seriesColor: Map<string, DataSeriesColorsValues>,
seriesColorMap: Map<string, string>,
seriesNameMap: Map<string, string>,
specs: Map<SpecId, BasicSeriesSpec>,
defaultColor: string,
axesSpecs: Map<AxisId, AxisSpec>,
Expand All @@ -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) {
Expand Down Expand Up @@ -68,8 +69,16 @@ export function computeLegend(
export function getSeriesColorLabel(
colorValues: any[],
hasSingleSeries: boolean,
seriesNameMap: Map<string, string>,
seriesKey: string,
spec?: BasicSeriesSpec,
): string | undefined {
const customLabel = seriesNameMap.get(seriesKey!);

if (customLabel) {
return customLabel;
}

let label = '';

if (hasSingleSeries || colorValues.length === 0 || !colorValues[0]) {
Expand Down
16 changes: 14 additions & 2 deletions src/chart_types/xy_chart/store/chart_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
FormattedDataSeries,
getSeriesColorMap,
RawDataSeries,
getSeriesNameMap,
} from '../utils/series';
import {
AnnotationSpec,
Expand Down Expand Up @@ -167,6 +168,7 @@ export class ChartStore {
deselectedDataSeries: DataSeriesColorsValues[] | null = null;
customSeriesColors: Map<string, string> = new Map();
seriesColorMap: Map<string, string> = new Map();
seriesNameMap: Map<string, string> = new Map();
totalBarsInCluster?: number;

tooltipData = observable.array<TooltipValue>([], { deep: false });
Expand Down Expand Up @@ -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];
}

Expand Down Expand Up @@ -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,
Expand Down
27 changes: 22 additions & 5 deletions src/chart_types/xy_chart/tooltip/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand All @@ -81,14 +82,30 @@ 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);
expect(tooltipValue.isHighlighted).toBe(false);
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<string, string>([[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,
Expand All @@ -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);
Expand All @@ -113,15 +130,15 @@ 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);
expect(tooltipValue.isHighlighted).toBe(false);
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);
});
});
7 changes: 6 additions & 1 deletion src/chart_types/xy_chart/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function formatTooltip(
spec: BasicSeriesSpec,
isXValue: boolean,
isHighlighted: boolean,
seriesNameMap: Map<string, string>,
axisSpec?: AxisSpec,
): TooltipValue {
const { id } = spec;
Expand All @@ -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}`;
Expand Down
Loading

0 comments on commit b432645

Please sign in to comment.