Skip to content

Commit 8e4acd2

Browse files
authored
LG-4661: Custom Charts Tooltip (#2769)
* Working state * WIP * Correct styling * Seperate concerns * Restructure and rename * Neaten up tooltip * More organization * Improve typing * Change params to series * Properly pass formatters * Add custom formats story * Small style changes * Fix sorting * Create spec file * Fix dark mode * Replace sort props with compare fn * Add spec for CustomTooltip * Remove sort key and direction * Fix charts stories * Fix types * Fix import * Changeset * Undo accidental glyphs changes * Remove marker from tooltip story * Fix format test * CR changes * Fix test * Fix story * Fix line dot color * Add axisValueFormatter * Add test * Update README * Fix types and deps * CR changes * Update changeset
1 parent 1d8408d commit 8e4acd2

36 files changed

+1036
-182
lines changed

.changeset/tough-mails-reflect.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@lg-charts/core': minor
3+
---
4+
5+
Makes various improvements to the `Tooltip` component
6+
- Adds `headerFormatter` prop to allow formatting of header in Tooltip.
7+
- Adds `seriesNameFormatter` prop to allow formatting of series names.
8+
- Updates prop `valueFormatter` to `seriesValueFormatter`, which can now return a `ReactNode` in addition to a string.
9+
- Makes multiple style improvements.
10+
- Replaces `sortDirection` and `sortKey` props with `sort` prop that accepts a compare function to be used for custom sorting.

charts/chart-card/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"@leafygreen-ui/icon": "workspace:^",
2323
"@leafygreen-ui/icon-button": "workspace:^",
2424
"@leafygreen-ui/lib": "workspace:^",
25-
"@leafygreen-ui/palette": "workspace:^",
2625
"@leafygreen-ui/tokens": "workspace:^",
2726
"@leafygreen-ui/typography": "workspace:^"
2827
},

charts/chart-card/tsconfig.json

-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@
3333
{
3434
"path": "../../packages/lib"
3535
},
36-
{
37-
"path": "../../packages/palette"
38-
},
3936
{
4037
"path": "../../packages/tokens"
4138
},

charts/core/README.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,14 @@ Renders a tooltip onto the chart.
161161

162162
#### Props
163163

164-
| Name | Description | Type | Default |
165-
| ----------------------------- | --------------------------------------------------- | ------------------------------------- | --------- |
166-
| `sortDirection` _(optional)_ | What direction to sort tooltip values in. | `'asc' \| 'desc'` | `'desc'` |
167-
| `sortKey` _(optional)_ | Whether to sort by name or value. | `'name' \| 'value'` | `'value'` |
168-
| `valueFormatter` _(optional)_ | Callback function for formatting each value string. | `(value: number \| string) => string` | |
164+
| Name | Description | Type | Default |
165+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------- |
166+
| `sort` _(optional)_ | Custom sort function, used to sort list of series. List will be sorted descending by value by default. | `(seriesA: SeriesInfo, seriesB: SeriesInfo) => number` | _descending by default_ |
167+
| `seriesNameFormatter` _(optional)_ | Callback function for formatting the name string for each series. | `(name: string) => string \| ReactNode` | |
168+
| `seriesValueFormatter` _(optional)_ | Callback function for formatting the value string for each series. | `(value: number \| string \| Date) => string \| ReactNode` | |
169+
| `headerFormatter` _(optional)_ | Callback function for formatting the header string. | `(value: number \| string) => string \| ReactNode` | |
170+
171+
Note: `SeriesInfo` is of type `{ name: string; value: string | number | Date; }`
169172

170173
### `EventMarkerLine`
171174

charts/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"devDependencies": {
3535
"@faker-js/faker": "8.0.2",
36+
"@leafygreen-ui/icon": "workspace:^",
3637
"@types/lodash.debounce": "^4.0.9"
3738
},
3839
"repository": {

charts/core/src/Chart.stories.tsx

+12-35
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { StoryObj } from '@storybook/react';
44

55
import { ChartProps } from './Chart/Chart.types';
66
import { HeaderProps } from './Header/Header.types';
7-
import { SortDirection, SortKey, TooltipProps } from './Tooltip/Tooltip.types';
7+
import { TooltipProps } from './Tooltip/Tooltip.types';
88
import { LineProps } from './Line';
99
import { makeLineData } from './testUtils';
1010
import { ThresholdLineProps } from './ThresholdLine';
@@ -56,8 +56,13 @@ export default {
5656
};
5757

5858
export const LiveExample: StoryObj<{
59+
darkMode: boolean;
5960
data: Array<LineProps>;
6061
state: ChartProps['state'];
62+
zoomSelect: ChartProps['zoomSelect'];
63+
onZoomSelect: ChartProps['onZoomSelect'];
64+
onChartReady: ChartProps['onChartReady'];
65+
groupId: ChartProps['groupId'];
6166
verticalGridLines: boolean;
6267
horizontalGridLines: boolean;
6368
renderGrid: boolean;
@@ -70,9 +75,7 @@ export const LiveExample: StoryObj<{
7075
yAxisFormatter: YAxisProps['formatter'];
7176
yAxisLabel: YAxisProps['label'];
7277
renderTooltip: boolean;
73-
tooltipSortDirection: TooltipProps['sortDirection'];
74-
tooltipSortKey: TooltipProps['sortKey'];
75-
tooltipValueFormatter: TooltipProps['valueFormatter'];
78+
tooltipSeriesValueFormatter: TooltipProps['seriesValueFormatter'];
7679
renderHeader: boolean;
7780
headerTitle: HeaderProps['title'];
7881
headerShowDivider: HeaderProps['showDivider'];
@@ -108,8 +111,6 @@ export const LiveExample: StoryObj<{
108111
yAxisType: 'value',
109112
yAxisLabel: 'Y-Axis Label',
110113
renderTooltip: true,
111-
tooltipSortDirection: SortDirection.Desc,
112-
tooltipSortKey: SortKey.Value,
113114
renderHeader: true,
114115
headerTitle: 'LeafyGreen Chart Header',
115116
headerShowDivider: true,
@@ -252,27 +253,9 @@ export const LiveExample: StoryObj<{
252253
category: 'Tooltip',
253254
},
254255
},
255-
tooltipSortDirection: {
256-
control: 'select',
257-
options: SortDirection,
258-
description: 'Direction to sort tooltip values',
259-
name: 'SortDirection',
260-
table: {
261-
category: 'Tooltip',
262-
},
263-
},
264-
tooltipSortKey: {
265-
control: 'select',
266-
options: SortKey,
267-
description: 'Which key to sort tooltip values by',
268-
name: 'SortKey',
269-
table: {
270-
category: 'Tooltip',
271-
},
272-
},
273-
tooltipValueFormatter: {
274-
description: 'Tooltip value formatter',
275-
name: 'ValueFormatter',
256+
tooltipSeriesValueFormatter: {
257+
description: 'Tooltip series value formatter',
258+
name: 'SeriesValueFormatter',
276259
table: {
277260
disable: true,
278261
},
@@ -465,9 +448,7 @@ export const LiveExample: StoryObj<{
465448
xAxisLabel,
466449
yAxisLabel,
467450
renderTooltip,
468-
tooltipSortDirection,
469-
tooltipSortKey,
470-
tooltipValueFormatter,
451+
tooltipSeriesValueFormatter,
471452
renderHeader,
472453
headerTitle,
473454
headerShowDivider,
@@ -521,11 +502,7 @@ export const LiveExample: StoryObj<{
521502
<Grid vertical={verticalGridLines} horizontal={horizontalGridLines} />
522503
)}
523504
{renderTooltip && (
524-
<Tooltip
525-
sortDirection={tooltipSortDirection}
526-
sortKey={tooltipSortKey}
527-
valueFormatter={tooltipValueFormatter}
528-
/>
505+
<Tooltip seriesValueFormatter={tooltipSeriesValueFormatter} />
529506
)}
530507
{renderXAxis && (
531508
<XAxis

charts/core/src/Line/Line.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export function Line({ name, data }: LineProps) {
3131
...defaultLineOptions.lineStyle,
3232
color,
3333
},
34+
itemStyle: {
35+
...defaultLineOptions.itemStyle,
36+
color,
37+
},
3438
});
3539
} else {
3640
chart.removeSeries(name);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import { CustomTooltip } from './CustomTooltip';
5+
import { CustomTooltipProps } from './CustomTooltip.types';
6+
7+
const baseSeriesData = {
8+
componentType: 'series',
9+
componentSubType: 'line',
10+
componentIndex: 0,
11+
seriesType: 'line',
12+
seriesIndex: 0,
13+
seriesId: '\u0000cluster2-shard-00-00-stuvwx3456.mongodb.net:27017\u00000',
14+
dataIndex: 18,
15+
color: '#016BF8',
16+
dimensionNames: ['x', 'y'],
17+
encode: {
18+
x: [0],
19+
y: [1],
20+
},
21+
$vars: ['seriesName', 'name', 'value'],
22+
axisDim: 'x',
23+
axisIndex: 0,
24+
axisType: 'xAxis.time',
25+
axisId: '\u0000X-Axis Label\u00000',
26+
axisValue: 1704086280000,
27+
axisValueLabel: '2024-01-01 00:18:00',
28+
marker:
29+
'<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:#016BF8;"></span>',
30+
};
31+
32+
const mockSeriesData: CustomTooltipProps['seriesData'] = [
33+
{
34+
...baseSeriesData,
35+
data: ['Series 1', 100],
36+
value: ['Series 1', 100],
37+
seriesName: 'Series 1',
38+
name: 'Series 1',
39+
},
40+
{
41+
...baseSeriesData,
42+
data: ['Series 3', 300],
43+
value: ['Series 3', 300],
44+
seriesName: 'Series 3',
45+
name: 'Series 3',
46+
},
47+
{
48+
...baseSeriesData,
49+
data: ['Series 2', 200],
50+
value: ['Series 2', 200],
51+
seriesName: 'Series 2',
52+
name: 'Series 2',
53+
},
54+
];
55+
56+
function descendingCompareFn(
57+
valueA: string | number | Date,
58+
valueB: string | number | Date,
59+
) {
60+
if (valueA < valueB) {
61+
return -1;
62+
}
63+
64+
if (valueA > valueB) {
65+
return 1;
66+
}
67+
68+
return 0;
69+
}
70+
71+
const renderCustomTooltip = (props: Partial<CustomTooltipProps> = {}) => {
72+
const defaultProps: CustomTooltipProps = {
73+
seriesData: mockSeriesData,
74+
};
75+
76+
return render(<CustomTooltip {...defaultProps} {...props} />);
77+
};
78+
79+
describe('@lg-charts/core/Tooltip/CustomTooltip', () => {
80+
test('should render properly formatted date', () => {
81+
renderCustomTooltip();
82+
const dateElement = screen.getByText(
83+
/\d{4}\/\d{2}\/\d{2}\/\d{2}:\d{2}:\d{2}/,
84+
);
85+
expect(dateElement).toBeInTheDocument();
86+
});
87+
88+
test('should render series list sorted desc by value by default', () => {
89+
renderCustomTooltip();
90+
91+
const seriesElements = screen.getAllByText(/Series/);
92+
expect(seriesElements[0]).toHaveTextContent('Series 3');
93+
expect(seriesElements[1]).toHaveTextContent('Series 2');
94+
expect(seriesElements[2]).toHaveTextContent('Series 1');
95+
});
96+
97+
test('should reorder list according to sort function', () => {
98+
renderCustomTooltip({
99+
sort: (seriesA, seriesB) =>
100+
descendingCompareFn(seriesA.value, seriesB.value),
101+
});
102+
103+
const seriesElements = screen.getAllByText(/Series/);
104+
expect(seriesElements[0]).toHaveTextContent('Series 1');
105+
expect(seriesElements[1]).toHaveTextContent('Series 2');
106+
expect(seriesElements[2]).toHaveTextContent('Series 3');
107+
});
108+
109+
test('should render custom series name with seriesNameFormatter', () => {
110+
renderCustomTooltip({
111+
seriesNameFormatter: (name: string) => `Name: ${name}`,
112+
});
113+
114+
expect(screen.getByText('Name: Series 1')).toBeInTheDocument();
115+
expect(screen.getByText('Name: Series 2')).toBeInTheDocument();
116+
expect(screen.getByText('Name: Series 3')).toBeInTheDocument();
117+
});
118+
119+
test('should render custom series value with seriesValueFormatter', () => {
120+
renderCustomTooltip({
121+
seriesValueFormatter: (value: number) => `$${value}`,
122+
});
123+
124+
expect(screen.getByText('$100')).toBeInTheDocument();
125+
expect(screen.getByText('$200')).toBeInTheDocument();
126+
expect(screen.getByText('$300')).toBeInTheDocument();
127+
});
128+
129+
test('should render custom axis value with headerFormatter', () => {
130+
renderCustomTooltip({
131+
headerFormatter: () => `Axis Value: test`,
132+
});
133+
134+
expect(screen.getByText('Axis Value: test')).toBeInTheDocument();
135+
});
136+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import { storybookArgTypes } from '@lg-tools/storybook-utils';
3+
import type { StoryObj } from '@storybook/react';
4+
5+
import Icon from '@leafygreen-ui/icon';
6+
7+
import { CustomTooltip } from './CustomTooltip';
8+
import { sampleTooltipParams } from './CustomTooltip.testUtils';
9+
import { CustomTooltipProps } from './CustomTooltip.types';
10+
11+
export default {
12+
title: 'Charts/Tooltip',
13+
component: CustomTooltip,
14+
args: {
15+
seriesData: sampleTooltipParams,
16+
},
17+
argTypes: {
18+
darkMode: storybookArgTypes.darkMode,
19+
seriesNameFormatter: {
20+
table: {
21+
disable: true,
22+
},
23+
},
24+
seriesValueFormatter: {
25+
table: {
26+
disable: true,
27+
},
28+
},
29+
},
30+
parameters: {
31+
generate: {
32+
combineArgs: {
33+
darkMode: [false, true],
34+
},
35+
},
36+
},
37+
};
38+
39+
export const Default: StoryObj<CustomTooltipProps> = {
40+
args: {
41+
seriesData: sampleTooltipParams.map((series, idx) => ({
42+
...series,
43+
seriesName: 'Series ' + (idx + 1),
44+
})),
45+
},
46+
};
47+
48+
export const Generated = () => {};
49+
50+
export const LongSeriesNames: StoryObj<CustomTooltipProps> = {};
51+
52+
export const CustomFormats: StoryObj<CustomTooltipProps> = {
53+
args: {
54+
seriesNameFormatter: (name: number | string | Date) => {
55+
const formattedName =
56+
name instanceof Date ? name.toLocaleDateString() : name;
57+
58+
const concatenatedName =
59+
typeof formattedName === 'string'
60+
? `${formattedName.substring(0, 16)}...${formattedName.slice(-5)}`
61+
: formattedName;
62+
63+
return (
64+
<div
65+
style={{
66+
display: 'grid',
67+
gridTemplateColumns: 'auto 1fr',
68+
gap: '4px',
69+
}}
70+
>
71+
<Icon glyph="Secondary" /> <span>{concatenatedName}</span>
72+
</div>
73+
);
74+
},
75+
76+
seriesValueFormatter: (value: number | string | Date) => {
77+
const formattedValue =
78+
value instanceof Date ? value.toLocaleDateString() : value;
79+
80+
return `${formattedValue} m/s`;
81+
},
82+
},
83+
};

0 commit comments

Comments
 (0)