Skip to content

feat: Add internal hover debounce to core chart - #283

Open
ryugaraj wants to merge 1 commit into
cloudscape-design:mainfrom
ryugaraj:feat/hover-debounce
Open

ryugaraj wants to merge 1 commit into
cloudscape-design:mainfrom
ryugaraj:feat/hover-debounce

Conversation

@ryugaraj

Copy link
Copy Markdown
Contributor

Description

In dense charts, series highlight, legend highlight, legend scroll-into-view and the tooltip all react to every pointer move, which causes a lot of motion. The existing tooltip.debounce only delays the tooltip, so the other effects drift apart from it.

This adds an internal hoverDebounce prop to core chart (a number in ms, or true for 300ms) that applies all of these effects together once the pointer settles on a target.

  • One scheduler. A new HoverScheduler (src/core/chart-api/hover-scheduler.ts) owns the delay, the pending target and the timer. The pointer uses it for points and groups, and the chart API uses a second instance for legend item hover.
  • Restart on target change only. Small moves within the same point or group don't push the delay back.
  • Immediate cursor. The cursor line and point markers are drawn straight away as a preview, without moving the tooltip's anchor tracks. Series highlight, legend highlight and scroll, the tooltip and onHighlight wait for the delay.
  • Aligned tooltips. The legend item tooltip uses the same delay, and tooltip.debounce is ignored while hoverDebounce is set, so the delays don't stack.
  • Click pins immediately. A click applies the pending hover first, so pinning still works before the delay ends.
  • Cancellation. A pending hover is dropped on:
    • hover lost and moving into the tooltip
    • keyboard focus and blur, Escape, and tooltip dismiss
    • legend item toggle or select
    • public API highlight calls
    • chart destroy
  • Re-check. A pending point or group is checked for visibility again when the delay ends.

Without hoverDebounce, behavior is unchanged. Like the zoom props, the prop lives in internal-interfaces.ts and isn't part of the public API, so the documenter snapshot doesn't change.

Related links, issue #, if available: n/a

How has this been tested?

  • Unit tests: 22 in src/core/__tests__/chart-core-hover-debounce.test.tsx. They cover delay and restart, the immediate cursor, legend hover and its tooltip, click to pin, the cancellation cases, the visibility re-check, and the default with no delay. Full unit suite: 467 passing.
  • Browser tests: 4 in test/functional/hover-debounce.test.ts, using real WebDriver mouse input against the dev page with a 1000ms delay. They check that the cursor shows at once while the tooltip and highlights wait, that the tooltip stays on the previous point until the pointer rests, the legend-hover delay, and the no-delay baseline. Run with npm run test:functional. Not run yet; the same scenarios were checked by hand in a browser.
  • Dev page: pages/03-core/hover-debounce.page.tsx (#/03-core/hover-debounce) has a line chart with 40 series and a height-limited legend. Set the delay in page settings, or with the hoverDebounce URL parameter; 0 shows the current behavior.
  • Consumer check: also tried in a consumer's Storybook and local dev server via a patched build.
Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Screen.Recording.2026-11-11.at.15.52.17.mov

@ryugaraj
ryugaraj requested a review from a team as a code owner November 11, 2026 15:54
@ryugaraj
ryugaraj requested review from taheramr and removed request for a team November 11, 2026 15:54
@pan-kot
pan-kot requested review from pan-kot and a balanced review from Copilot November 11, 2026 16:20
@pan-kot

pan-kot commented Nov 12, 2026

Copy link
Copy Markdown
Member

I ran my review agent on the PR, here are its findings that I deem relevant:

  1. Two debounce mechanisms exist at the same time - we need a strategy to sunset the old one.

  2. Cancellation is sprinkled over 15 call sites. cancelPendingHovers()/legendItemHover.cancel() appear in onDismissTooltip, hideTooltip, four public API methods, five navigation handlers, two legend callbacks, destroy, and clearHover. Every one of those paths ends in highlightActions, clearHighlightActions or highlightChartItems. Put the cancel at the top of those three funnels and most call sites go away. The scheduler's own flush() clears pending before it applies, so the funnel's cancel is a no-op for the hover it is applying. Keep an explicit cancel only where a path skips the funnels (onDismissHoverTooltip when nothing is visible, destroy). Fewer sites means fewer paths to miss later.

  3. Redundant tests. "hides the datapoint cursor when hover is lost" (97) and "drops a pending highlight when hover is lost" (172) are the same scenario with different assertions; merge. "highlights immediately when hoverDebounce is not set" (221) is what every other test in the repo already exercises. "restarts the delay when the target changes" (125) and "keeps the current highlight until the next target settles" (155) overlap. 393 lines should be about 300.

  4. hoverDelay leaks into the internal legend only to set tooltipShowDelay and force scrollDelay to 0. Passing the two derived values (or just the tooltip delay) keeps the internal component ignorant of the core's debounce.

  5. onItemHighlightEnter(item, detail?: { pointer }). The internal legend always passes detail; make it required and drop the ?..

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Incomplete hover cancellation and timer cleanup allow delayed tooltips and highlights to appear after Escape, legend interactions, or delay changes, while canceled or hidden targets can leave stale cursor previews.

6 open issues
What changed in this PR

Adds internal hover debouncing to the core chart to coordinate highlights, legend scrolling, and tooltips in dense charts while keeping cursor previews immediate.

Changes:

  • Introduces a shared scheduler implementation for delayed point, group, and legend hover effects.
  • Adds cancellation, click-to-pin handling, and visibility checks for pending targets.
  • Adds unit tests, browser tests, and a configurable dense-chart demo.
File Description
test/​functional/​hover-debounce.test.ts Adds browser coverage for delayed and immediate hover behavior.
src/​internal/​components/​chart-legend/​index.tsx Configures tooltip and scroll timing and distinguishes pointer hover.
src/​core/​internal-interfaces.ts Defines the internal hoverDebounce prop.
src/​core/​components/​core-legend.tsx Routes legend pointer highlights through the scheduler.
src/​core/​chart-core.tsx Wires hover delay into chart settings, legends, and tooltips.
src/​core/​chart-api/​index.tsx Coordinates legend scheduling, cursor previews, and cancellation.
src/​core/​chart-api/​hover-scheduler.ts Adds reusable target-aware hover scheduling.
src/​core/​chart-api/​chart-extra-tooltip.tsx Separates cursor previews from tooltip anchor updates.
src/​core/​chart-api/​chart-extra-pointer.tsx Schedules pointer targets and flushes pending hover on click.
src/​core/​chart-api/​chart-extra-context.tsx Adds the hover-delay setting and default.
src/​core/​__tests__/​common.tsx Extends test helpers to accept internal hover props.
src/​core/​__tests__/​chart-core-hover-debounce.test.tsx Covers timing, previews, cancellation, and visibility checks.
pages/​03-core/​hover-debounce.page.tsx Adds a dense-chart demo with adjustable delay.

Review effort: Balanced

Comment thread src/core/chart-api/index.tsx Resolved
Comment thread src/core/chart-api/index.tsx Resolved
Comment thread src/core/chart-api/index.tsx Resolved
@ryugaraj
ryugaraj force-pushed the feat/hover-debounce branch from d12e9a7 to 9c26861 Compare November 12, 2026 14:04
@ryugaraj

ryugaraj commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All points are addressed in 9c26861:

  1. Two debounce mechanisms. hoverDebounce supersedes tooltip.debounce. While hoverDebounce is set, tooltip.debounce is ignored, so the two never stack. Code comments on InternalHoverProps, TooltipOptions.debounce and in chart-core.tsx record this and the plan to remove tooltip.debounce. CloudWatch is planning to drop tooltip.debounce and use hoverDebounce: 300 for all charts and sizes after this change is merged. Once that's adopted, we'd propose:

    • making hoverDebounce public, or the default;
    • deprecating tooltip.debounce;
    • removing it after consumers (CloudWatch) move over.

    We're happy to align the true default (300ms here vs 200ms for tooltip.debounce) to whichever you prefer.

  2. Cancellation. It now lives in the three funnels: highlightActions, clearHighlightActions and highlightChartItems. Explicit cancels remain only where a path skips them: Escape with nothing visible (navigation and document handlers), visibility changes, and destroy.

  3. Tests. I merged the two hover-lost tests and the restart/keep pair, and dropped the no-delay test. The file is still about 440 lines because it now also covers the six Copilot findings (23 tests in total).

  4. Legend. The internal legend now takes plain tooltipShowDelay and scrollIntoViewDelay props and knows nothing about the core debounce.

  5. detail. It's now required.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

}
};

private applyHover = (target: PointerHoverTarget) => {
Comment thread src/core/chart-api/index.tsx Resolved
Comment thread src/core/chart-api/index.tsx Outdated
if (this.chartExtraPointer.cancelPendingHover()) {
this.chartExtraTooltip.restoreCursor();
}
this.legendItemHover.cancel();
Comment thread src/core/chart-api/index.tsx Resolved
In dense charts, series highlight, legend highlight, legend
scroll-into-view and the tooltip all react to every pointer move,
which causes a lot of motion. The existing tooltip.debounce only
delays the tooltip, so the other effects drift apart from it.

Add an internal hoverDebounce prop (number in ms, or true for
300ms) that applies all of these effects together once the pointer
settles on a target:

- A HoverScheduler owns the delay, the pending target and the timer.
  The pointer uses it for points and groups; the chart API uses a
  second instance for legend item hover.
- The timer restarts only when the hovered target changes.
- The cursor line and point markers are drawn immediately as a
  preview; series, legend, scroll, tooltip and onHighlight wait.
- The legend item tooltip uses the same delay, and tooltip.debounce
  is ignored while hoverDebounce is set so the delays don't stack.
  tooltip.debounce is planned for removal once consumers have moved
  to hoverDebounce.
- A click applies the pending hover first, so pinning still works.
- Pending hovers are cancelled at the three highlight funnels
  (highlightActions, clearHighlightActions, highlightChartItems), so
  keyboard, API, legend and dismiss paths all take precedence. Only
  Escape with nothing visible, visibility changes and destroy cancel
  explicitly. A cancelled or rejected preview restores the cursor.
- A pending point or group is applied only while the pointer is still
  on it, and is re-checked for visibility when the delay ends.
- Leaving a legend item cancels its pending highlight immediately,
  Escape also cancels a pending legend tooltip, and losing the plot
  hover does not cancel a newer legend hover.
- The internal legend receives plain tooltip and scroll delays and
  keeps one tooltip timer, so changing the delay can't leave a stale
  timer behind.

Without hoverDebounce the behavior is unchanged. The prop is internal
(like the zoom props) and not part of the public API.

Adds a dev page (03-core/hover-debounce) with a dense line chart,
unit tests, and browser tests that run against that page.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved hover scheduling and state issues can show stale tooltips, highlight items after legend teardown, cancel newer cross-legend hovers, and reset cursor previews on redraw.

6 open issues
2 resolved since last review

Review effort: Balanced

Comment on lines +99 to +102
public showCursor(point: null | Highcharts.Point, group: readonly Highcharts.Point[]) {
if (!this.tooltipLock && this.chartType === "cartesian") {
this.createCursor(point, group);
}
};
public onLeaveLegendItem = () => this.legendItemHover.cancel();
public onClearChartItemsHighlight = () => {
this.legendItemHover.cancel();
Comment on lines +508 to +510
if (!keepLegendHover) {
this.legendItemHover.cancel();
}
},
};
}, []);
useEffect(() => cancelTooltip, [cancelTooltip]);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants