Skip to content

Commit 68b99ce

Browse files
committed
Render default keys and values in tooltip if not defined
1 parent 9626152 commit 68b99ce

File tree

3 files changed

+161
-117
lines changed

3 files changed

+161
-117
lines changed

src/cartesian-chart/__tests__/cartesian-chart-tooltip.test.tsx

Lines changed: 146 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -112,115 +112,168 @@ describe("CartesianChart: tooltip", () => {
112112
});
113113
});
114114

115-
test("customizes series rendering", async () => {
115+
describe("Custom tooltip content", () => {
116+
const series: CartesianChartProps.SeriesOptions[] = [
117+
{ type: "line", id: "l", name: "Line", data: [{ x: 1, y: 2 }] },
118+
{ type: "errorbar", linkedTo: "l", name: "Error", data: [{ x: 1, low: 1, high: 4 }] },
119+
{ type: "x-threshold", name: "Threshold", value: 1 },
120+
];
121+
116122
const onClickValue = vi.fn();
117-
renderCartesianChart({
118-
highcharts,
119-
series: [
120-
{ type: "line", id: "l", name: "Line", data: [{ x: 1, y: 2 }] },
121-
{ type: "errorbar", linkedTo: "l", name: "Error", data: [{ x: 1, low: 1, high: 4 }] },
122-
{ type: "x-threshold", name: "Threshold", value: 1 },
123-
],
124-
tooltip: {
125-
point({ item }) {
126-
return {
127-
key: <span>{item.series.name}</span>,
128-
value: <button onClick={() => onClickValue("root")}>{item.y ?? "T"}</button>,
129-
expandable: item.series.name === "Line",
130-
subItems:
131-
item.series.name === "Line"
132-
? [
133-
{
134-
key: <span>sub-1 key</span>,
135-
value: <button onClick={() => onClickValue("sub-1")}>sub-1 value</button>,
136-
},
137-
{
138-
key: <span>sub-2 key</span>,
139-
value: <button onClick={() => onClickValue("sub-2")}>sub-2 value</button>,
140-
},
141-
]
142-
: [],
143-
description: item.errorRanges?.length ? `${item.errorRanges[0].low} - ${item.errorRanges[0].high}` : null,
144-
};
145-
},
146-
},
147-
});
123+
const customKey = (item: CartesianChartProps.TooltipPointItem) => (
124+
<span>{`Custom name for ${item.series.name}`}</span>
125+
);
126+
const customValue = (item: CartesianChartProps.TooltipPointItem) => (
127+
<button onClick={() => onClickValue("root")}>{item.y ?? "T"}</button>
128+
);
129+
const customSubItems = (item: CartesianChartProps.TooltipPointItem) =>
130+
item.series.name === "Line"
131+
? [
132+
{
133+
key: <span>sub-1 key</span>,
134+
value: <button onClick={() => onClickValue("sub-1")}>sub-1 value</button>,
135+
},
136+
{
137+
key: <span>sub-2 key</span>,
138+
value: <button onClick={() => onClickValue("sub-2")}>sub-2 value</button>,
139+
},
140+
]
141+
: [];
142+
const customDescription = (item: CartesianChartProps.TooltipPointItem) =>
143+
item.errorRanges?.length ? `${item.errorRanges[0].low} - ${item.errorRanges[0].high}` : null;
148144

149-
act(() => hc.highlightChartPoint(0, 0));
145+
const openTooltip = async () => {
146+
act(() => hc.highlightChartPoint(0, 0));
150147

151-
await waitFor(() => {
152-
expect(getTooltip()).not.toBe(null);
148+
await waitFor(() => {
149+
expect(getTooltip()).not.toBe(null);
150+
});
151+
152+
expect(getTooltipHeader().getElement().textContent).toBe("1");
153+
expect(getAllTooltipSeries()).toHaveLength(2);
154+
};
155+
156+
const expectCustomSubItems = () => {
157+
expect(getTooltipSeries(0).find('[aria-expanded="false"]')).not.toBe(null);
158+
getTooltipSeries(0).find('[aria-expanded="false"]')!.click({ bubbles: false });
159+
expect(getTooltipSeries(0).findSubItems()).toHaveLength(2);
160+
expect(getTooltipSeries(0).findSubItems()[1].findKey().getElement().textContent).toBe("sub-2 key");
161+
expect(getTooltipSeries(0).findSubItems()[1].findValue().getElement().textContent).toBe("sub-2 value");
162+
getTooltipSeries(0).findSubItems()[1].findValue().find("button")!.click();
163+
expect(onClickValue).toHaveBeenCalledWith("sub-2");
164+
expect(getTooltipSeries(1).find('[aria-expanded="false"]')).toBe(null);
165+
};
166+
167+
afterEach(() => {
168+
onClickValue.mockReset();
153169
});
154170

155-
expect(getTooltipHeader().getElement().textContent).toBe("1");
156-
expect(getAllTooltipSeries()).toHaveLength(2);
171+
test("customizes key, value, sub-items and description", async () => {
172+
renderCartesianChart({
173+
highcharts,
174+
series,
175+
tooltip: {
176+
point({ item }) {
177+
return {
178+
key: customKey(item),
179+
value: customValue(item),
180+
expandable: item.series.name === "Line",
181+
subItems: customSubItems(item),
182+
description: customDescription(item),
183+
};
184+
},
185+
},
186+
});
157187

158-
expect(getTooltipSeries(0).findKey().getElement().textContent).toBe("Line");
159-
expect(getTooltipSeries(0).findValue().getElement().textContent).toBe("2");
160-
expect(getTooltipSeries(0).find('[aria-expanded="false"]')).not.toBe(null);
188+
await openTooltip();
161189

162-
getTooltipSeries(0).find('[aria-expanded="false"]')!.click({ bubbles: false });
163-
expect(getTooltipSeries(0).findSubItems()).toHaveLength(2);
164-
expect(getTooltipSeries(0).findSubItems()[1].findKey().getElement().textContent).toBe("sub-2 key");
165-
expect(getTooltipSeries(0).findSubItems()[1].findValue().getElement().textContent).toBe("sub-2 value");
190+
expect(getTooltipSeries(0).findKey().getElement().textContent).toBe("Custom name for Line");
191+
expect(getTooltipSeries(0).findValue().getElement().textContent).toBe("2");
166192

167-
getTooltipSeries(0).findValue().find("button")!.click();
168-
expect(onClickValue).toHaveBeenCalledWith("root");
193+
expectCustomSubItems();
169194

170-
getTooltipSeries(0).findSubItems()[1].findValue().find("button")!.click();
171-
expect(onClickValue).toHaveBeenCalledWith("sub-2");
195+
getTooltipSeries(0).findValue().find("button")!.click();
196+
expect(onClickValue).toHaveBeenCalledWith("root");
172197

173-
expect(getTooltipSeries(0).findDescription().getElement().textContent).toBe("1 - 4");
198+
expect(getTooltipSeries(0).findDescription().getElement().textContent).toBe("1 - 4");
174199

175-
expect(getTooltipSeries(1).findKey().getElement().textContent).toBe("Threshold");
176-
expect(getTooltipSeries(1).findValue().getElement().textContent).toBe("T");
177-
expect(getTooltipSeries(1).find('[aria-expanded="false"]')).toBe(null);
178-
});
200+
expect(getTooltipSeries(1).findKey().getElement().textContent).toBe("Custom name for Threshold");
201+
expect(getTooltipSeries(1).findValue().getElement().textContent).toBe("T");
202+
});
179203

180-
test("customizes tooltip slots", async () => {
181-
renderCartesianChart({
182-
highcharts,
183-
series: [
184-
{ type: "line", id: "l", name: "Line", data: [{ x: 1, y: 2 }] },
185-
{ type: "x-threshold", name: "Threshold", value: 1 },
186-
{ type: "errorbar", name: "Error", data: [{ x: 1, low: 3, high: 4 }], linkedTo: "l" },
187-
],
188-
tooltip: {
189-
header({ x, items }) {
190-
return (
191-
<span>
192-
header {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
193-
{items[0].errorRanges![0].high} {items[1].series.name}
194-
</span>
195-
);
196-
},
197-
body({ x, items }) {
198-
return (
199-
<span>
200-
body {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
201-
{items[0].errorRanges![0].high} {items[1].series.name}
202-
</span>
203-
);
204-
},
205-
footer({ x, items }) {
206-
return (
207-
<span>
208-
footer {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
209-
{items[0].errorRanges![0].high} {items[1].series.name}
210-
</span>
211-
);
204+
test("renders default key and value if not defined", async () => {
205+
renderCartesianChart({
206+
highcharts,
207+
series,
208+
tooltip: {
209+
point({ item }) {
210+
return {
211+
expandable: item.series.name === "Line",
212+
subItems: customSubItems(item),
213+
description: customDescription(item),
214+
};
215+
},
212216
},
213-
},
214-
});
217+
});
215218

216-
act(() => hc.highlightChartPoint(0, 0));
219+
await openTooltip();
217220

218-
await waitFor(() => {
219-
expect(getTooltip()).not.toBe(null);
221+
expect(getTooltipSeries(0).findKey().getElement().textContent).toBe("Line");
222+
expect(getTooltipSeries(0).findValue().getElement().textContent).toBe("2");
223+
224+
expectCustomSubItems();
225+
226+
expect(getTooltipSeries(0).findDescription().getElement().textContent).toBe("1 - 4");
227+
228+
expect(getTooltipSeries(1).findKey().getElement().textContent).toBe("Threshold");
229+
expect(getTooltipSeries(1).findValue().getElement().textContent).toBe(""); // X threshold has no y value
220230
});
221231

222-
expect(getTooltipHeader().getElement().textContent).toBe("header 1 2 Line 3 4 Threshold");
223-
expect(getTooltipBody().getElement().textContent).toBe("body 1 2 Line 3 4 Threshold");
224-
expect(getTooltipFooter().getElement().textContent).toBe("footer 1 2 Line 3 4 Threshold");
232+
test("customizes tooltip slots", async () => {
233+
renderCartesianChart({
234+
highcharts,
235+
series: [
236+
{ type: "line", id: "l", name: "Line", data: [{ x: 1, y: 2 }] },
237+
{ type: "x-threshold", name: "Threshold", value: 1 },
238+
{ type: "errorbar", name: "Error", data: [{ x: 1, low: 3, high: 4 }], linkedTo: "l" },
239+
],
240+
tooltip: {
241+
header({ x, items }) {
242+
return (
243+
<span>
244+
header {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
245+
{items[0].errorRanges![0].high} {items[1].series.name}
246+
</span>
247+
);
248+
},
249+
body({ x, items }) {
250+
return (
251+
<span>
252+
body {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
253+
{items[0].errorRanges![0].high} {items[1].series.name}
254+
</span>
255+
);
256+
},
257+
footer({ x, items }) {
258+
return (
259+
<span>
260+
footer {x} {items.length} {items[0].series.name} {items[0].errorRanges![0].low}{" "}
261+
{items[0].errorRanges![0].high} {items[1].series.name}
262+
</span>
263+
);
264+
},
265+
},
266+
});
267+
268+
act(() => hc.highlightChartPoint(0, 0));
269+
270+
await waitFor(() => {
271+
expect(getTooltip()).not.toBe(null);
272+
});
273+
274+
expect(getTooltipHeader().getElement().textContent).toBe("header 1 2 Line 3 4 Threshold");
275+
expect(getTooltipBody().getElement().textContent).toBe("body 1 2 Line 3 4 Threshold");
276+
expect(getTooltipFooter().getElement().textContent).toBe("footer 1 2 Line 3 4 Threshold");
277+
});
225278
});
226279
});

src/cartesian-chart/interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ export namespace CartesianChartProps {
178178
series: NonErrorBarSeriesOptions;
179179
}
180180
export interface TooltipPointFormatted {
181-
key: React.ReactNode;
182-
value: React.ReactNode;
181+
key?: React.ReactNode;
182+
value?: React.ReactNode;
183183
description?: React.ReactNode;
184184
expandable?: boolean;
185185
subItems?: ReadonlyArray<{ key: React.ReactNode; value: React.ReactNode }>;

src/core/components/core-tooltip.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
CoreTooltipOptions,
1919
GetTooltipContent,
2020
GetTooltipContentProps,
21-
TooltipPointFormatted,
2221
TooltipSlotProps,
2322
} from "../interfaces";
2423
import { getPointColor, getSeriesColor, getSeriesId, getSeriesMarkerType, isXThreshold } from "../utils";
@@ -150,16 +149,18 @@ function getTooltipContentCartesian(
150149
const matchedItems = findTooltipSeriesItems(chart.series, group);
151150
const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => {
152151
const valueFormatter = getFormatter(item.point.series.yAxis);
153-
const formatted: TooltipPointFormatted = (() => {
154-
// Using consumer-defined details.
155-
if (renderers.point) {
156-
return renderers.point({ item });
157-
}
158-
const itemY = isXThreshold(item.point.series) ? null : (item.point.y ?? null);
159-
return {
160-
key: item.point.series.name,
161-
value: valueFormatter(itemY),
162-
description: item.errorRanges.length ? (
152+
const itemY = isXThreshold(item.point.series) ? null : (item.point.y ?? null);
153+
const customContent = renderers.point ? renderers.point({ item }) : null;
154+
return {
155+
key: customContent?.key ?? item.point.series.name,
156+
value: customContent?.value ?? valueFormatter(itemY),
157+
marker: getSeriesMarker(item.point.series),
158+
subItems: customContent?.subItems,
159+
expandableId: customContent?.expandable ? item.point.series.name : undefined,
160+
highlighted: item.point.x === point?.x && item.point.y === point?.y,
161+
description:
162+
customContent?.description ??
163+
(item.errorRanges.length ? (
163164
<div>
164165
{item.errorRanges.map((errorBarPoint, index) => (
165166
<div key={index} className={styles["error-range"]}>
@@ -170,17 +171,7 @@ function getTooltipContentCartesian(
170171
</div>
171172
))}
172173
</div>
173-
) : null,
174-
};
175-
})();
176-
return {
177-
key: formatted.key,
178-
value: formatted.value,
179-
marker: getSeriesMarker(item.point.series),
180-
subItems: formatted.subItems,
181-
expandableId: formatted.expandable ? item.point.series.name : undefined,
182-
description: formatted.description,
183-
highlighted: item.point.x === point?.x && item.point.y === point?.y,
174+
) : null),
184175
};
185176
});
186177
if (chart.options.plotOptions?.series?.stacking) {

0 commit comments

Comments
 (0)