Skip to content

Commit

Permalink
feat(tooltip): add custom headerFormatter (#233)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Previously, you could define `tooltipType` and `tooltipSnap` props in a Settings component; this commit removes these from `SettingsSpecProps` and instead there is a single `tooltip` prop which can accept either a `TooltipType` or a full `TooltipProps` object which may include `type`, `snap`, and/or `headerFormattter` for formatting the header.
  • Loading branch information
emmacunningham authored Jun 13, 2019
1 parent 3b31cea commit bd181b5
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 52 deletions.
13 changes: 11 additions & 2 deletions src/components/tooltips.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { TooltipValue, TooltipValueFormatter } from '../lib/utils/interactions';
import { ChartStore } from '../state/chart_state';

interface TooltipProps {
Expand All @@ -10,14 +11,22 @@ interface TooltipProps {
class TooltipsComponent extends React.Component<TooltipProps> {
static displayName = 'Tooltips';

renderHeader(headerData?: TooltipValue, formatter?: TooltipValueFormatter) {
if (!headerData) {
return null;
}

return formatter ? formatter(headerData) : headerData.value;
}

render() {
const { isTooltipVisible, tooltipData, tooltipPosition } = this.props.chartStore!;
const { isTooltipVisible, tooltipData, tooltipPosition, tooltipHeaderFormatter } = this.props.chartStore!;
if (!isTooltipVisible.get()) {
return <div className="echTooltip echTooltip--hidden" />;
}
return (
<div className="echTooltip" style={{ transform: tooltipPosition.transform }}>
<p className="echTooltip__header">{tooltipData[0] && tooltipData[0].value}</p>
<div className="echTooltip__header">{this.renderHeader(tooltipData[0], tooltipHeaderFormatter)}</div>
<div className="echTooltip__table">
<table>
<tbody>
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './specs';
export { Chart } from './components/chart';
export { TooltipType } from './lib/utils/interactions';
export { TooltipType, TooltipValue, TooltipValueFormatter } from './lib/utils/interactions';
export { getAxisId, getGroupId, getSpecId, getAnnotationId } from './lib/utils/ids';
export { ScaleType } from './lib/utils/scales/scales';
export { Position, Rendering, Rotation } from './lib/series/specs';
Expand Down
4 changes: 2 additions & 2 deletions src/lib/series/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ export function formatTooltip(
};
}

function emptyFormatter(value: any): string {
return `${value}`;
function emptyFormatter<T>(value: T): T {
return value;
}
3 changes: 3 additions & 0 deletions src/lib/utils/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export interface TooltipValue {
isXValue: boolean;
seriesKey: string;
}

export type TooltipValueFormatter = (data: TooltipValue) => JSX.Element | string;

export interface HighlightedElement {
position: {
x: number;
Expand Down
12 changes: 8 additions & 4 deletions src/specs/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ describe('Settings spec component', () => {
rendering: 'svg' as Rendering,
animateData: true,
showLegend: true,
tooltipType: TooltipType.None,
tooltipSnap: false,
tooltip: {
type: TooltipType.None,
snap: false,
},
legendPosition: Position.Bottom,
showLegendDisplayValue: false,
debug: true,
Expand Down Expand Up @@ -74,8 +76,10 @@ describe('Settings spec component', () => {
rendering: 'svg' as Rendering,
animateData: true,
showLegend: true,
tooltipType: TooltipType.None,
tooltipSnap: false,
tooltip: {
type: TooltipType.None,
snap: false,
},
legendPosition: Position.Bottom,
showLegendDisplayValue: false,
debug: true,
Expand Down
41 changes: 30 additions & 11 deletions src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DomainRange, Position, Rendering, Rotation } from '../lib/series/specs'
import { LIGHT_THEME } from '../lib/themes/light_theme';
import { Theme } from '../lib/themes/theme';
import { Domain } from '../lib/utils/domain';
import { TooltipType } from '../lib/utils/interactions';
import { TooltipType, TooltipValueFormatter } from '../lib/utils/interactions';
import {
BrushEndListener,
ChartStore,
Expand All @@ -16,17 +16,29 @@ import {
export const DEFAULT_TOOLTIP_TYPE = TooltipType.VerticalCursor;
export const DEFAULT_TOOLTIP_SNAP = true;

interface TooltipProps {
type?: TooltipType;
snap?: boolean;
headerFormatter?: TooltipValueFormatter;
}

function isTooltipProps(config: TooltipType | TooltipProps): config is TooltipProps {
return typeof config === 'object';
}

function isTooltipType(config: TooltipType | TooltipProps): config is TooltipType {
return typeof config === 'string';
}

interface SettingSpecProps {
chartStore?: ChartStore;
theme?: Theme;
rendering: Rendering;
rotation: Rotation;
animateData: boolean;
showLegend: boolean;
/** Specify the tooltip type */
tooltipType?: TooltipType;
/** Snap tooltip to grid */
tooltipSnap?: boolean;
/** Either a TooltipType or an object with configuration of type, snap, and/or headerFormatter */
tooltip?: TooltipType | TooltipProps;
debug: boolean;
legendPosition?: Position;
showLegendDisplayValue: boolean;
Expand All @@ -50,8 +62,7 @@ function updateChartStore(props: SettingSpecProps) {
rendering,
animateData,
showLegend,
tooltipType,
tooltipSnap,
tooltip,
legendPosition,
showLegendDisplayValue,
onElementClick,
Expand All @@ -75,8 +86,14 @@ function updateChartStore(props: SettingSpecProps) {
chartStore.animateData = animateData;
chartStore.debug = debug;

chartStore.tooltipType.set(tooltipType!);
chartStore.tooltipSnap.set(tooltipSnap!);
if (tooltip && isTooltipProps(tooltip)) {
const { type, snap, headerFormatter } = tooltip;
chartStore.tooltipType.set(type!);
chartStore.tooltipSnap.set(snap!);
chartStore.tooltipHeaderFormatter = headerFormatter;
} else if (tooltip && isTooltipType(tooltip)) {
chartStore.tooltipType.set(tooltip);
}

chartStore.setShowLegend(showLegend);
chartStore.legendPosition = legendPosition;
Expand Down Expand Up @@ -119,8 +136,10 @@ export class SettingsComponent extends PureComponent<SettingSpecProps> {
animateData: true,
showLegend: false,
debug: false,
tooltipType: DEFAULT_TOOLTIP_TYPE,
tooltipSnap: DEFAULT_TOOLTIP_SNAP,
tooltip: {
type: DEFAULT_TOOLTIP_TYPE,
snap: DEFAULT_TOOLTIP_SNAP,
},
showLegendDisplayValue: true,
};
componentDidMount() {
Expand Down
33 changes: 33 additions & 0 deletions src/state/chart_state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,39 @@ describe('Chart Store', () => {
expect(store.isTooltipVisible.get()).toBe(true);
});

describe('can use a custom tooltip header formatter', () => {
beforeEach(() => {
const axisSpec: AxisSpec = {
id: AXIS_ID,
groupId: spec.groupId,
hide: true,
showOverlappingTicks: false,
showOverlappingLabels: false,
position: Position.Bottom,
tickSize: 30,
tickPadding: 10,
tickFormat: (value: any) => `foo ${value}`,
};

store.addAxisSpec(axisSpec);
store.addSeriesSpec(spec);
store.tooltipType.set(TooltipType.Crosshairs);
store.computeChart();
});

test('with no tooltipHeaderFormatter defined, should return value formatted using xAxis tickFormatter', () => {
store.tooltipHeaderFormatter = undefined;
store.setCursorPosition(10, 10);
expect(store.tooltipData[0].value).toBe('foo 1');
});

test('with tooltipHeaderFormatter defined, should return value formatted', () => {
store.tooltipHeaderFormatter = (value: TooltipValue) => `${value}`;
store.setCursorPosition(10, 10);
expect(store.tooltipData[0].value).toBe(1);
});
});

test('can disable brush based on scale and listener', () => {
store.xScale = undefined;
expect(store.isBrushEnabled()).toBe(false);
Expand Down
36 changes: 18 additions & 18 deletions src/state/chart_state.timescales.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ describe('Render chart', () => {
test('check mouse position correctly return inverted value', () => {
store.setCursorPosition(15, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${day1}`); // x value
expect(store.tooltipData[1].value).toBe('10'); // y value
expect(store.tooltipData[0].value).toBe(day1); // x value
expect(store.tooltipData[1].value).toBe(10); // y value
store.setCursorPosition(35, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${day2}`); // x value
expect(store.tooltipData[1].value).toBe('22'); // y value
expect(store.tooltipData[0].value).toBe(day2); // x value
expect(store.tooltipData[1].value).toBe(22); // y value
store.setCursorPosition(76, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${day3}`); // x value
expect(store.tooltipData[1].value).toBe('6'); // y value
expect(store.tooltipData[0].value).toBe(day3); // x value
expect(store.tooltipData[1].value).toBe(6); // y value
});
});
describe('line, utc-time, 5m interval', () => {
Expand Down Expand Up @@ -97,16 +97,16 @@ describe('Render chart', () => {
test('check mouse position correctly return inverted value', () => {
store.setCursorPosition(15, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date1}`); // x value
expect(store.tooltipData[1].value).toBe('10'); // y value
expect(store.tooltipData[0].value).toBe(date1); // x value
expect(store.tooltipData[1].value).toBe(10); // y value
store.setCursorPosition(35, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date2}`); // x value
expect(store.tooltipData[1].value).toBe('22'); // y value
expect(store.tooltipData[0].value).toBe(date2); // x value
expect(store.tooltipData[1].value).toBe(22); // y value
store.setCursorPosition(76, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date3}`); // x value
expect(store.tooltipData[1].value).toBe('6'); // y value
expect(store.tooltipData[0].value).toBe(date3); // x value
expect(store.tooltipData[1].value).toBe(6); // y value
});
});
describe('line, non utc-time, 5m + 1s interval', () => {
Expand Down Expand Up @@ -164,16 +164,16 @@ describe('Render chart', () => {
test('check mouse position correctly return inverted value', () => {
store.setCursorPosition(15, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date1}`); // x value
expect(store.tooltipData[1].value).toBe('10'); // y value
expect(store.tooltipData[0].value).toBe(date1); // x value
expect(store.tooltipData[1].value).toBe(10); // y value
store.setCursorPosition(35, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date2}`); // x value
expect(store.tooltipData[1].value).toBe('22'); // y value
expect(store.tooltipData[0].value).toBe(date2); // x value
expect(store.tooltipData[1].value).toBe(22); // y value
store.setCursorPosition(76, 10); // check first valid tooltip
expect(store.tooltipData.length).toBe(2); // x value + y value
expect(store.tooltipData[0].value).toBe(`${date3}`); // x value
expect(store.tooltipData[1].value).toBe('6'); // y value
expect(store.tooltipData[0].value).toBe(date3); // x value
expect(store.tooltipData[1].value).toBe(6); // y value
});
});
});
6 changes: 5 additions & 1 deletion src/state/chart_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
isFollowTooltipType,
TooltipType,
TooltipValue,
TooltipValueFormatter,
} from '../lib/utils/interactions';
import { Scale, ScaleType } from '../lib/utils/scales/scales';
import { DEFAULT_TOOLTIP_SNAP, DEFAULT_TOOLTIP_TYPE } from '../specs/settings';
Expand Down Expand Up @@ -175,6 +176,7 @@ export class ChartStore {
tooltipType = observable.box(DEFAULT_TOOLTIP_TYPE);
tooltipSnap = observable.box(DEFAULT_TOOLTIP_SNAP);
tooltipPosition = observable.object<{ transform: string }>({ transform: '' });
tooltipHeaderFormatter?: TooltipValueFormatter;

/** cursorPosition is used by tooltip, so this is a way to expose the position for other uses */
rawCursorPosition = observable.object<{ x: number; y: number }>({ x: -1, y: -1 }, undefined, {
Expand Down Expand Up @@ -377,7 +379,9 @@ export class ChartStore {

// format only one time the x value
if (!xValueInfo) {
xValueInfo = formatTooltip(indexedGeometry, spec, true, false, xAxis);
// if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter
const formatterAxis = this.tooltipHeaderFormatter ? undefined : xAxis;
xValueInfo = formatTooltip(indexedGeometry, spec, true, false, formatterAxis);
return [xValueInfo, ...acc, formattedTooltip];
}

Expand Down
6 changes: 5 additions & 1 deletion stories/bar_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -915,9 +915,13 @@ storiesOf('Bar Chart', module)
.add('with high data volume', () => {
const dg = new DataGenerator();
const data = dg.generateSimpleSeries(15000);
const tooltipProps = {
type: TooltipType.Follow,
};

return (
<Chart className={'story-chart'}>
<Settings tooltipType={TooltipType.Follow} />
<Settings tooltip={tooltipProps} />
<Axis id={getAxisId('bottom')} position={Position.Bottom} title={'Bottom axis'} />
<Axis
id={getAxisId('left2')}
Expand Down
Loading

0 comments on commit bd181b5

Please sign in to comment.