fix: active dot appearing outside graph#6612
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.tsxsrc/state/selectors/combiners/combineActiveTooltipIndex.tssrc/cartesian/Line.tsxsrc/state/selectors/selectors.tssrc/state/keyboardEventsMiddleware.tssrc/state/selectors/tooltipSelectors.tssrc/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.tsxsrc/state/selectors/selectors.tstest/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.tstest/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
selectTooltipAxisDataKeyandselectTooltipAxisDomainas 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
clipDotcases, 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
activePointsClipPathto 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
activePointsClipPathis properly passed to theActivePointscomponent, 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
allowDataOverflowvalues ensures the fix works consistently.src/component/ActivePoints.tsx (2)
21-79: LGTM! ClipPath prop cleanly threaded through ActivePoint.The optional
clipPathprop is properly added to the component signature and forwarded to theLayerwrapper, enabling clipping when provided while maintaining backward compatibility.
94-125: LGTM! ClipPath prop properly propagated in ActivePoints.The
clipPathprop is correctly accepted inActivePointsProps, received in the component function, and forwarded to theActivePointchild 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
-1when 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
toFiniteNumberhelper properly handles numbers, Dates, and parseable values, returningundefinedfor 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, returningfalsewhen 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
nullwhen 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
isValueWithinDomainin the provided snippet:
Lines 47-49 (Array length !== 2): Returning
truefor 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.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 returnstrue(no filtering). This is a permissive approach for partial domains, which appears intentional for handling incomplete or auto-calculated domains.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
selectTooltipAxisDomainis properly sourced and used in theselectActiveIndexselector below.
92-94: Domain-aware filtering correctly implemented incombineActiveTooltipIndex.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
nullto prevent tooltip display. The implementation handles both numeric ranges and categorical domains with comprehensive null-safety checks.
| ) => { isActive: boolean; activeIndex: TooltipIndex | null } = createSelector( | ||
| [selectTooltipInteractionState, selectActiveIndex], | ||
| (tooltipInteractionState: TooltipInteractionState, activeIndex: TooltipIndex | null) => { | ||
| return { isActive: tooltipInteractionState.active && activeIndex != null, activeIndex }; | ||
| }, |
There was a problem hiding this comment.
🧩 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 tsxLength 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 -30Length 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.tsLength 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.tsLength 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:
- Old selector (
tooltipSelectors.ts:446): Returnsboolean, used by external middleware - 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.
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
@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. |
PavelVanecek
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
How does this interact with connectNulls prop? https://recharts.github.io/en-US/examples/AreaChartConnectNulls/
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 becauseNumber.isFinitewill 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
truewhen 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
nullwhen 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 whenentryis 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
nullfor 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
createInteractionimproves test readability and reduces duplication.
| 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'); | ||
| }); |
There was a problem hiding this comment.
🛠️ 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:
-
Date value handling - The
toFiniteNumberhelper 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'); });
-
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'); });
-
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'); });
-
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.
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
Types of changes
Checklist:
Summary by CodeRabbit
New Features
Improvements
Documentation
Tests