Skip to content

feat(cartesian): treat all-Date x values as a time scale - #683

Open
denniske wants to merge 1 commit into
FormidableLabs:mainfrom
denniske:feat/date-x-axis-time-scale
Open

feat(cartesian): treat all-Date x values as a time scale#683
denniske wants to merge 1 commit into
FormidableLabs:mainfrom
denniske:feat/date-x-axis-time-scale

Conversation

@denniske

@denniske denniske commented Aug 6, 2026

Copy link
Copy Markdown

Description

Disclaimer: Created with Claude Code Opus 5. Tested manually on my app.

Adds time scale support to the Cartesian x axis, as requested in #384.

When every xKey value is a Date, the axis is treated as a time scale:

  • points are positioned by elapsed time rather than by index
  • x ticks land on calendar boundaries instead of round numbers
  • formatXLabel receives a Date
<CartesianChart
  data={[{ date: new Date("2024-01-01"), value: 3 }, /* … */]}
  xKey="date"
  yKeys={["value"]}
  xAxis={{ formatXLabel: (d) => d.toLocaleDateString("en", { month: "short" }) }}
/>

Detection is deliberately strict: mixed arrays, empty arrays, Invalid Date, date-like strings and raw timestamps all keep their existing numerical/categorical behavior, so charts that don't use dates are unaffected.

Implementation notes

Dates are positioned as epoch timestamps on the existing linear scale. A d3 time scale and a linear scale over timestamps produce identical positions — they differ only in tick placement and formatting. So makeScale is untouched, and isDateData implies isNumericalData, letting dates reuse the whole numeric path (domain, bounds, zoom, press handling) unchanged.

Only two places need to know the values are dates:

  1. TicksgetXAxisTicks borrows scaleTime to pick calendar-aware boundaries from the scale's domain, then maps back to timestamps so every downstream consumer stays numeric. Without this, a linear scale over timestamps produces ticks at values like 1756000000000.
  2. Labels — the existing isNumericalData ? tick : ix[tick] pattern becomes isDateData ? new Date(tick) : …, so formatXLabel gets a real Date.

d3-scale already exports scaleTime and is an existing dependency, so no new dependencies are added.

Two smaller changes fall out of this:

  • InputFieldType gains Date. Without it, InputFields<T> filtered Date-typed keys out entirely, so xKey couldn't be typed to a date column.
  • The default formatXLabel uses toLocaleDateString() for Date values, rather than String(date)'s "Mon Jan 01 2024 00:00:00 GMT+0100 (…)".

Scope / limitations

  • Horizontal charts explicitly pass isDateData: false. There xKey drives the category axis, so a time scale doesn't apply the same way; this keeps horizontal layout semantics unchanged.
  • tickValues still accepts number[], not Date[]. Explicit tick values continue to take precedence over generated date ticks. Widening it means touching downsampleTicks / getDomainFromTicks, which currently throw on non-numbers — there's an existing TODO there about string/date support. Happy to add it here or in a follow-up, whichever you prefer.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

yarn check:code passes (lint, typecheck, 274 unit tests + 3 headless).

22 new tests across three files:

  • lib/src/utils/isDateData.test.ts — detection, including Invalid Date, empty and mixed arrays
  • lib/src/cartesian/utils/getXAxisTicks.test.ts — ticks are calendar-aligned, stay inside the domain, remain numeric, and yield to explicit tickValues
  • lib/src/cartesian/utils/transformInputData.test.ts — end to end: detection, timestamp positioning, ix preserving the original Date objects, chronological sorting with y values reordered to match, and that date-like strings/timestamps are not treated as dates

The load-bearing one asserts proportional spacing: for points at Jan 1, Jan 2 and Jan 11 across a 500px window, the middle point lands at 50 (10% of the span) rather than the categorical 250. That's what distinguishes a real time scale from evenly spaced categories.

Also verified on a physical device (iOS): a new Date Axis example screen renders one chart with deliberately irregular gaps (Jan 1–3, then Jan 13 and Jan 20) and one spanning twelve months with no formatXLabel.

Checklist

  • I have included a changeset
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Happy to add website documentation in this PR or a follow-up — let me know which you'd prefer.

When every xKey value is a Date, the x axis becomes a time scale:
points are positioned by elapsed time rather than by index, ticks land
on calendar boundaries, and formatXLabel receives a Date.

Dates are positioned as epoch timestamps on the existing linear scale,
which is equivalent to a d3 time scale for positioning, so scale
construction is unchanged. Only tick generation and label formatting
need to know the values are dates.

Mixed arrays, empty arrays and Invalid Dates fall back to the existing
numerical/categorical handling, so charts that don't use dates are
unaffected.

Closes FormidableLabs#384

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

@denniske is attempting to deploy a commit to the Nearform Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce3815a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
victory-native Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

1 participant