Skip to content

Rendered ticks in hooks#6990

Merged
PavelVanecek merged 11 commits intomainfrom
useticks
Feb 15, 2026
Merged

Rendered ticks in hooks#6990
PavelVanecek merged 11 commits intomainfrom
useticks

Conversation

@PavelVanecek
Copy link
Collaborator

@PavelVanecek PavelVanecek commented Feb 11, 2026

Description

The problem is that CartesianAxis uses DOM to calculate the actual font size and overlap, and then uses that to decide which ticks get rendered. That's legit but doesn't fit in the reducer nor selector. So I added a new slice where we effectively cache the calculation and return it from the tick hooks.

I have also removed the incorrect domain as the robot told me in the other PR.

Related Issue

Fixes #6978

Screenshots (if appropriate):

Screen.Recording.2026-02-11.at.20.14.03.mov

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
    • Added fontSize and letterSpacing props to XAxis and YAxis for finer tick text control.
    • Axes can now be identified with an axisId and publish rendered tick metadata to shared state for improved multi-axis coordination.
  • Documentation
    • Examples updated to use automatic Y axis width in relevant demos.
  • Tests
    • Test expectations adjusted to reflect the new rendered-ticks behavior and additional render passes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Walkthrough

Adds a rendered-ticks Redux slice, threads axisId through Cartesian axis components, publishes rendered tick data from render to store via a reporter component, updates selectors/hooks to consume rendered ticks, and adjusts tests and snapshots for the new interactions.

Changes

Cohort / File(s) Summary
Snapshots
scripts/snapshots/es6Files.txt, scripts/snapshots/libFiles.txt, scripts/snapshots/typesFiles.txt
Added entries for state/renderedTicksSlice in ES6, lib, and types snapshots.
New slice & store
src/state/renderedTicksSlice.ts, src/state/store.ts
Introduces renderedTicks slice with setRenderedTicks/removeRenderedTicks and wires reducer into root store and RechartsRootState.
Selectors
src/state/selectors/axisSelectors.ts
Adds selectRenderedTicksOfAxis; changes selectAxisInverseTickSnapScale to consume rendered ticks and removes isPanorama param; updates tick item types.
Cartesian axis & tick plumbing
src/cartesian/CartesianAxis.tsx, src/cartesian/XAxis.tsx, src/cartesian/YAxis.tsx
Threads axisId through components, adds RenderedTicksReporter that dispatches rendered ticks to store on mount/update and cleans up on unmount; XAxis/YAxis add fontSize/letterSpacing props and forward axisId.
Hooks
src/hooks.ts
Switches tick consumers to selectRenderedTicksOfAxis; updates inverse tick-snap calls and tick item types.
Tests
test/... (many files, e.g., test/cartesian/*, test/state/selectors/*, test/hooks/*, etc.)
Updated numerous spy call-count expectations to account for extra selector/store interactions; added test asserting rendered ticks are published; minor test refactors (e.g., spy.mock.lastCall).
Docs / Examples
www/src/docs/.../DataSnapExample.tsx, www/src/docs/.../AxisTicksSnapExample.tsx
Replaced YAxis domain [0,7000] with width="auto" in two examples.

Sequence Diagram

sequenceDiagram
    participant CartesianAxis as CartesianAxis
    participant Ticks as Ticks Component
    participant Reporter as RenderedTicksReporter
    participant Redux as Redux Store
    participant Selector as Selector / Hook

    CartesianAxis->>Ticks: render(axisId, tick data)
    Ticks->>Reporter: mount/update(ticks, axisType, axisId)
    Reporter->>Reporter: useEffect (map ticks -> TickItem[])
    Reporter->>Redux: dispatch setRenderedTicks(axisType, axisId, TickItem[])
    Redux->>Redux: store at state.renderedTicks[axisType][axisId]
    Selector->>Redux: selectRenderedTicksOfAxis(state, axisType, axisId)
    Redux-->>Selector: TickItem[] | undefined
    Reporter->>Reporter: cleanup on unmount
    Reporter->>Redux: dispatch removeRenderedTicks(axisType, axisId)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

refactor

Suggested reviewers

  • ckifer
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Rendered ticks in hooks' accurately summarizes the main change: exposing rendered ticks to hooks through a new Redux slice that caches DOM-based tick calculations.
Description check ✅ Passed The PR description covers the key problem statement, motivation, and solution approach. Related issue #6978 is linked, types of changes are specified, and testing is noted. All major template sections are addressed.
Linked Issues check ✅ Passed The PR implements the core requirements from #6978: ensuring hooks return the same ticks as rendered by moving DOM-based filtering logic into a new Redux slice (renderedTicksSlice) that both hooks and CartesianAxis use.
Out of Scope Changes check ✅ Passed All changes are in scope. Core changes add rendered ticks caching and expose via hooks. Test updates reflect additional Redux selector invocations. Documentation examples updated to remove incorrect domain constraints—all directly support the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch useticks

No actionable comments were generated in the recent review. 🎉


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

🤖 Fix all issues with AI agents
In `@test/cartesian/XAxis/XAxis.state.spec.tsx`:
- Around line 343-373: The rendered-ticks test relies on DOM measurements but
JSDOM returns zero sizes; import and use the test helper
mockGetBoundingClientRect (from test/helper/MockGetBoundingClientRect.ts) inside
the XAxis.state.spec.tsx case that uses
createSelectorTestCase/selectRenderedTicksOfAxis so the tick elements have
stable bounding rects before rendering (call mockGetBoundingClientRect() prior
to render and ensure it is restored/cleaned up after the test to avoid side
effects), leaving the rest of the test (expectedTicks and
expectLastCalledWith(spy, ...)) unchanged.
🧹 Nitpick comments (5)
test/cartesian/ReferenceLine/ReferenceLine.spec.tsx (1)

32-36: Missing vi.useFakeTimers() in the test suite.

The coding guidelines require vi.useFakeTimers() in all tests due to Redux autoBatchEnhancer dependency on timers. This beforeEach block doesn't set up fake timers. While this is a pre-existing gap, it's worth addressing since you're already touching this file.

Proposed fix
   beforeEach(() => {
+    vi.useFakeTimers();
     consoleSpy = vi.spyOn(console, 'warn').mockImplementation((): void => undefined);
   });

As per coding guidelines: "Use vi.useFakeTimers() in all tests due to Redux autoBatchEnhancer dependency on timers and requestAnimationFrame"

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

131-132: Hardcoded call index may be stale after the count bump.

Line 132 accesses spy.mock.calls[2][0] — previously this was the last call (count=3), but now with count=4 it's the second-to-last. The test still passes because the scale is identical, but consider using spy.mock.lastCall[0] or spy.mock.calls[spy.mock.calls.length - 1][0] to be resilient to future count changes.

Suggested improvement
       expect(spy).toHaveBeenCalledTimes(4);
-      const scale = spy.mock.calls[2][0];
+      const scale = spy.mock.lastCall[0];
test/state/selectors/selectAxisScale.spec.tsx (1)

40-40: Commented-out assertion reduces render-count verification coverage.

The call-count assertion is commented out instead of being updated to the new expected value. Per coding guidelines, selector call counts should be verified to spot unnecessary re-renders. If the count is now known (e.g., 3 as in the sibling test), update the assertion; otherwise, add a TODO explaining why it's deferred.

Proposed fix
-    // expect(spy).toHaveBeenCalledTimes(3);
+    expect(spy).toHaveBeenCalledTimes(3);

Based on learnings: "Verify the number of selector calls using the spy object from createSelectorTestCase to spot unnecessary re-renders and improve performance"

src/cartesian/CartesianAxis.tsx (2)

339-362: axisId added to TicksProps — verify JSDoc/type documentation.

The axisId field is added to TicksProps without a JSDoc comment. While this is an internal type (not public API), the coding guidelines ask to keep TypeScript definitions up-to-date for documentation purposes. Consider adding a brief comment.

As per coding guidelines: "JSDoc comments and TypeScript definitions in source files must be kept up-to-date as they are used to autogenerate API documentation via 'npm run omnidoc'."


45-48: axisId prop added to CartesianAxisProps without JSDoc.

Other props in this interface have comments. A brief JSDoc would be consistent and helpful for the autogenerated API docs.

As per coding guidelines: "JSDoc comments and TypeScript definitions in source files must be kept up-to-date as they are used to autogenerate API documentation via 'npm run omnidoc'."

Suggested addition
   axisType?: 'xAxis' | 'yAxis';
+  /** Unique identifier for this axis instance, used to track rendered ticks in state. */
   axisId?: AxisId;

Comment on lines +343 to +373
it('should publish rendered ticks to the store', () => {
const renderTestCase = createSelectorTestCase(({ children }) => (
<BarChart width={100} height={100} data={[{ x: 'x-1' }, { x: 'x-2' }, { x: 'x-3' }]}>
<XAxis xAxisId="foo" dataKey="x" />
{children}
</BarChart>
));

const { spy } = renderTestCase(state => selectRenderedTicksOfAxis(state, 'xAxis', 'foo'));
const expectedTicks: ReadonlyArray<TickItem> = [
{
coordinate: 20,
index: 0,
offset: 15,
value: 'x-1',
},
{
coordinate: 50,
index: 1,
offset: 15,
value: 'x-2',
},
{
coordinate: 80,
index: 2,
offset: 15,
value: 'x-3',
},
];
expectLastCalledWith(spy, expectedTicks);
});
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 | 🟡 Minor

Add mockGetBoundingClientRect for rendered-ticks test stability.
This test exercises DOM-based tick measurement; without a mock, JSDOM returns zero sizes and can make rendered-tick selection brittle.

Proposed fix
@@
-import { createSelectorTestCase, rechartsTestRender } from '../../helper/createSelectorTestCase';
+import { createSelectorTestCase, rechartsTestRender } from '../../helper/createSelectorTestCase';
+import { mockGetBoundingClientRect } from '../../helper/mockGetBoundingClientRect';
@@
   it('should publish rendered ticks to the store', () => {
+    mockGetBoundingClientRect({ width: 10, height: 10 });
     const renderTestCase = createSelectorTestCase(({ children }) => (
       <BarChart width={100} height={100} data={[{ x: 'x-1' }, { x: 'x-2' }, { x: 'x-3' }]}>
         <XAxis xAxisId="foo" dataKey="x" />
         {children}
       </BarChart>
     ));

As per coding guidelines: Mock getBoundingClientRect in tests using the helper function provided in test/helper/MockGetBoundingClientRect.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should publish rendered ticks to the store', () => {
const renderTestCase = createSelectorTestCase(({ children }) => (
<BarChart width={100} height={100} data={[{ x: 'x-1' }, { x: 'x-2' }, { x: 'x-3' }]}>
<XAxis xAxisId="foo" dataKey="x" />
{children}
</BarChart>
));
const { spy } = renderTestCase(state => selectRenderedTicksOfAxis(state, 'xAxis', 'foo'));
const expectedTicks: ReadonlyArray<TickItem> = [
{
coordinate: 20,
index: 0,
offset: 15,
value: 'x-1',
},
{
coordinate: 50,
index: 1,
offset: 15,
value: 'x-2',
},
{
coordinate: 80,
index: 2,
offset: 15,
value: 'x-3',
},
];
expectLastCalledWith(spy, expectedTicks);
});
it('should publish rendered ticks to the store', () => {
mockGetBoundingClientRect({ width: 10, height: 10 });
const renderTestCase = createSelectorTestCase(({ children }) => (
<BarChart width={100} height={100} data={[{ x: 'x-1' }, { x: 'x-2' }, { x: 'x-3' }]}>
<XAxis xAxisId="foo" dataKey="x" />
{children}
</BarChart>
));
const { spy } = renderTestCase(state => selectRenderedTicksOfAxis(state, 'xAxis', 'foo'));
const expectedTicks: ReadonlyArray<TickItem> = [
{
coordinate: 20,
index: 0,
offset: 15,
value: 'x-1',
},
{
coordinate: 50,
index: 1,
offset: 15,
value: 'x-2',
},
{
coordinate: 80,
index: 2,
offset: 15,
value: 'x-3',
},
];
expectLastCalledWith(spy, expectedTicks);
});
🤖 Prompt for AI Agents
In `@test/cartesian/XAxis/XAxis.state.spec.tsx` around lines 343 - 373, The
rendered-ticks test relies on DOM measurements but JSDOM returns zero sizes;
import and use the test helper mockGetBoundingClientRect (from
test/helper/MockGetBoundingClientRect.ts) inside the XAxis.state.spec.tsx case
that uses createSelectorTestCase/selectRenderedTicksOfAxis so the tick elements
have stable bounding rects before rendering (call mockGetBoundingClientRect()
prior to render and ensure it is restored/cleaned up after the test to avoid
side effects), leaving the rest of the test (expectedTicks and
expectLastCalledWith(spy, ...)) unchanged.

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

🤖 Fix all issues with AI agents
In `@src/hooks.ts`:
- Around line 263-265: Change the return type annotations of useXAxisTicks and
useYAxisTicks to match selectRenderedTicksOfAxis by replacing
ReadonlyArray<CartesianTickItem> | undefined with ReadonlyArray<TickItem> |
undefined; update the type import/usage so the hooks return TickItem arrays (not
CartesianTickItem) and ensure any callers are compatible with TickItem
properties.
🧹 Nitpick comments (3)
test/cartesian/XAxis/XAxis.state.spec.tsx (1)

351-372: Missing toHaveBeenCalledTimes assertion on the spy.

The test validates the last-called value but does not verify how many times the selector was invoked. Adding this assertion helps catch unnecessary re-renders.

Proposed fix
     const { spy } = renderTestCase(state => selectRenderedTicksOfAxis(state, 'xAxis', 'foo'));
+    expect(spy).toHaveBeenCalledTimes(3);
     const expectedTicks: ReadonlyArray<TickItem> = [

Based on learnings: "Verify the number of selector calls using the spy object from createSelectorTestCase to spot unnecessary re-renders and improve performance."

src/cartesian/CartesianAxis.tsx (2)

45-49: Missing JSDoc on new axisId prop.

CartesianAxisProps.axisId is a new public-facing prop but lacks a JSDoc comment. As per coding guidelines, JSDoc should be kept up-to-date since it's used for auto-generated API documentation.

Add JSDoc
   axisType?: 'xAxis' | 'yAxis';
+  /**
+   * The unique identifier of the axis this component belongs to.
+   * Used internally to track rendered ticks per axis in the Redux store.
+   */
   axisId?: AxisId;

304-337: Potential unnecessary re-dispatches due to unstable ticks reference in the dependency array.

finalTicks is recomputed via getTicks(...) on every render of Ticks, producing a new array reference even when the tick content hasn't changed. This causes the useEffect cleanup + re-dispatch cycle on every render, which will trigger subscribers of the renderedTicks state to re-render as well.

Consider either:

  1. Memoizing finalTicks in the Ticks component (e.g., via useMemo with stable inputs), or
  2. Doing a shallow comparison of the previous ticks before dispatching inside the effect.
Option 2: Guard dispatch with shallow equality check
 function RenderedTicksReporter({
   ticks,
   axisType,
   axisId,
 }: {
   ticks: ReadonlyArray<TickItemType>;
   axisType: 'xAxis' | 'yAxis' | undefined;
   axisId: AxisId | undefined;
 }) {
   const dispatch = useAppDispatch();
+  const prevTicksRef = useRef<ReadonlyArray<TickItemType> | null>(null);
   useEffect(() => {
     if (axisId == null || axisType == null) {
       return noop;
     }
     // Filter out irrelevant internal properties before exposing externally
     const tickItems = ticks.map(tick => ({
       value: tick.value,
       coordinate: tick.coordinate,
       offset: tick.offset,
       index: tick.index,
     }));
+    // Skip dispatch if ticks haven't meaningfully changed
+    if (
+      prevTicksRef.current != null &&
+      prevTicksRef.current.length === ticks.length &&
+      prevTicksRef.current.every((t, i) => t.value === ticks[i].value && t.coordinate === ticks[i].coordinate)
+    ) {
+      return noop;
+    }
+    prevTicksRef.current = ticks;
     dispatch(setRenderedTicks({ ticks: tickItems, axisId, axisType }));
     return () => {
       dispatch(
         removeRenderedTicks({
           axisId,
           axisType,
         }),
       );
     };
   }, [dispatch, ticks, axisId, axisType]);

   return null;
 }
#!/bin/bash
# Check how finalTicks is produced and whether it could be memoized
rg -n "finalTicks" --type=ts --type=tsx -C3

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 97.59036% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.12%. Comparing base (7b1b097) to head (5beba9b).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
...es/useAxisInverseDataSnapScale/DataSnapExample.tsx 0.00% 1 Missing ⚠️
...eAxisInverseTickSnapScale/AxisTicksSnapExample.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6990      +/-   ##
==========================================
+ Coverage   90.10%   90.12%   +0.02%     
==========================================
  Files         522      523       +1     
  Lines       38848    38914      +66     
  Branches     5347     5357      +10     
==========================================
+ Hits        35004    35073      +69     
+ Misses       3835     3832       -3     
  Partials        9        9              

☔ 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.

@codecov
Copy link

codecov bot commented Feb 11, 2026

Bundle Report

Changes will increase total bundle size by 6.03kB (0.21%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.26MB 2.83kB (0.23%) ⬆️
recharts/bundle-es6 1.09MB 2.42kB (0.22%) ⬆️
recharts/bundle-umd 540.04kB 770 bytes (0.14%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 770 bytes 540.04kB 0.14%
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 262 bytes 55.04kB 0.48%
hooks.js -168 bytes 21.15kB -0.79%
cartesian/CartesianAxis.js 1.06kB 17.26kB 6.53% ⚠️
cartesian/YAxis.js 21 bytes 9.57kB 0.22%
cartesian/XAxis.js 21 bytes 8.17kB 0.26%
state/store.js 100 bytes 4.32kB 2.37%
state/renderedTicksSlice.js (New) 1.13kB 1.13kB 100.0% 🚀
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 354 bytes 64.88kB 0.55%
hooks.js -264 bytes 23.33kB -1.12%
cartesian/CartesianAxis.js 1.1kB 18.59kB 6.28% ⚠️
cartesian/YAxis.js 21 bytes 10.99kB 0.19%
cartesian/XAxis.js 21 bytes 9.54kB 0.22%
state/store.js 118 bytes 4.84kB 2.5%
state/renderedTicksSlice.js (New) 1.49kB 1.49kB 100.0% 🚀

@github-actions
Copy link
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

1 similar comment
@github-actions
Copy link
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@PavelVanecek PavelVanecek merged commit 19c7abc into main Feb 15, 2026
48 checks passed
@PavelVanecek PavelVanecek deleted the useticks branch February 15, 2026 07:24
PavelVanecek pushed a commit that referenced this pull request Feb 28, 2026
Fixes a regression introduced in #6990 where the `textAnchor` prop on
XAxis/YAxis was being ignored.

The calculated `textAnchor` (from orientation/mirror) was overriding the
user-provided value because it was assigned after spreading `axisProps`.

**Change:** Now uses user-provided `textAnchor` if present, otherwise
falls back to the calculated value.

Fixes #7027

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Chart tick labels now respect an explicit horizontal text alignment
when provided; otherwise they fall back to the previous automatic
horizontal alignment. Vertical alignment behavior remains unchanged.

* **Tests**
* Added tests that verify tick label horizontal alignment when a
user-specified alignment is present and when defaults are used, and
confirm the expected tick count.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jarvis <jarvis@pierreeurope.github>
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.8] useAxisTicks returns different ticks than rendered

1 participant