From 3f1c9185365a9c168594d32e8994a2e4008fc227 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Tue, 24 Sep 2019 12:41:09 -0500 Subject: [PATCH] feat(tooltip): allow postfix banded area series Allow user to set postfix for upper and lower bound of banded series to distinguish between values closes #162 --- .../rendering/rendering.areas.test.ts | 18 ++++++ .../rendering/rendering.bands.test.ts | 10 +++ .../rendering/rendering.lines.test.ts | 18 ++++++ .../xy_chart/rendering/rendering.test.ts | 1 + .../xy_chart/rendering/rendering.ts | 59 +++++++++++------ src/chart_types/xy_chart/store/utils.test.ts | 8 ++- .../xy_chart/tooltip/tooltip.test.ts | 64 +++++++++++++++++++ src/chart_types/xy_chart/tooltip/tooltip.ts | 23 ++++--- .../xy_chart/utils/interactions.test.ts | 1 + src/chart_types/xy_chart/utils/specs.ts | 12 ++++ stories/area_chart.tsx | 6 +- 11 files changed, 184 insertions(+), 36 deletions(-) diff --git a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts index d3b8f12407..c503b76f45 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -146,6 +146,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -165,6 +166,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -282,6 +284,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -301,6 +304,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -328,6 +332,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -347,6 +352,7 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -424,6 +430,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -443,6 +450,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -559,6 +567,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -578,6 +587,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -605,6 +615,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -624,6 +635,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -701,6 +713,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -720,6 +733,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -821,6 +835,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -840,6 +855,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -867,6 +883,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -886,6 +903,7 @@ describe('Rendering points - areas', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts index 3bbeca0b78..adb64bfe48 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -146,6 +146,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[1]).toEqual({ @@ -167,6 +168,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[2]).toEqual({ x: 50, @@ -186,6 +188,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[3]).toEqual({ x: 50, @@ -205,6 +208,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); }); }); @@ -289,6 +293,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[1]).toEqual({ @@ -310,6 +315,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[2]).toEqual({ x: 50, @@ -329,6 +335,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[3]).toEqual({ x: 50, @@ -348,6 +355,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[4]).toEqual({ x: 75, @@ -367,6 +375,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); expect(points[5]).toEqual({ x: 75, @@ -386,6 +395,7 @@ describe('Rendering bands - areas', () => { x: 25, y: 0, }, + banded: true, } as PointGeometry); }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts index 1e1de5e138..f6c348e4f8 100644 --- a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts @@ -136,6 +136,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -155,6 +156,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -270,6 +272,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -289,6 +292,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -316,6 +320,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 50, @@ -335,6 +340,7 @@ describe('Rendering points - line', () => { x: 25, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -411,6 +417,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -430,6 +437,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -544,6 +552,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -563,6 +572,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -590,6 +600,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -609,6 +620,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -685,6 +697,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -704,6 +717,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -805,6 +819,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -824,6 +839,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); @@ -851,6 +867,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(points[1]).toEqual({ x: 100, @@ -870,6 +887,7 @@ describe('Rendering points - line', () => { x: 0, y: 0, }, + banded: false, } as PointGeometry); expect(indexedGeometries.size).toEqual(points.length); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.test.ts b/src/chart_types/xy_chart/rendering/rendering.test.ts index f392c0f3f9..62e28cc0b5 100644 --- a/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -75,6 +75,7 @@ describe('Rendering utils', () => { x: 0, y: 0, radius: 10, + banded: false, }; expect(isPointOnGeometry(0, 0, geometry)).toBe(true); expect(isPointOnGeometry(10, 10, geometry)).toBe(true); diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 9be50179a0..5fe8585325 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -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 */ @@ -54,6 +64,10 @@ export interface PointGeometry { }; geometryId: GeometryId; value: GeometryValue; + /** + * Is the point a pair in a banded series + */ + banded: boolean; } export interface BarGeometry { x: number; @@ -71,6 +85,12 @@ export interface BarGeometry { geometryId: GeometryId; value: GeometryValue; seriesStyle: BarSeriesStyle; + /** + * Is the point a pair in a banded series + * + * TODO: Fix band bar in tooltips + */ + banded?: boolean; } export interface LineGeometry { line: string; @@ -165,19 +185,17 @@ export function renderPoints( const isLogScale = isLogarithmicScale(yScale); const pointGeometries = dataset.reduce( - (acc, datum) => { - const x = xScale.scale(datum.x); + (acc, { x: xValue, y0, y1, initialY0, initialY1 }) => { + const x = xScale.scale(xValue); if (x < xScale.range[0] || x > xScale.range[1]) { return acc; } 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; @@ -193,14 +211,14 @@ export function renderPoints( if (y < yScale.range[1] || y > yScale.range[0]) { return; } - const originalY = hasY0Accessors && index === 0 ? datum.initialY0 : datum.initialY1; + const originalY = hasY0Accessors && index === 0 ? initialY0 : initialY1; const pointGeometry: PointGeometry = { radius, x, y, color, value: { - x: datum.x, + x: xValue, y: originalY, accessor: hasY0Accessors && index === 0 ? 'y0' : 'y1', }, @@ -212,8 +230,9 @@ export function renderPoints( specId, seriesKey, }, + banded: hasY0Accessors, }; - mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, pointGeometry); + mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, pointGeometry); if (!isHidden) { points.push(pointGeometry); } @@ -373,9 +392,9 @@ export function renderLine( const isLogScale = isLogarithmicScale(yScale); const pathGenerator = line() - .x((datum: DataSeriesDatum) => xScale.scale(datum.x) - xScaleOffset) - .y((datum: DataSeriesDatum) => yScale.scale(datum.y1)) - .defined((datum: DataSeriesDatum) => datum.y1 !== null && !(isLogScale && datum.y1 <= 0)) + .x(({ x }) => xScale.scale(x) - xScaleOffset) + .y(({ y1 }) => yScale.scale(y1)) + .defined(({ y1 }) => y1 !== null && !(isLogScale && y1 <= 0)) .curve(getCurveFactory(curve)); const y = 0; const x = shift; @@ -431,15 +450,15 @@ export function renderArea( const isLogScale = isLogarithmicScale(yScale); const pathGenerator = area() - .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) => datum.y1 !== null && !(isLogScale && datum.y1 <= 0)) + .defined(({ y1 }) => y1 !== null && !(isLogScale && y1 <= 0)) .curve(getCurveFactory(curve)); const y1Line = pathGenerator.lineY1()(dataset); diff --git a/src/chart_types/xy_chart/store/utils.test.ts b/src/chart_types/xy_chart/store/utils.test.ts index 2ad7e1c67e..5dfb0ee699 100644 --- a/src/chart_types/xy_chart/store/utils.test.ts +++ b/src/chart_types/xy_chart/store/utils.test.ts @@ -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, @@ -1009,9 +1009,10 @@ 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: [] }, + banded: false, }, ]); const map2 = new Map(); @@ -1021,9 +1022,10 @@ 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: [] }, + banded: false, }, ]); const merged = mergeGeometriesIndexes(map1, map2); diff --git a/src/chart_types/xy_chart/tooltip/tooltip.test.ts b/src/chart_types/xy_chart/tooltip/tooltip.test.ts index 2181a19ba8..ac5a12dac9 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.test.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.test.ts @@ -63,6 +63,24 @@ 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, + banded: true, + }; test('format simple tooltip', () => { const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC); @@ -74,6 +92,52 @@ describe('Tooltip formatting', () => { expect(tooltipValue.color).toBe('blue'); expect(tooltipValue.value).toBe('10'); }); + test('format banded tooltip - upper', () => { + const tooltipValue = formatTooltip(indexedBandedGeometry, SPEC_1, false, false, YAXIS_SPEC); + expect(tooltipValue.name).toBe('bar_1 - upper'); + }); + test('format banded tooltip - y1AccessorPostfix', () => { + const tooltipValue = formatTooltip( + indexedBandedGeometry, + { ...SPEC_1, y1AccessorPostfix: ' [max]' }, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.name).toBe('bar_1 [max]'); + }); + test('format banded tooltip - lower', () => { + const tooltipValue = formatTooltip( + { + ...indexedBandedGeometry, + value: { + ...indexedBandedGeometry.value, + accessor: 'y0', + }, + }, + SPEC_1, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.name).toBe('bar_1 - lower'); + }); + test('format banded tooltip - y0AccessorPostfix', () => { + const tooltipValue = formatTooltip( + { + ...indexedBandedGeometry, + value: { + ...indexedBandedGeometry.value, + accessor: 'y0', + }, + }, + { ...SPEC_1, y0AccessorPostfix: ' [min]' }, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.name).toBe('bar_1 [min]'); + }); test('format tooltip with seriesKey name', () => { const geometry: BarGeometry = { ...indexedGeometry, diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index f5b6a680ae..7608070a3b 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -25,30 +25,28 @@ export function getSeriesTooltipValues(tooltipValues: TooltipValue[], defaultVal } export function formatTooltip( - searchIndexValue: IndexedGeometry, - spec: BasicSeriesSpec, + { color, value: { x, y, accessor }, geometryId: { seriesKey }, banded }: IndexedGeometry, + { id, name, y0AccessorPostfix = ' - lower', y1AccessorPostfix = ' - upper' }: 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 (banded) { + displayName = `${displayName}${accessor === 'y0' ? y0AccessorPostfix : y1AccessorPostfix}`; } const value = isXValue ? x : y; return { seriesKey: seriesKeyAsString, - name, + name: displayName, value: axisSpec ? axisSpec.tickFormat(value) : emptyFormatter(value), color, isHighlighted: isXValue ? false : isHighlighted, @@ -136,6 +134,7 @@ export function getTooltipAndHighlightFromXValue( return [...acc, formattedTooltip]; }, []); + return { tooltipData, highlightedGeometries, diff --git a/src/chart_types/xy_chart/utils/interactions.test.ts b/src/chart_types/xy_chart/utils/interactions.test.ts index cbcbb661b1..d8f332bf4b 100644 --- a/src/chart_types/xy_chart/utils/interactions.test.ts +++ b/src/chart_types/xy_chart/utils/interactions.test.ts @@ -133,6 +133,7 @@ const ig6: PointGeometry = { x: 0, y: 0, }, + banded: false, }; describe('Interaction utils', () => { const chartDimensions: Dimensions = { diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index 8ebcda6f9b..563cc2b31c 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -89,6 +89,18 @@ export interface SeriesSpec { /** Index per series to sort by */ sortIndex?: number; displayValueSettings?: DisplayValueSpec; + /** + * Postfix for y1 accesor when using `y0Accessors` + * + * @default 'upper' + */ + y0AccessorPostfix?: string; + /** + * Postfix for y1 accesor when using `y0Accessors` + * + * @default 'lower' + */ + y1AccessorPostfix?: string; } export type CustomSeriesColorsMap = Map; diff --git a/stories/area_chart.tsx b/stories/area_chart.tsx index 51f5aa7668..66182ad062 100644 --- a/stories/area_chart.tsx +++ b/stories/area_chart.tsx @@ -1,4 +1,4 @@ -import { boolean } from '@storybook/addon-knobs'; +import { boolean, text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import { DateTime } from 'luxon'; import React from 'react'; @@ -429,6 +429,8 @@ storiesOf('Area Chart', module) return [d[0], d[1]]; }); const scaleToDataExtent = boolean('scale to extent', true); + const y0AccessorPostfix = text('y0AccessorPostfix', ''); + const y1AccessorPostfix = text('y1AccessorPostfix', ''); return ( @@ -453,6 +455,8 @@ storiesOf('Area Chart', module) xAccessor={'x'} yAccessors={['max']} y0Accessors={['min']} + y1AccessorPostfix={y1AccessorPostfix || undefined} + y0AccessorPostfix={y0AccessorPostfix || undefined} data={data} yScaleToDataExtent={scaleToDataExtent} curve={CurveType.CURVE_MONOTONE_X}