Skip to content

Commit

Permalink
feat(tooltip): tooltip label format for upper/lower banded area series (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
nickofthyme authored Oct 2, 2019
1 parent 79cd100 commit dfd5d7b
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 49 deletions.
57 changes: 33 additions & 24 deletions src/chart_types/xy_chart/rendering/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,20 @@ export interface GeometryId {
seriesKey: any[];
}

/**
* The accessor type
*/
export const AccessorType = Object.freeze({
Y0: 'y0' as 'y0',
Y1: 'y1' as 'y1',
});

export type AccessorType = typeof AccessorType.Y0 | typeof AccessorType.Y1;

export interface GeometryValue {
y: any;
x: any;
accessor: 'y1' | 'y0';
accessor: AccessorType;
}

/** Shared style properties for varies geometries */
Expand Down Expand Up @@ -187,19 +197,18 @@ export function renderPoints(
const isLogScale = isLogarithmicScale(yScale);
const pointGeometries = dataset.reduce(
(acc, datum) => {
const { x: xValue, y0, y1, initialY0, initialY1 } = datum;
// don't create the point if not within the xScale domain
if (!xScale.isValueInDomain(datum.x)) {
if (!xScale.isValueInDomain(xValue)) {
return acc;
}
const x = xScale.scale(datum.x);
const x = xScale.scale(xValue);
const points: PointGeometry[] = [];
const yDatums = [datum.y1];
if (hasY0Accessors) {
yDatums.unshift(datum.y0);
}
const yDatums = hasY0Accessors ? [y0, y1] : [y1];

yDatums.forEach((yDatum, index) => {
// skip rendering point if y1 is null
if (datum.y1 === null) {
if (y1 === null) {
return;
}
let y;
Expand All @@ -212,7 +221,7 @@ export function renderPoints(
} else {
y = yScale.scale(yDatum);
}
const originalY = hasY0Accessors && index === 0 ? datum.initialY0 : datum.initialY1;
const originalY = hasY0Accessors && index === 0 ? initialY0 : initialY1;
const geometryId = {
specId,
seriesKey,
Expand All @@ -224,9 +233,9 @@ export function renderPoints(
y,
color,
value: {
x: datum.x,
x: xValue,
y: originalY,
accessor: hasY0Accessors && index === 0 ? 'y0' : 'y1',
accessor: hasY0Accessors && index === 0 ? AccessorType.Y0 : AccessorType.Y1,
},
transform: {
x: shift,
Expand All @@ -235,7 +244,7 @@ export function renderPoints(
geometryId,
styleOverrides,
};
mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, pointGeometry);
mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, pointGeometry);
// use the geometry only if the yDatum in contained in the current yScale domain
if (!isHidden && yScale.isValueInDomain(yDatum)) {
points.push(pointGeometry);
Expand Down Expand Up @@ -358,7 +367,7 @@ export function renderBars(
value: {
x: datum.x,
y: initialY1,
accessor: 'y1',
accessor: AccessorType.Y1,
},
geometryId,
seriesStyle,
Expand Down Expand Up @@ -395,10 +404,10 @@ export function renderLine(
const isLogScale = isLogarithmicScale(yScale);

const pathGenerator = line<DataSeriesDatum>()
.x((datum: DataSeriesDatum) => xScale.scale(datum.x) - xScaleOffset)
.y((datum: DataSeriesDatum) => yScale.scale(datum.y1))
.defined((datum: DataSeriesDatum) => {
return datum.y1 !== null && !(isLogScale && datum.y1 <= 0) && xScale.isValueInDomain(datum.x);
.x(({ x }) => xScale.scale(x) - xScaleOffset)
.y(({ y1 }) => yScale.scale(y1))
.defined(({ x, y1 }) => {
return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
})
.curve(getCurveFactory(curve));
const y = 0;
Expand Down Expand Up @@ -458,16 +467,16 @@ export function renderArea(
const isLogScale = isLogarithmicScale(yScale);

const pathGenerator = area<DataSeriesDatum>()
.x((datum: DataSeriesDatum) => xScale.scale(datum.x) - xScaleOffset)
.y1((datum: DataSeriesDatum) => yScale.scale(datum.y1))
.y0((datum: DataSeriesDatum) => {
if (datum.y0 === null || (isLogScale && datum.y0 <= 0)) {
.x(({ x }) => xScale.scale(x) - xScaleOffset)
.y1(({ y1 }) => yScale.scale(y1))
.y0(({ y0 }) => {
if (y0 === null || (isLogScale && y0 <= 0)) {
return yScale.range[0];
}
return yScale.scale(datum.y0);
return yScale.scale(y0);
})
.defined((datum: DataSeriesDatum) => {
return datum.y1 !== null && !(isLogScale && datum.y1 <= 0) && xScale.isValueInDomain(datum.x);
.defined(({ y1, x }) => {
return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
})
.curve(getCurveFactory(curve));

Expand Down
6 changes: 3 additions & 3 deletions src/chart_types/xy_chart/store/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeYCustomDomainsByGroupId } from '../utils/axis_utils';
import { IndexedGeometry } from '../rendering/rendering';
import { IndexedGeometry, AccessorType } from '../rendering/rendering';
import { DataSeriesColorsValues, findDataSeriesByColorValues, getSeriesColorMap } from '../utils/series';
import {
AreaSeriesSpec,
Expand Down Expand Up @@ -1009,7 +1009,7 @@ describe('Chart State utils', () => {
x: 0,
y: 0,
color: '#1EA593',
value: { x: 0, y: 5, accessor: 'y1' },
value: { x: 0, y: 5, accessor: AccessorType.Y1 },
transform: { x: 0, y: 0 },
geometryId: { specId: getSpecId('line1'), seriesKey: [] },
},
Expand All @@ -1021,7 +1021,7 @@ describe('Chart State utils', () => {
x: 0,
y: 175.8,
color: '#2B70F7',
value: { x: 0, y: 2, accessor: 'y1' },
value: { x: 0, y: 2, accessor: AccessorType.Y1 },
transform: { x: 0, y: 0 },
geometryId: { specId: getSpecId('line2'), seriesKey: [] },
},
Expand Down
5 changes: 3 additions & 2 deletions src/chart_types/xy_chart/store/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
isLineSeriesSpec,
LineSeriesSpec,
Rotation,
isBandedSpec,
} from '../utils/specs';
import { ColorConfig, Theme } from '../../../utils/themes/theme';
import { identity, mergePartial } from '../../../utils/commons';
Expand Down Expand Up @@ -474,7 +475,7 @@ export function renderGeometries(
color,
(spec as LineSeriesSpec).curve || CurveType.LINEAR,
ds.specId,
Boolean(spec.y0Accessors),
isBandedSpec(spec.y0Accessors),
ds.key,
xScaleOffset,
lineSeriesStyle,
Expand All @@ -500,7 +501,7 @@ export function renderGeometries(
color,
(spec as AreaSeriesSpec).curve || CurveType.LINEAR,
ds.specId,
Boolean(spec.y0Accessors),
isBandedSpec(spec.y0Accessors),
ds.key,
xScaleOffset,
areaSeriesStyle,
Expand Down
93 changes: 93 additions & 0 deletions src/chart_types/xy_chart/tooltip/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ describe('Tooltip formatting', () => {
yScaleType: ScaleType.Linear,
xScaleType: ScaleType.Linear,
};
const bandedSpec = {
...SPEC_1,
y0Accessors: [1],
};
const YAXIS_SPEC: AxisSpec = {
id: getAxisId('axis_1'),
groupId: SPEC_GROUP_ID_1,
Expand Down Expand Up @@ -63,6 +67,23 @@ describe('Tooltip formatting', () => {
},
seriesStyle,
};
const indexedBandedGeometry: BarGeometry = {
x: 0,
y: 0,
width: 0,
height: 0,
color: 'blue',
geometryId: {
specId: SPEC_ID_1,
seriesKey: [],
},
value: {
x: 1,
y: 10,
accessor: 'y1',
},
seriesStyle,
};

test('format simple tooltip', () => {
const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC);
Expand All @@ -74,6 +95,78 @@ describe('Tooltip formatting', () => {
expect(tooltipValue.color).toBe('blue');
expect(tooltipValue.value).toBe('10');
});
test('format banded tooltip - upper', () => {
const tooltipValue = formatTooltip(indexedBandedGeometry, bandedSpec, false, false, YAXIS_SPEC);
expect(tooltipValue.name).toBe('bar_1 - upper');
});
test('format banded tooltip - y1AccessorFormat', () => {
const tooltipValue = formatTooltip(
indexedBandedGeometry,
{ ...bandedSpec, y1AccessorFormat: ' [max]' },
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.name).toBe('bar_1 [max]');
});
test('format banded tooltip - y1AccessorFormat as function', () => {
const tooltipValue = formatTooltip(
indexedBandedGeometry,
{ ...bandedSpec, y1AccessorFormat: (label) => `[max] ${label}` },
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.name).toBe('[max] bar_1');
});
test('format banded tooltip - lower', () => {
const tooltipValue = formatTooltip(
{
...indexedBandedGeometry,
value: {
...indexedBandedGeometry.value,
accessor: 'y0',
},
},
bandedSpec,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.name).toBe('bar_1 - lower');
});
test('format banded tooltip - y0AccessorFormat', () => {
const tooltipValue = formatTooltip(
{
...indexedBandedGeometry,
value: {
...indexedBandedGeometry.value,
accessor: 'y0',
},
},
{ ...bandedSpec, y0AccessorFormat: ' [min]' },
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.name).toBe('bar_1 [min]');
});
test('format banded tooltip - y0AccessorFormat as function', () => {
const tooltipValue = formatTooltip(
{
...indexedBandedGeometry,
value: {
...indexedBandedGeometry.value,
accessor: 'y0',
},
},
{ ...bandedSpec, y0AccessorFormat: (label) => `[min] ${label}` },
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.name).toBe('[min] bar_1');
});
test('format tooltip with seriesKey name', () => {
const geometry: BarGeometry = {
...indexedGeometry,
Expand Down
29 changes: 15 additions & 14 deletions src/chart_types/xy_chart/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { TooltipValue, isFollowTooltipType, TooltipType, TooltipValueFormatter } from '../utils/interactions';
import { IndexedGeometry, isPointOnGeometry } from '../rendering/rendering';
import { IndexedGeometry, isPointOnGeometry, AccessorType } from '../rendering/rendering';
import { getColorValuesAsString } from '../utils/series';
import { AxisSpec, BasicSeriesSpec, Rotation } from '../utils/specs';
import { AxisSpec, BasicSeriesSpec, Rotation, isBandedSpec } from '../utils/specs';
import { SpecId, AxisId, GroupId } from '../../../utils/ids';
import { getAxesSpecForSpecId } from '../store/utils';
import { Scale } from '../../../utils/scales/scales';
import { Point } from '../store/chart_state';
import { getAccessorFormatLabel } from '../../../utils/accessor';

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

export function formatTooltip(
searchIndexValue: IndexedGeometry,
spec: BasicSeriesSpec,
{ color, value: { x, y, accessor }, geometryId: { seriesKey } }: IndexedGeometry,
{ id, name, y0AccessorFormat = ' - lower', y1AccessorFormat = ' - upper', y0Accessors }: BasicSeriesSpec,
isXValue: boolean,
isHighlighted: boolean,
axisSpec?: AxisSpec,
): TooltipValue {
const { id } = spec;
const {
color,
value: { x, y, accessor },
geometryId: { seriesKey },
} = searchIndexValue;
const seriesKeyAsString = getColorValuesAsString(seriesKey, id);
let name: string | undefined;
let displayName: string | undefined;
if (seriesKey.length > 0) {
name = seriesKey.join(' - ');
displayName = seriesKey.join(' - ');
} else {
name = spec.name || `${spec.id}`;
displayName = name || `${id}`;
}

if (isBandedSpec(y0Accessors)) {
const formatter = accessor === AccessorType.Y0 ? y0AccessorFormat : y1AccessorFormat;
displayName = getAccessorFormatLabel(formatter, displayName);
}

const value = isXValue ? x : y;
return {
seriesKey: seriesKeyAsString,
name,
name: displayName,
value: axisSpec ? axisSpec.tickFormat(value) : emptyFormatter(value),
color,
isHighlighted: isXValue ? false : isHighlighted,
Expand Down Expand Up @@ -136,6 +136,7 @@ export function getTooltipAndHighlightFromXValue(

return [...acc, formattedTooltip];
}, []);

return {
tooltipData,
highlightedGeometries,
Expand Down
18 changes: 17 additions & 1 deletion src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
BarSeriesStyle,
PointStyle,
} from '../../../utils/themes/theme';
import { Accessor } from '../../../utils/accessor';
import { Accessor, AccessorFormat } from '../../../utils/accessor';
import { Omit, RecursivePartial } from '../../../utils/commons';
import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids';
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
Expand Down Expand Up @@ -108,6 +108,18 @@ export interface SeriesSpec {
/** Index per series to sort by */
sortIndex?: number;
displayValueSettings?: DisplayValueSpec;
/**
* Postfix string or accessor function for y1 accesor when using `y0Accessors`
*
* @default ' - upper'
*/
y0AccessorFormat?: AccessorFormat;
/**
* Postfix string or accessor function for y1 accesor when using `y0Accessors`
*
* @default ' - lower'
*/
y1AccessorFormat?: AccessorFormat;
}

export type CustomSeriesColorsMap = Map<DataSeriesColorsValues, string>;
Expand Down Expand Up @@ -413,3 +425,7 @@ export function isLineSeriesSpec(spec: BasicSeriesSpec): spec is LineSeriesSpec
export function isAreaSeriesSpec(spec: BasicSeriesSpec): spec is AreaSeriesSpec {
return spec.seriesType === 'area';
}

export function isBandedSpec(y0Accessors: SeriesAccessors['y0Accessors']): boolean {
return Boolean(y0Accessors && y0Accessors.length > 0);
}
Loading

0 comments on commit dfd5d7b

Please sign in to comment.