Skip to content

Commit dfd5d7b

Browse files
authored
feat(tooltip): tooltip label format for upper/lower banded area series (#391)
Allow users to set a formatted label for upper and lower bound of banded series to distinguish between series in tooltip. Format allows a postfix string or accessor function. closes #162
1 parent 79cd100 commit dfd5d7b

File tree

9 files changed

+190
-49
lines changed

9 files changed

+190
-49
lines changed

src/chart_types/xy_chart/rendering/rendering.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ export interface GeometryId {
2525
seriesKey: any[];
2626
}
2727

28+
/**
29+
* The accessor type
30+
*/
31+
export const AccessorType = Object.freeze({
32+
Y0: 'y0' as 'y0',
33+
Y1: 'y1' as 'y1',
34+
});
35+
36+
export type AccessorType = typeof AccessorType.Y0 | typeof AccessorType.Y1;
37+
2838
export interface GeometryValue {
2939
y: any;
3040
x: any;
31-
accessor: 'y1' | 'y0';
41+
accessor: AccessorType;
3242
}
3343

3444
/** Shared style properties for varies geometries */
@@ -187,19 +197,18 @@ export function renderPoints(
187197
const isLogScale = isLogarithmicScale(yScale);
188198
const pointGeometries = dataset.reduce(
189199
(acc, datum) => {
200+
const { x: xValue, y0, y1, initialY0, initialY1 } = datum;
190201
// don't create the point if not within the xScale domain
191-
if (!xScale.isValueInDomain(datum.x)) {
202+
if (!xScale.isValueInDomain(xValue)) {
192203
return acc;
193204
}
194-
const x = xScale.scale(datum.x);
205+
const x = xScale.scale(xValue);
195206
const points: PointGeometry[] = [];
196-
const yDatums = [datum.y1];
197-
if (hasY0Accessors) {
198-
yDatums.unshift(datum.y0);
199-
}
207+
const yDatums = hasY0Accessors ? [y0, y1] : [y1];
208+
200209
yDatums.forEach((yDatum, index) => {
201210
// skip rendering point if y1 is null
202-
if (datum.y1 === null) {
211+
if (y1 === null) {
203212
return;
204213
}
205214
let y;
@@ -212,7 +221,7 @@ export function renderPoints(
212221
} else {
213222
y = yScale.scale(yDatum);
214223
}
215-
const originalY = hasY0Accessors && index === 0 ? datum.initialY0 : datum.initialY1;
224+
const originalY = hasY0Accessors && index === 0 ? initialY0 : initialY1;
216225
const geometryId = {
217226
specId,
218227
seriesKey,
@@ -224,9 +233,9 @@ export function renderPoints(
224233
y,
225234
color,
226235
value: {
227-
x: datum.x,
236+
x: xValue,
228237
y: originalY,
229-
accessor: hasY0Accessors && index === 0 ? 'y0' : 'y1',
238+
accessor: hasY0Accessors && index === 0 ? AccessorType.Y0 : AccessorType.Y1,
230239
},
231240
transform: {
232241
x: shift,
@@ -235,7 +244,7 @@ export function renderPoints(
235244
geometryId,
236245
styleOverrides,
237246
};
238-
mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, pointGeometry);
247+
mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, pointGeometry);
239248
// use the geometry only if the yDatum in contained in the current yScale domain
240249
if (!isHidden && yScale.isValueInDomain(yDatum)) {
241250
points.push(pointGeometry);
@@ -358,7 +367,7 @@ export function renderBars(
358367
value: {
359368
x: datum.x,
360369
y: initialY1,
361-
accessor: 'y1',
370+
accessor: AccessorType.Y1,
362371
},
363372
geometryId,
364373
seriesStyle,
@@ -395,10 +404,10 @@ export function renderLine(
395404
const isLogScale = isLogarithmicScale(yScale);
396405

397406
const pathGenerator = line<DataSeriesDatum>()
398-
.x((datum: DataSeriesDatum) => xScale.scale(datum.x) - xScaleOffset)
399-
.y((datum: DataSeriesDatum) => yScale.scale(datum.y1))
400-
.defined((datum: DataSeriesDatum) => {
401-
return datum.y1 !== null && !(isLogScale && datum.y1 <= 0) && xScale.isValueInDomain(datum.x);
407+
.x(({ x }) => xScale.scale(x) - xScaleOffset)
408+
.y(({ y1 }) => yScale.scale(y1))
409+
.defined(({ x, y1 }) => {
410+
return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
402411
})
403412
.curve(getCurveFactory(curve));
404413
const y = 0;
@@ -458,16 +467,16 @@ export function renderArea(
458467
const isLogScale = isLogarithmicScale(yScale);
459468

460469
const pathGenerator = area<DataSeriesDatum>()
461-
.x((datum: DataSeriesDatum) => xScale.scale(datum.x) - xScaleOffset)
462-
.y1((datum: DataSeriesDatum) => yScale.scale(datum.y1))
463-
.y0((datum: DataSeriesDatum) => {
464-
if (datum.y0 === null || (isLogScale && datum.y0 <= 0)) {
470+
.x(({ x }) => xScale.scale(x) - xScaleOffset)
471+
.y1(({ y1 }) => yScale.scale(y1))
472+
.y0(({ y0 }) => {
473+
if (y0 === null || (isLogScale && y0 <= 0)) {
465474
return yScale.range[0];
466475
}
467-
return yScale.scale(datum.y0);
476+
return yScale.scale(y0);
468477
})
469-
.defined((datum: DataSeriesDatum) => {
470-
return datum.y1 !== null && !(isLogScale && datum.y1 <= 0) && xScale.isValueInDomain(datum.x);
478+
.defined(({ y1, x }) => {
479+
return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
471480
})
472481
.curve(getCurveFactory(curve));
473482

src/chart_types/xy_chart/store/utils.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mergeYCustomDomainsByGroupId } from '../utils/axis_utils';
2-
import { IndexedGeometry } from '../rendering/rendering';
2+
import { IndexedGeometry, AccessorType } from '../rendering/rendering';
33
import { DataSeriesColorsValues, findDataSeriesByColorValues, getSeriesColorMap } from '../utils/series';
44
import {
55
AreaSeriesSpec,
@@ -1009,7 +1009,7 @@ describe('Chart State utils', () => {
10091009
x: 0,
10101010
y: 0,
10111011
color: '#1EA593',
1012-
value: { x: 0, y: 5, accessor: 'y1' },
1012+
value: { x: 0, y: 5, accessor: AccessorType.Y1 },
10131013
transform: { x: 0, y: 0 },
10141014
geometryId: { specId: getSpecId('line1'), seriesKey: [] },
10151015
},
@@ -1021,7 +1021,7 @@ describe('Chart State utils', () => {
10211021
x: 0,
10221022
y: 175.8,
10231023
color: '#2B70F7',
1024-
value: { x: 0, y: 2, accessor: 'y1' },
1024+
value: { x: 0, y: 2, accessor: AccessorType.Y1 },
10251025
transform: { x: 0, y: 0 },
10261026
geometryId: { specId: getSpecId('line2'), seriesKey: [] },
10271027
},

src/chart_types/xy_chart/store/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
isLineSeriesSpec,
3636
LineSeriesSpec,
3737
Rotation,
38+
isBandedSpec,
3839
} from '../utils/specs';
3940
import { ColorConfig, Theme } from '../../../utils/themes/theme';
4041
import { identity, mergePartial } from '../../../utils/commons';
@@ -474,7 +475,7 @@ export function renderGeometries(
474475
color,
475476
(spec as LineSeriesSpec).curve || CurveType.LINEAR,
476477
ds.specId,
477-
Boolean(spec.y0Accessors),
478+
isBandedSpec(spec.y0Accessors),
478479
ds.key,
479480
xScaleOffset,
480481
lineSeriesStyle,
@@ -500,7 +501,7 @@ export function renderGeometries(
500501
color,
501502
(spec as AreaSeriesSpec).curve || CurveType.LINEAR,
502503
ds.specId,
503-
Boolean(spec.y0Accessors),
504+
isBandedSpec(spec.y0Accessors),
504505
ds.key,
505506
xScaleOffset,
506507
areaSeriesStyle,

src/chart_types/xy_chart/tooltip/tooltip.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ describe('Tooltip formatting', () => {
1818
yScaleType: ScaleType.Linear,
1919
xScaleType: ScaleType.Linear,
2020
};
21+
const bandedSpec = {
22+
...SPEC_1,
23+
y0Accessors: [1],
24+
};
2125
const YAXIS_SPEC: AxisSpec = {
2226
id: getAxisId('axis_1'),
2327
groupId: SPEC_GROUP_ID_1,
@@ -63,6 +67,23 @@ describe('Tooltip formatting', () => {
6367
},
6468
seriesStyle,
6569
};
70+
const indexedBandedGeometry: BarGeometry = {
71+
x: 0,
72+
y: 0,
73+
width: 0,
74+
height: 0,
75+
color: 'blue',
76+
geometryId: {
77+
specId: SPEC_ID_1,
78+
seriesKey: [],
79+
},
80+
value: {
81+
x: 1,
82+
y: 10,
83+
accessor: 'y1',
84+
},
85+
seriesStyle,
86+
};
6687

6788
test('format simple tooltip', () => {
6889
const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC);
@@ -74,6 +95,78 @@ describe('Tooltip formatting', () => {
7495
expect(tooltipValue.color).toBe('blue');
7596
expect(tooltipValue.value).toBe('10');
7697
});
98+
test('format banded tooltip - upper', () => {
99+
const tooltipValue = formatTooltip(indexedBandedGeometry, bandedSpec, false, false, YAXIS_SPEC);
100+
expect(tooltipValue.name).toBe('bar_1 - upper');
101+
});
102+
test('format banded tooltip - y1AccessorFormat', () => {
103+
const tooltipValue = formatTooltip(
104+
indexedBandedGeometry,
105+
{ ...bandedSpec, y1AccessorFormat: ' [max]' },
106+
false,
107+
false,
108+
YAXIS_SPEC,
109+
);
110+
expect(tooltipValue.name).toBe('bar_1 [max]');
111+
});
112+
test('format banded tooltip - y1AccessorFormat as function', () => {
113+
const tooltipValue = formatTooltip(
114+
indexedBandedGeometry,
115+
{ ...bandedSpec, y1AccessorFormat: (label) => `[max] ${label}` },
116+
false,
117+
false,
118+
YAXIS_SPEC,
119+
);
120+
expect(tooltipValue.name).toBe('[max] bar_1');
121+
});
122+
test('format banded tooltip - lower', () => {
123+
const tooltipValue = formatTooltip(
124+
{
125+
...indexedBandedGeometry,
126+
value: {
127+
...indexedBandedGeometry.value,
128+
accessor: 'y0',
129+
},
130+
},
131+
bandedSpec,
132+
false,
133+
false,
134+
YAXIS_SPEC,
135+
);
136+
expect(tooltipValue.name).toBe('bar_1 - lower');
137+
});
138+
test('format banded tooltip - y0AccessorFormat', () => {
139+
const tooltipValue = formatTooltip(
140+
{
141+
...indexedBandedGeometry,
142+
value: {
143+
...indexedBandedGeometry.value,
144+
accessor: 'y0',
145+
},
146+
},
147+
{ ...bandedSpec, y0AccessorFormat: ' [min]' },
148+
false,
149+
false,
150+
YAXIS_SPEC,
151+
);
152+
expect(tooltipValue.name).toBe('bar_1 [min]');
153+
});
154+
test('format banded tooltip - y0AccessorFormat as function', () => {
155+
const tooltipValue = formatTooltip(
156+
{
157+
...indexedBandedGeometry,
158+
value: {
159+
...indexedBandedGeometry.value,
160+
accessor: 'y0',
161+
},
162+
},
163+
{ ...bandedSpec, y0AccessorFormat: (label) => `[min] ${label}` },
164+
false,
165+
false,
166+
YAXIS_SPEC,
167+
);
168+
expect(tooltipValue.name).toBe('[min] bar_1');
169+
});
77170
test('format tooltip with seriesKey name', () => {
78171
const geometry: BarGeometry = {
79172
...indexedGeometry,

src/chart_types/xy_chart/tooltip/tooltip.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { TooltipValue, isFollowTooltipType, TooltipType, TooltipValueFormatter } from '../utils/interactions';
2-
import { IndexedGeometry, isPointOnGeometry } from '../rendering/rendering';
2+
import { IndexedGeometry, isPointOnGeometry, AccessorType } from '../rendering/rendering';
33
import { getColorValuesAsString } from '../utils/series';
4-
import { AxisSpec, BasicSeriesSpec, Rotation } from '../utils/specs';
4+
import { AxisSpec, BasicSeriesSpec, Rotation, isBandedSpec } from '../utils/specs';
55
import { SpecId, AxisId, GroupId } from '../../../utils/ids';
66
import { getAxesSpecForSpecId } from '../store/utils';
77
import { Scale } from '../../../utils/scales/scales';
88
import { Point } from '../store/chart_state';
9+
import { getAccessorFormatLabel } from '../../../utils/accessor';
910

1011
export function getSeriesTooltipValues(tooltipValues: TooltipValue[], defaultValue?: string): Map<string, any> {
1112
// map from seriesKey to tooltipValue
@@ -25,30 +26,29 @@ export function getSeriesTooltipValues(tooltipValues: TooltipValue[], defaultVal
2526
}
2627

2728
export function formatTooltip(
28-
searchIndexValue: IndexedGeometry,
29-
spec: BasicSeriesSpec,
29+
{ color, value: { x, y, accessor }, geometryId: { seriesKey } }: IndexedGeometry,
30+
{ id, name, y0AccessorFormat = ' - lower', y1AccessorFormat = ' - upper', y0Accessors }: BasicSeriesSpec,
3031
isXValue: boolean,
3132
isHighlighted: boolean,
3233
axisSpec?: AxisSpec,
3334
): TooltipValue {
34-
const { id } = spec;
35-
const {
36-
color,
37-
value: { x, y, accessor },
38-
geometryId: { seriesKey },
39-
} = searchIndexValue;
4035
const seriesKeyAsString = getColorValuesAsString(seriesKey, id);
41-
let name: string | undefined;
36+
let displayName: string | undefined;
4237
if (seriesKey.length > 0) {
43-
name = seriesKey.join(' - ');
38+
displayName = seriesKey.join(' - ');
4439
} else {
45-
name = spec.name || `${spec.id}`;
40+
displayName = name || `${id}`;
41+
}
42+
43+
if (isBandedSpec(y0Accessors)) {
44+
const formatter = accessor === AccessorType.Y0 ? y0AccessorFormat : y1AccessorFormat;
45+
displayName = getAccessorFormatLabel(formatter, displayName);
4646
}
4747

4848
const value = isXValue ? x : y;
4949
return {
5050
seriesKey: seriesKeyAsString,
51-
name,
51+
name: displayName,
5252
value: axisSpec ? axisSpec.tickFormat(value) : emptyFormatter(value),
5353
color,
5454
isHighlighted: isXValue ? false : isHighlighted,
@@ -136,6 +136,7 @@ export function getTooltipAndHighlightFromXValue(
136136

137137
return [...acc, formattedTooltip];
138138
}, []);
139+
139140
return {
140141
tooltipData,
141142
highlightedGeometries,

src/chart_types/xy_chart/utils/specs.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
BarSeriesStyle,
88
PointStyle,
99
} from '../../../utils/themes/theme';
10-
import { Accessor } from '../../../utils/accessor';
10+
import { Accessor, AccessorFormat } from '../../../utils/accessor';
1111
import { Omit, RecursivePartial } from '../../../utils/commons';
1212
import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids';
1313
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
@@ -108,6 +108,18 @@ export interface SeriesSpec {
108108
/** Index per series to sort by */
109109
sortIndex?: number;
110110
displayValueSettings?: DisplayValueSpec;
111+
/**
112+
* Postfix string or accessor function for y1 accesor when using `y0Accessors`
113+
*
114+
* @default ' - upper'
115+
*/
116+
y0AccessorFormat?: AccessorFormat;
117+
/**
118+
* Postfix string or accessor function for y1 accesor when using `y0Accessors`
119+
*
120+
* @default ' - lower'
121+
*/
122+
y1AccessorFormat?: AccessorFormat;
111123
}
112124

113125
export type CustomSeriesColorsMap = Map<DataSeriesColorsValues, string>;
@@ -413,3 +425,7 @@ export function isLineSeriesSpec(spec: BasicSeriesSpec): spec is LineSeriesSpec
413425
export function isAreaSeriesSpec(spec: BasicSeriesSpec): spec is AreaSeriesSpec {
414426
return spec.seriesType === 'area';
415427
}
428+
429+
export function isBandedSpec(y0Accessors: SeriesAccessors['y0Accessors']): boolean {
430+
return Boolean(y0Accessors && y0Accessors.length > 0);
431+
}

0 commit comments

Comments
 (0)