Skip to content

fix: active dot appearing outside graph#6612

Merged
ckifer merged 2 commits intorecharts:mainfrom
cameronm-orion:fix/active-dot-outside-graph
Nov 21, 2025
Merged

fix: active dot appearing outside graph#6612
ckifer merged 2 commits intorecharts:mainfrom
cameronm-orion:fix/active-dot-outside-graph

Conversation

@cameronm-orion
Copy link
Contributor

@cameronm-orion cameronm-orion commented Nov 12, 2025

Description

This fixes the issue of active dot and tooltip appearing outside the graph when the domain is set so that no dots are on the graph. I've added some checks to ensure that the domain is respected when rendering the active dot and tooltip.

Related Issue

#5086

Motivation and Context

This problem will occur mainly when implementing zoom and pan controls for graphs. In my application, I'm changing the domain as the user pans side to side on the graph, which led to the discovery of this edge case.

How Has This Been Tested?

I've added a unit test inside the ActiveDot test suite and a story in the LineChart story to verify the changes.

Screenshots (if appropriate):

Image of bug

image

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • I have added a storybook story or VR test, or extended an existing story or VR test to show my changes

Summary by CodeRabbit

  • New Features

    • Active dots and tooltips now respect axis domains and will remain inactive when data lies outside configured domains.
  • Improvements

    • Active-point rendering now supports clipping so dots align visually with chart clip regions.
  • Documentation

    • Added a story demonstrating behavior when data is excluded from the visible domain.
  • Tests

    • Added tests validating tooltip/active-dot behavior and index resolution when data falls outside domains.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

Adds clipPath propagation for active-point rendering in Line and Area components and makes tooltip active-index resolution domain-aware by extending combineActiveTooltipIndex and selectors to consider axis dataKey and domain, preventing active dots/tooltips when underlying values fall outside the axis domain.

Changes

Cohort / File(s) Change Summary
Clipping active points
src/cartesian/Area.tsx, src/cartesian/Line.tsx, src/component/ActivePoints.tsx
Compute an activePointsClipPath when clipping is required and propagate it via a new optional clipPath?: string prop through ActivePointsActivePointLayer, enabling active-dot rendering to be clipped to the chart region.
Domain-aware tooltip index filtering
src/state/selectors/combiners/combineActiveTooltipIndex.ts, src/state/selectors/tooltipSelectors.ts, src/state/selectors/selectors.ts
Extend combineActiveTooltipIndex signature to accept optional axisDataKey and domain; add internal helpers (toFiniteNumber, isValueWithinDomain, etc.); return `TooltipIndex
Keyboard event handling
src/state/keyboardEventsMiddleware.ts
Import and pass selectTooltipAxisDataKey and selectTooltipAxisDomain into index resolution; handle nullable resolved index and guard against non-finite or negative indices before keyboard navigation.
Stories and tests
storybook/stories/Examples/LineChart/LineChart.stories.tsx, test/component/Tooltip/ActiveDot.spec.tsx, test/state/selectors/combiners/combineActiveTooltipIndex.spec.ts
Add ActiveDotExcludedFromDomain story and tests verifying tooltips/active dots are not shown when data points fall outside configured axis domains; add unit tests for domain-aware behavior of combineActiveTooltipIndex.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / Keyboard
    participant Middleware as keyboardEventsMiddleware
    participant Selector as selectActiveIndex
    participant Combiner as combineActiveTooltipIndex
    participant Domain as Axis Domain Check
    participant Renderer as ActivePoints / Render

    User->>Middleware: navigation or hover event
    Middleware->>Selector: request active index
    Selector->>Combiner: combineActiveTooltipIndex(interaction, data, axisDataKey, domain)
    Combiner->>Domain: validate datum value against domain
    alt Value within domain
        Domain-->>Combiner: valid
        Combiner-->>Selector: return index (string)
    else Value outside domain
        Domain-->>Combiner: invalid
        Combiner-->>Selector: return null
    end
    Selector-->>Middleware: activeIndex (string|null)
    Middleware->>Renderer: render with clipPath if provided and activeIndex non-null
    alt activeIndex non-null
        Renderer->>Renderer: show clipped active dot & tooltip
    else activeIndex null
        Renderer->>Renderer: hide active dot & tooltip
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Areas to focus:
    • combineActiveTooltipIndex: verify coercion, domain checks, and null return semantics.
    • Selector wiring: confirm selector dependency lists and memoization implications.
    • ActivePoints/Layer clipPath propagation: ensure clipPath string formation and conditional logic around needClip / clipDot are correct.
    • keyboardEventsMiddleware: check guards for resolved index and navigation edge cases.

Possibly related PRs

Suggested reviewers

  • ckifer
  • PavelVanecek

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately describes the primary issue being fixed: preventing the active dot from appearing outside the graph when the domain excludes data points.
Description check ✅ Passed The description covers all required template sections: clearly describes the fix, links to related issue #5086, provides motivation and context, explains testing approach, and correctly marks the bug fix type and completed checklist items.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b086656 and b9fdefa.

📒 Files selected for processing (9)
  • src/cartesian/Area.tsx (2 hunks)
  • src/cartesian/Line.tsx (2 hunks)
  • src/component/ActivePoints.tsx (5 hunks)
  • src/state/keyboardEventsMiddleware.ts (2 hunks)
  • src/state/selectors/combiners/combineActiveTooltipIndex.ts (2 hunks)
  • src/state/selectors/selectors.ts (3 hunks)
  • src/state/selectors/tooltipSelectors.ts (1 hunks)
  • storybook/stories/Examples/LineChart/LineChart.stories.tsx (1 hunks)
  • test/component/Tooltip/ActiveDot.spec.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not hardcode any user-facing strings or formatting choices in the library code; leave internationalization to library users

Files:

  • src/cartesian/Area.tsx
  • src/state/selectors/combiners/combineActiveTooltipIndex.ts
  • src/cartesian/Line.tsx
  • src/state/selectors/selectors.ts
  • src/state/keyboardEventsMiddleware.ts
  • src/state/selectors/tooltipSelectors.ts
  • src/component/ActivePoints.tsx
🧠 Learnings (6)
📚 Learning: 2025-10-25T07:35:46.188Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-10-25T07:35:46.188Z
Learning: Applies to test/**/*.test.{ts,tsx} : Mock getBoundingClientRect using test/helper/MockGetBoundingClientRect.ts when rendering Recharts components (e.g., Tooltip, Legend, charts)

Applied to files:

  • storybook/stories/Examples/LineChart/LineChart.stories.tsx
  • src/state/selectors/selectors.ts
  • test/component/Tooltip/ActiveDot.spec.tsx
📚 Learning: 2025-10-25T07:35:46.188Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-10-25T07:35:46.188Z
Learning: Applies to test/**/*.test.{ts,tsx} : When testing selectors, verify render counts using the spy and rerenderSameComponent from createSelectorTestCase

Applied to files:

  • src/state/selectors/selectors.ts
  • test/component/Tooltip/ActiveDot.spec.tsx
📚 Learning: 2025-10-25T07:35:46.188Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-10-25T07:35:46.188Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer using the createSelectorTestCase helper when writing or modifying tests

Applied to files:

  • test/component/Tooltip/ActiveDot.spec.tsx
📚 Learning: 2025-10-25T07:35:46.188Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-10-25T07:35:46.188Z
Learning: Write unit tests using Vitest and React Testing Library

Applied to files:

  • test/component/Tooltip/ActiveDot.spec.tsx
📚 Learning: 2025-10-25T07:36:02.229Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-25T07:36:02.229Z
Learning: Recharts aims for simple, declarative, and composable charts; prioritize consistency, usability, performance, and accessibility

Applied to files:

  • test/component/Tooltip/ActiveDot.spec.tsx
📚 Learning: 2025-10-25T07:35:46.188Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-10-25T07:35:46.188Z
Learning: Applies to test/**/*.test.{ts,tsx} : In tests not using createSelectorTestCase, advance timers after renders with vi.runOnlyPendingTimers()

Applied to files:

  • test/component/Tooltip/ActiveDot.spec.tsx
🧬 Code graph analysis (8)
src/cartesian/Area.tsx (1)
src/component/ActivePoints.tsx (1)
  • ActivePoints (94-126)
storybook/stories/Examples/LineChart/LineChart.stories.tsx (1)
storybook/storybook-addon-recharts/RechartsHookInspector.tsx (1)
  • RechartsHookInspector (68-136)
src/state/selectors/combiners/combineActiveTooltipIndex.ts (4)
src/util/types.ts (3)
  • NumberDomain (686-686)
  • DataKey (59-59)
  • CategoricalDomain (688-688)
src/util/ChartUtils.ts (1)
  • getValueByDataKey (41-55)
src/state/tooltipSlice.ts (1)
  • TooltipInteractionState (118-154)
src/state/chartDataSlice.ts (1)
  • ChartData (12-12)
src/state/selectors/selectors.ts (3)
src/state/selectors/tooltipSelectors.ts (2)
  • selectTooltipDisplayedData (137-140)
  • selectTooltipAxisDomain (254-266)
src/state/selectors/axisSelectors.ts (1)
  • selectTooltipAxisDataKey (592-595)
src/state/tooltipSlice.ts (2)
  • TooltipIndex (38-38)
  • TooltipInteractionState (118-154)
src/state/keyboardEventsMiddleware.ts (3)
src/state/selectors/combiners/combineActiveTooltipIndex.ts (1)
  • combineActiveTooltipIndex (79-120)
src/state/selectors/tooltipSelectors.ts (2)
  • selectTooltipDisplayedData (137-140)
  • selectTooltipAxisDomain (254-266)
src/state/selectors/axisSelectors.ts (1)
  • selectTooltipAxisDataKey (592-595)
src/state/selectors/tooltipSelectors.ts (2)
src/state/selectors/selectors.ts (1)
  • selectTooltipInteractionState (76-84)
src/state/selectors/axisSelectors.ts (1)
  • selectTooltipAxisDataKey (592-595)
test/component/Tooltip/ActiveDot.spec.tsx (4)
src/cartesian/Line.tsx (2)
  • render (567-619)
  • Line (780-780)
src/component/Tooltip.tsx (1)
  • Tooltip (155-291)
test/component/Tooltip/tooltipTestHelpers.tsx (2)
  • showTooltip (113-115)
  • expectTooltipNotVisible (127-131)
test/component/Tooltip/tooltipMouseHoverSelectors.ts (1)
  • lineChartMouseHoverTooltipSelector (26-26)
src/component/ActivePoints.tsx (1)
src/container/Layer.tsx (1)
  • Layer (13-22)
🔇 Additional comments (16)
storybook/stories/Examples/LineChart/LineChart.stories.tsx (1)

1141-1188: LGTM! Well-structured test story for domain-exclusion behavior.

The story effectively demonstrates the fix: when both data points lie outside the visible domain [1.01, 1.15], the tooltip and active dot are properly suppressed regardless of allowDataOverflow.

src/state/selectors/tooltipSelectors.ts (1)

395-398: LGTM! Selector dependencies properly extended for domain-aware filtering.

The addition of selectTooltipAxisDataKey and selectTooltipAxisDomain as dependencies enables domain-based validation of tooltip indices, which is essential for preventing tooltips from displaying outside the visible domain.

src/cartesian/Area.tsx (2)

631-631: LGTM! ClipPath calculation correctly mirrors dot clipping logic.

The conditional clipPath URL construction properly handles both clipDot cases, aligning active point clipping with the chart's clip region.


653-668: LGTM! ClipPath propagated to both main and baseline ActivePoints.

The implementation correctly passes the computed activePointsClipPath to both the main points (line 658) and the baseline points for range areas (line 666), ensuring consistent clipping behavior across all active points.

src/cartesian/Line.tsx (2)

580-580: LGTM! ClipPath calculation consistent with Area component.

The conditional clipPath URL construction properly differentiates between clipped and non-clipped dot scenarios, maintaining consistency with the existing clipping infrastructure.


610-616: LGTM! ClipPath correctly propagated to ActivePoints.

The activePointsClipPath is properly passed to the ActivePoints component, enabling domain-aware clipping of active dots in Line charts.

test/component/Tooltip/ActiveDot.spec.tsx (1)

279-306: LGTM! Comprehensive test validates domain-exclusion behavior.

The test properly verifies that when all data points fall outside the visible X-axis domain, the active dot and cursor are suppressed while the tooltip wrapper remains in the DOM (but not visible). Testing both allowDataOverflow values ensures the fix works consistently.

src/component/ActivePoints.tsx (2)

21-79: LGTM! ClipPath prop cleanly threaded through ActivePoint.

The optional clipPath prop is properly added to the component signature and forwarded to the Layer wrapper, enabling clipping when provided while maintaining backward compatibility.


94-125: LGTM! ClipPath prop properly propagated in ActivePoints.

The clipPath prop is correctly accepted in ActivePointsProps, received in the component function, and forwarded to the ActivePoint child component, completing the prop threading chain.

src/state/keyboardEventsMiddleware.ts (1)

36-45: LGTM! Domain-aware keyboard navigation properly implemented.

The middleware now respects axis domain constraints when resolving tooltip indices. The null-handling logic correctly defaults to -1 when the resolved index is null (indicating data is outside the domain), and the early exit prevents invalid keyboard navigation operations.

src/state/selectors/combiners/combineActiveTooltipIndex.ts (4)

7-17: LGTM! Robust type coercion for domain validation.

The toFiniteNumber helper properly handles numbers, Dates, and parseable values, returning undefined for non-finite results, which enables safe numeric domain comparisons.


19-31: LGTM! Correct numeric domain boundary checking.

The function properly normalizes domain bounds (handling reversed domains with Math.min/Math.max) and performs inclusive boundary checks, returning false when any value is non-finite.


79-119: LGTM! Domain-aware index resolution with proper null handling.

The function correctly:

  • Clamps the desired index to valid bounds
  • Returns the clamped index if no entry exists at that position
  • Validates the entry against the domain and returns null when outside bounds
  • Returns the stringified index when validation passes

This ensures tooltips and active dots only display when data points fall within the visible domain.


33-77: Based on my verification of the code through the search results, I can now assess the review comment.

The logic is sound and intentional.

Looking at the function isValueWithinDomain in the provided snippet:

  1. Lines 47-49 (Array length !== 2): Returning true for arrays with length !== 2 is correct. This allows categorical domains (which can be any length > 2) to fall through to the .some() check below, where they're compared for exact matches or numeric equality. This is intentional behavior.

  2. Lines 54-63 (Numeric domain bounds): The logic checks if both bounds are numeric and within range via isValueWithinNumberDomain. If either bound is null/undefined, it returns true (no filtering). This is a permissive approach for partial domains, which appears intentional for handling incomplete or auto-calculated domains.

  3. Lines 65-76 (Categorical fallback): The .some() check correctly handles both exact value matches and numeric equality when the numeric conversion is possible.

The function handles edge cases appropriately:

  • Undefined/null inputs early return
  • Non-numeric domain values gracefully fall back to equality checks
  • Partial numeric domains (with null bounds) skip filtering
  • Mixed categorical/numeric comparisons are supported
src/state/selectors/selectors.ts (2)

28-28: LGTM! Import addition is correct.

The new import selectTooltipAxisDomain is properly sourced and used in the selectActiveIndex selector below.


92-94: Domain-aware filtering correctly implemented in combineActiveTooltipIndex.

The function properly validates that the clamped active index's corresponding entry lies within the specified domain before returning it. When outside domain bounds, it returns null to prevent tooltip display. The implementation handles both numeric ranges and categorical domains with comprehensive null-safety checks.

Comment on lines +192 to 196
) => { isActive: boolean; activeIndex: TooltipIndex | null } = createSelector(
[selectTooltipInteractionState, selectActiveIndex],
(tooltipInteractionState: TooltipInteractionState, activeIndex: TooltipIndex | null) => {
return { isActive: tooltipInteractionState.active && activeIndex != null, activeIndex };
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Breaking change: Return type updated to include activeIndex.

The return type has changed from returning just the tooltip active state to returning an object containing both isActive and activeIndex. This change ensures the tooltip is only considered active when there's a valid index within the domain, which correctly implements the PR objective.

The logic is sound: isActive now requires both tooltipInteractionState.active and a non-null activeIndex, preventing tooltips from displaying when data points fall outside the visible domain.

Verify that all consumers of selectIsTooltipActive have been updated to handle the new return type:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of selectIsTooltipActive to verify consumers handle the new return type

# Search for direct usages of selectIsTooltipActive
rg -n -C 3 'selectIsTooltipActive' --type ts --type tsx

# Search for useAppSelector calls that might use this selector
rg -n -C 3 'useAppSelector.*selectIsTooltipActive' --type ts --type tsx

# Search for destructuring patterns that might consume the old return type
rg -n -C 3 'const.*=.*selectIsTooltipActive' --type ts --type tsx

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of selectIsTooltipActive using glob patterns for proper file matching

# Search for direct usages of selectIsTooltipActive in .ts and .tsx files
rg -n -C 3 'selectIsTooltipActive' -g '*.ts' -g '*.tsx'

# Alternative: Search for imports of selectIsTooltipActive
echo "=== Imports of selectIsTooltipActive ==="
rg -n 'import.*selectIsTooltipActive' -g '*.ts' -g '*.tsx'

# Search for useAppSelector usage with selectIsTooltipActive
echo "=== useAppSelector with selectIsTooltipActive ==="
rg -n 'useAppSelector.*selectIsTooltipActive' -g '*.ts' -g '*.tsx'

# Search for references to isActive pattern (might indicate old usage pattern)
echo "=== References to old boolean pattern ==="
rg -n 'const\s+isActive\s*=.*selectIsTooltipActive' -g '*.ts' -g '*.tsx'

Length of output: 37994


🏁 Script executed:

#!/bin/bash
# Check the middleware more closely to understand its usage of selectIsTooltipActive

echo "=== Full context of middleware usage ==="
cat src/state/externalEventsMiddleware.ts | head -50

echo -e "\n=== Check what old selector is supposed to return ==="
cat src/state/selectors/tooltipSelectors.ts | sed -n '440,450p'

echo -e "\n=== Check if old selector still exported and used elsewhere ==="
rg -n 'selectIsTooltipActive' src/state/selectors/tooltipSelectors.ts

echo -e "\n=== Check imports in middleware to confirm which version is imported ==="
rg -n -B5 -A5 'selectIsTooltipActive' src/state/externalEventsMiddleware.ts | head -30

Length of output: 2982


🏁 Script executed:

#!/bin/bash
# Check if there are any other files importing from tooltipSelectors that might use old selector
echo "=== All files importing from tooltipSelectors ==="
rg -n 'from.*selectors/tooltipSelectors' -g '*.ts' -g '*.tsx'

echo -e "\n=== Verify the type definition for MouseHandlerDataParam ==="
rg -n -A5 'type MouseHandlerDataParam' src/state/synchronisation/types.ts

Length of output: 2529


🏁 Script executed:

#!/bin/bash
# Search for MouseHandlerDataParam type definition
echo "=== Search for MouseHandlerDataParam ==="
rg -n 'MouseHandlerDataParam' --type ts --type tsx -g '*.ts' -g '*.tsx'

echo -e "\n=== Check keyboard middleware to see if it also uses old selectIsTooltipActive ==="
rg -n -B2 -A10 'selectIsTooltipActive' src/state/keyboardEventsMiddleware.ts

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Search for MouseHandlerDataParam type definition using glob patterns
echo "=== Search for MouseHandlerDataParam type definition ==="
rg -n 'MouseHandlerDataParam' -g '*.ts' -g '*.tsx'

echo -e "\n=== Check keyboard middleware imports ==="
cat src/state/keyboardEventsMiddleware.ts | head -20

echo -e "\n=== Find where MouseHandlerDataParam is defined ==="
rg -n 'type MouseHandlerDataParam\|interface MouseHandlerDataParam' -g '*.ts' -g '*.tsx'

Length of output: 3198


Update review comment: Naming collision creates confusion between internal and external selector versions.

There are two selectors named selectIsTooltipActive in different files serving different purposes:

  1. Old selector (tooltipSelectors.ts:446): Returns boolean, used by external middleware
  2. New selector (selectors.ts:187): Returns { isActive: boolean; activeIndex: TooltipIndex | null }, used by Tooltip component

The new selector correctly implements the PR objective of tracking both active state and index. The Tooltip component properly handles the new object return type. However, the MouseHandlerDataParam.isTooltipActive field expects a boolean, which is correctly provided by the old selector still used in externalEventsMiddleware.ts.

Recommendation: Rename the new selector to avoid confusion. Consider selectIsTooltipActiveWithIndex or similar to clarify it returns both properties and distinguish it from the external API contract maintained by the old selector.

🤖 Prompt for AI Agents
In src/state/selectors/selectors.ts around lines 192 to 196, the selector
currently named selectIsTooltipActive returns an object { isActive: boolean;
activeIndex: TooltipIndex | null } which collides with the older boolean-only
selectIsTooltipActive used by external middleware; rename this selector (e.g.
selectIsTooltipActiveWithIndex) and export it under the new name, update all
local imports/usages (Tooltip component and any internal consumers) to use the
new name, and leave the old boolean selector and its external usages untouched
so the MouseHandlerDataParam.isTooltipActive contract remains valid.

@ckifer ckifer requested a review from PavelVanecek November 12, 2025 22:48
@codecov
Copy link

codecov bot commented Nov 12, 2025

Codecov Report

❌ Patch coverage is 89.02439% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.13%. Comparing base (b086656) to head (9f21cb1).
⚠️ Report is 34 commits behind head on main.

Files with missing lines Patch % Lines
...e/selectors/combiners/combineActiveTooltipIndex.ts 84.61% 8 Missing ⚠️
src/state/keyboardEventsMiddleware.ts 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6612      +/-   ##
==========================================
- Coverage   94.16%   94.13%   -0.03%     
==========================================
  Files         493      493              
  Lines       41081    41173      +92     
  Branches     4778     4806      +28     
==========================================
+ Hits        38685    38760      +75     
- Misses       2391     2408      +17     
  Partials        5        5              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@PavelVanecek
Copy link
Collaborator

Codecov Report

❌ Patch coverage is 75.96154% with 25 lines in your changes missing coverage.

@cameronm-orion can you please add test coverage for the combiner file? It should be fairly simple to get to 100% there. The keyboard middleware would be nice to have tested too but it might be a bit challenging to set up.

Copy link
Collaborator

@PavelVanecek PavelVanecek left a comment

Choose a reason for hiding this comment

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

I agree in principle, I am not sure I fully understand the selector/combiner changes.


const value = getValueByDataKey(entry, axisDataKey);
if (value == null) {
return true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

How does this interact with connectNulls prop? https://recharts.github.io/en-US/examples/AreaChartConnectNulls/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made a small test in Storybook to see if this change caused issues with connectNulls and everything appears to work.

const ConnectNullsTest = ({ args }: { args: Args }) => {
  const [domain, setDomain] = useState(args.domain);
  return (
    <>
      <ResponsiveContainer width="100%" height={200}>
        <LineChart {...args}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis
            dataKey="name"
            allowDataOverflow={args.allowDataOverflow}
            type="number"
            domain={domain}
          />
          <YAxis />
          <Line type="monotone" dataKey="uv" stroke="#8884d8" fill="#8884d8" />
          <Tooltip />
          <RechartsHookInspector />
        </LineChart>
      </ResponsiveContainer>

      <ResponsiveContainer width="100%" height={200}>
        <LineChart {...args}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis
            dataKey="name"
            allowDataOverflow={args.allowDataOverflow}
            type="number"
            domain={domain}
          />
          <YAxis />
          <Line
            connectNulls
            type="monotone"
            dataKey="uv"
            stroke="#8884d8"
            fill="#8884d8"
          />
          <Tooltip />
        </LineChart>
      </ResponsiveContainer>

      <div>
        <button
          type="button"
          onClick={() => setDomain((prev) => [prev[0] - 0.01, prev[1] - 0.01])}
        >
          Left
        </button>
        <button
          type="button"
          onClick={() => setDomain((prev) => [prev[0] + 0.01, prev[1] + 0.01])}
        >
          Right
        </button>
      </div>
    </>
  );
};

export const ConnectNullsNumbers = {
  render: (args: Args) => {
    return (
      <>
        <ConnectNullsTest args={args} />
      </>
    );
  },
  args: {
    width: 500,
    height: 200,
    domain: [1.01, 7.5],
    allowDataOverflow: true,
    margin: {
      top: 10,
      right: 30,
      left: 0,
      bottom: 0,
    },
    data: [
      { name: "1", uv: 300, pv: 456 },
      { name: "2", pv: 230 },
      { name: "3", pv: 345 },
      { name: "4", pv: 450 },
      { name: "5", pv: 321 },
      { name: "6", pv: 235 },
      { name: "7", pv: 267 },
      { name: "8", uv: 252, pv: -378 },
      { name: "9", uv: 79, pv: -210 },
      { name: "10", uv: 294, pv: -23 },
      { name: "12", uv: 43, pv: 45 },
      { name: "13", uv: -74, pv: 90 },
      { name: "14", uv: -71, pv: 130 },
      { name: "15", uv: -117, pv: 11 },
      { name: "16", uv: -186, pv: 107 },
      { name: "17", uv: -16, pv: 926 },
      { name: "18", uv: -125, pv: 653 },
      { name: "19", uv: 222, pv: 366 },
      { name: "20", uv: 372, pv: 486 },
      { name: "21", uv: 182, pv: 512 },
      { name: "22", uv: 164, pv: 302 },
      { name: "23", uv: 316, pv: 425 },
      { name: "24", uv: 131, pv: 467 },
      { name: "25", uv: 291, pv: -190 },
      { name: "26", uv: -47, pv: 194 },
      { name: "27", uv: -415, pv: 371 },
      { name: "28", uv: -182, pv: 376 },
      { name: "29", uv: -93, pv: 295 },
      { name: "30", uv: -99, pv: 322 },
      { name: "31", uv: -52, pv: 246 },
      { name: "32", uv: 154, pv: 33 },
      { name: "33", uv: 205, pv: 354 },
      { name: "34", uv: 70, pv: 258 },
      { name: "35", uv: -25, pv: 359 },
      { name: "36", uv: -59, pv: 192 },
      { name: "37", uv: -63, pv: 464 },
      { name: "38", uv: -91, pv: -2 },
      { name: "39", uv: -66, pv: 154 },
      { name: "40", uv: -50, pv: 186 },
    ],
  },
};

}

if (Array.isArray(domain) && domain.length !== 2) {
return true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

So in a categorical domain, this active dot outside of a chart is still going to be a problem? Can we have that fixed too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't believe this bug is reproducible with categorical charts as the domain prop only works on a numerical axis, if my understanding is correct. And the domain prop is important for recreating this bug.

- utilised utility function
- removed unnecessary code
- added unit tests
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9fdefa and 9f21cb1.

📒 Files selected for processing (2)
  • src/state/selectors/combiners/combineActiveTooltipIndex.ts (2 hunks)
  • test/state/selectors/combiners/combineActiveTooltipIndex.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/state/selectors/combiners/combineActiveTooltipIndex.ts (5)
src/util/types.ts (3)
  • NumberDomain (686-686)
  • DataKey (59-59)
  • CategoricalDomain (688-688)
src/util/ChartUtils.ts (1)
  • getValueByDataKey (41-55)
src/util/isDomainSpecifiedByUser.ts (1)
  • isWellFormedNumberDomain (6-14)
src/state/tooltipSlice.ts (1)
  • TooltipInteractionState (118-154)
src/state/chartDataSlice.ts (1)
  • ChartData (12-12)
test/state/selectors/combiners/combineActiveTooltipIndex.spec.ts (4)
src/state/tooltipSlice.ts (1)
  • TooltipInteractionState (118-154)
src/state/chartDataSlice.ts (1)
  • ChartData (12-12)
src/util/types.ts (2)
  • NumberDomain (686-686)
  • CategoricalDomain (688-688)
src/state/selectors/combiners/combineActiveTooltipIndex.ts (1)
  • combineActiveTooltipIndex (55-96)
🔇 Additional comments (5)
src/state/selectors/combiners/combineActiveTooltipIndex.ts (4)

4-6: LGTM! Imports are appropriate.

The new imports support the domain-aware filtering logic and are all utilized in the implementation.


8-18: LGTM! Robust type coercion.

The helper correctly handles numbers, Dates, and other types with proper finite checks. The fallback to Number(value) is safe because Number.isFinite will catch invalid conversions.


20-53: Verify null value handling is intentional.

The domain validation logic is well-structured and correctly handles reversed domains (lines 29-31). However, lines 44-46 return true when the extracted value is null, treating null values as "within domain" by default.

This is likely intentional to support sparse data or missing values, but please confirm this behavior aligns with the expected user experience when dealing with incomplete datasets.


84-96: Clarify the behavior when entry is null.

The domain filtering logic correctly returns null when data falls outside the domain (lines 91-93), which achieves the PR objective. However, lines 87-89 return the clamped index as a string even when entry is null or undefined.

This could occur with sparse arrays or undefined entries. Is this intentional to handle edge cases in certain chart types, or could this lead to rendering issues when attempting to display a tooltip for non-existent data?

Consider whether returning null for undefined entries would be more consistent:

  if (entry == null) {
-   return String(clampedIndex);
+   return null;
  }
test/state/selectors/combiners/combineActiveTooltipIndex.spec.ts (1)

1-12: LGTM! Clean test setup.

The helper function createInteraction improves test readability and reduces duplication.

Comment on lines +14 to +52
describe('combineActiveTooltipIndex', () => {
it('returns null when the hovered datum falls outside the provided numeric domain', () => {
const chartData: ChartData = [{ value: 2 }, { value: 10 }];
const tooltipInteraction = createInteraction('1');
const domain: NumberDomain = [0, 5] as const;

const result = combineActiveTooltipIndex(tooltipInteraction, chartData, 'value', domain);

expect(result).toBeNull();
});

it('clamps the index to visible entries and still honors the numeric domain guard', () => {
const chartData: ChartData = [{ value: 2 }, { value: 7 }, { value: 15 }];
const tooltipInteraction = createInteraction('30');
const domain: NumberDomain = [0, 20] as const;

const result = combineActiveTooltipIndex(tooltipInteraction, chartData, 'value', domain);

expect(result).toBe('2');
});

it('keeps categorical axes interactive because categorical domains are ignored by the guard', () => {
const chartData: ChartData = [{ category: 'alpha' }];
const tooltipInteraction = createInteraction('0');
const categoricalDomain: CategoricalDomain = ['alpha', 'beta'];

const result = combineActiveTooltipIndex(tooltipInteraction, chartData, 'category', categoricalDomain);

expect(result).toBe('0');
});

it('returns the original index when it cannot be coerced into a finite number', () => {
const chartData: ChartData = [];
const tooltipInteraction = createInteraction('node-3');

const result = combineActiveTooltipIndex(tooltipInteraction, chartData, undefined, undefined);

expect(result).toBe('node-3');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Expand test coverage to address reviewer feedback.

The existing tests cover the main scenarios well. However, based on the PR comment noting 75.96% patch coverage with 25 lines missing, consider adding tests for:

  1. Date value handling - The toFiniteNumber helper handles Date objects:

    it('handles Date values in domain checks', () => {
      const chartData: ChartData = [{ timestamp: new Date('2024-01-01') }];
      const interaction = createInteraction('0');
      const domain: NumberDomain = [new Date('2024-01-01').valueOf(), new Date('2024-12-31').valueOf()] as const;
      
      expect(combineActiveTooltipIndex(interaction, chartData, 'timestamp', domain)).toBe('0');
    });
  2. String-to-number coercion - Line 16-17 in the implementation attempts Number(value):

    it('coerces string numbers when checking domain', () => {
      const chartData: ChartData = [{ value: '5' }];
      const interaction = createInteraction('0');
      const domain: NumberDomain = [0, 10] as const;
      
      expect(combineActiveTooltipIndex(interaction, chartData, 'value', domain)).toBe('0');
    });
  3. Reversed domain - Lines 29-31 normalize min/max:

    it('handles reversed domain bounds', () => {
      const chartData: ChartData = [{ value: 5 }];
      const interaction = createInteraction('0');
      const domain: NumberDomain = [10, 0] as const; // reversed
      
      expect(combineActiveTooltipIndex(interaction, chartData, 'value', domain)).toBe('0');
    });
  4. Null/undefined values - Lines 44-46 return true for null values:

    it('allows entries with null/undefined values', () => {
      const chartData: ChartData = [{ value: null }];
      const interaction = createInteraction('0');
      const domain: NumberDomain = [0, 10] as const;
      
      expect(combineActiveTooltipIndex(interaction, chartData, 'value', domain)).toBe('0');
    });

Based on learnings: PavelVanecek specifically requested better test coverage for this file.

🤖 Prompt for AI Agents
In test/state/selectors/combiners/combineActiveTooltipIndex.spec.ts around lines
14 to 52, add unit tests to cover missing branches: add a test that uses a Date
value in chartData and a numeric domain made from Date.valueOf() to ensure Date
handling; add a test where the datum value is a numeric string (e.g. '5') to
verify string-to-number coercion; add a test with a reversed numeric domain
(e.g. [10, 0]) to ensure min/max normalization works; and add a test where the
datum value is null/undefined to ensure such entries are allowed — each test
should call combineActiveTooltipIndex with appropriate chartData, interaction,
key and domain and assert the expected index string.

Copy link
Member

@ckifer ckifer left a comment

Choose a reason for hiding this comment

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

@PavelVanecek this ok to you?

@ckifer ckifer merged commit 3a7cf75 into recharts:main Nov 21, 2025
20 of 22 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 4, 2026
7 tasks
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