Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,21 @@ for each visited { x, y } point.",
"inlineType": {
"name": "CoreChartProps.TooltipOptions",
"properties": [
{
"inlineType": {
"name": "number | boolean",
"type": "union",
"valueDescriptions": undefined,
"values": [
"number",
"false",
"true",
],
},
"name": "debounce",
"optional": true,
"type": "number | boolean",
},
{
"name": "enabled",
"optional": true,
Expand Down
72 changes: 72 additions & 0 deletions src/core/__tests__/chart-core-tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,76 @@ describe("CoreChart: tooltip", () => {
});
});
});

describe("debounce functionality", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good tests 👍

beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

test("debounces tooltip rendering with debounce", () => {
const { wrapper } = renderChart({
highcharts,
options: { series: pieSeries },
tooltip: { debounce: 100 },
getTooltipContent: () => ({ header: () => "Header", body: () => "Body" }),
});

act(() => hc.highlightChartPoint(0, 0));

// Tooltip should not be visible immediately
expect(wrapper.findTooltip()).toBe(null);

// Fast forward time by 50ms - still not visible
act(() => vi.advanceTimersByTime(50));
expect(wrapper.findTooltip()).toBe(null);

// Fast forward time by another 50ms (100ms total) - now visible
act(() => vi.advanceTimersByTime(50));
expect(wrapper.findTooltip()).not.toBe(null);
});

test("cancels previous debounced call when new highlight occurs", () => {
const { wrapper } = renderChart({
highcharts,
options: { series: pieSeries },
tooltip: { debounce: 100 },
getTooltipContent: ({ point }) => ({ header: () => `Point ${point?.name}`, body: () => "Body" }),
});

// Highlight first point
act(() => hc.highlightChartPoint(0, 0));

// Fast forward 50ms
act(() => vi.advanceTimersByTime(50));
expect(wrapper.findTooltip()).toBe(null);

// Highlight second point before first debounce completes
act(() => hc.highlightChartPoint(0, 1));

// Fast forward another 50ms (100ms from first highlight, 50ms from second)
act(() => vi.advanceTimersByTime(50));
expect(wrapper.findTooltip()).toBe(null);

// Fast forward another 50ms (100ms from second highlight)
act(() => vi.advanceTimersByTime(50));
expect(wrapper.findTooltip()).not.toBe(null);
expect(wrapper.findTooltip()!.findHeader()!.getElement().textContent).toBe("Point P2");
});

test("renders immediately when debounce is 0", () => {
const { wrapper } = renderChart({
highcharts,
options: { series: pieSeries },
tooltip: { debounce: 0 },
getTooltipContent: () => ({ header: () => "Header", body: () => "Body" }),
});

act(() => hc.highlightChartPoint(0, 0));
expect(wrapper.findTooltip()).not.toBe(null);
});
});
});
20 changes: 13 additions & 7 deletions src/core/components/core-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LiveRegion from "@cloudscape-design/components/live-region";
import ChartSeriesDetails, { ChartSeriesDetailItem } from "../../internal/components/series-details";
import { useSelector } from "../../internal/utils/async-store";
import { getChartSeries } from "../../internal/utils/chart-series";
import { useDebouncedValue } from "../../internal/utils/use-debounced-value";
import { ChartAPI } from "../chart-api";
import { getFormatter } from "../formatters";
import { BaseI18nStrings, CoreChartProps } from "../interfaces";
Expand All @@ -19,6 +20,7 @@ import { getPointColor, getSeriesColor, getSeriesId, getSeriesMarkerType, isXThr
import styles from "../styles.css.js";

const MIN_VISIBLE_BLOCK_SIZE = 200;
const DEFAULT_DEBOUNCE = 200;

type ExpandedSeriesState = Record<string, Set<string>>;

Expand All @@ -44,18 +46,22 @@ export function ChartTooltip({
getTooltipContent: getTooltipContentOverrides,
api,
i18nStrings,
debounce = false,
}: CoreChartProps.TooltipOptions & {
i18nStrings?: BaseI18nStrings;
getTooltipContent?: CoreChartProps.GetTooltipContent;
api: ChartAPI;
}) {
const [expandedSeries, setExpandedSeries] = useState<ExpandedSeriesState>({});
const tooltip = useSelector(api.tooltipStore, (s) => s);
if (!tooltip.visible || tooltip.group.length === 0) {
const debouncedTooltip = useDebouncedValue(tooltip, debounce === true ? DEFAULT_DEBOUNCE : debounce || 0);

if (!debouncedTooltip || !debouncedTooltip.visible || debouncedTooltip.group.length === 0) {
return null;
}
const chart = tooltip.group[0]?.series.chart;
const renderers = getTooltipContentOverrides?.({ point: tooltip.point, group: tooltip.group });

const chart = debouncedTooltip.group[0]?.series.chart;
const renderers = getTooltipContentOverrides?.({ point: debouncedTooltip.point, group: debouncedTooltip.group });
const getTrack = placement === "target" ? api.getTargetTrack : api.getGroupTrack;
const position = (() => {
if (placement === "target" || placement === "middle") {
Expand All @@ -66,8 +72,8 @@ export function ChartTooltip({
})();
const content = getTooltipContent(api, {
renderers,
point: tooltip.point,
group: tooltip.group,
point: debouncedTooltip.point,
group: debouncedTooltip.group,
expandedSeries,
setExpandedSeries,
hideTooltip: () => {
Expand All @@ -80,9 +86,9 @@ export function ChartTooltip({
return (
<InternalChartTooltip
getTrack={getTrack}
trackKey={getTrackKey(tooltip.point, tooltip.group)}
trackKey={getTrackKey(debouncedTooltip.point, debouncedTooltip.group)}
container={null}
dismissButton={tooltip.pinned}
dismissButton={debouncedTooltip.pinned}
dismissAriaLabel={i18nStrings?.detailPopoverDismissAriaLabel}
onDismiss={api.onDismissTooltip}
onMouseEnter={api.onMouseEnterTooltip}
Expand Down
1 change: 1 addition & 0 deletions src/core/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ export namespace CoreChartProps {
enabled?: boolean;
placement?: "middle" | "outside" | "target";
size?: "small" | "medium" | "large";
debounce?: number | boolean;
}

export type GetTooltipContent = (props: GetTooltipContentProps) => TooltipContentRenderer;
Expand Down
33 changes: 33 additions & 0 deletions src/internal/utils/use-debounced-value.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useRef, useState } from "react";

import { DebouncedCall } from "./utils";

export function useDebouncedValue<T>(value: T, duration: number) {
const [debouncedValue, setDebouncedValue] = useState<T | null>(null);
const [shouldRender, setShouldRender] = useState(false);
const debouncedCall = useRef(new DebouncedCall());

useEffect(() => {
if (duration <= 0) {
return;
}

setShouldRender(false);
const current = debouncedCall.current;
current.call(() => {
setDebouncedValue(value);
setShouldRender(true);
}, duration);

return () => current.cancelPrevious();
}, [value, duration]);

if (duration <= 0) {
return value;
}

return shouldRender ? debouncedValue : null;
}
Loading