Skip to content

Give bars, heatmaps and distribution groups real datetime axes - #318

Merged
Alek99 merged 3 commits into
alek/raster-text-fidelityfrom
alek/temporal-position-axes
Jul 26, 2026
Merged

Give bars, heatmaps and distribution groups real datetime axes#318
Alek99 merged 3 commits into
alek/raster-text-fidelityfrom
alek/temporal-position-axes

Conversation

@Alek99

@Alek99 Alek99 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Stacked on #316 (base: alek/raster-text-fidelity).

line() canonicalizes datetimes to milliseconds through the column store. The
position axes — bar, column, heatmap, box/violin grouping —
resolve outside that store, and had no datetime branch at all: they reached
astype(float64), which yields the raw tick count in whatever unit the array
happened to carry.

before and after

Three different wrong answers for the same dates

input before
datetime64[s…ns], pandas Series/DatetimeIndex positions in nanoseconds, so a default width of "0.8" asked for a bar 0.8 ms wide — rounds to zero pixels, blank chart, axis labelled 1.7e18
datetime64[D] positions in day-counts; bars visible, axis linear and labelled 19723.0
list[datetime.date] object dtype, so it took the category path — three dates two months apart drawn as three evenly spaced bars

And layering a line over datetime bars did not merely render wrong, it
crashed: the line's ingested column flipped the axis to time while the bars
sat at raw nanoseconds, so the calendar tick code got a domain in the year
54,001,545 and to_svg(), to_png() and to_pdf() all raised
ValueError: year must be in 1..9999.

Changes

  • _axis_positions* canonicalize datetimes to ms using the same helpers the
    store uses. is_datetime_like moves to columns.py so there is one
    probe rather than the two that existed and the two that were missing —
    pyplot/_axes.py was importing one of them across module boundaries.
  • Figure._axis_temporal records which axes hold datetimes, the sibling of
    _axis_categories. Rect marks ingest derived centers, so their columns are
    always kind="float" and _axis_kind cannot see the datetimes the caller
    passed. Registered at commit time — so a mark that raises afterwards leaves
    nothing behind, matching how category labels already work — and captured in
    the checkpoint.
  • width= on bar/column defaults to 0.8 of the closest gap between
    adjacent positions. On a category axis, whose positions are one unit apart,
    that is the same 0.8 as before, byte for byte (pinned by a test). The closest
    gap rather than the mean or median: overlapping bars hide each other's data,
    so irregular spacing should give narrow bars, not wide ones.

Behavior change

bar(list_of_dates, y) was a categorical axis (equal spacing,
spec["x_axis"]["categories"] populated) and is now temporal (proportional
spacing, no categories). That matches line() on identical input and is what
docs/core-concepts/axes-and-scales.md has always promised — "datetime-like
values select the time scale automatically" — but irregular date series will
now show their real gaps
. Stringify (d.isoformat()) to keep dates
categorical; documented in docs/charts/bar-chart.md.

Mixing categories and datetimes on one axis now raises with an explanation
instead of letting one silently win.

Scope

This was reported as a bar/column bug. Verifying it first turned up the wider
scope — box/violin grouping, heatmap, both bar orientations, and the
pyplot shim's ax.bar/ax.barh all share _axis_positions* — and the
layered-chart crash, which the original report did not mention. Fixing at
ingest rather than in the renderers covers all four renderers at once.

Verification

  • 2481 tests pass (19 new), covering all five datetime input forms, both
    orientations, heatmap and box axes, the layered crash, the unchanged
    category/numeric/boolean paths, the byte-identical category width, the
    mixed-axis error, and rollback leaving no temporal registration behind.
  • ruff, ruff format, pre-commit clean; ty at the 13-diagnostic baseline;
    abi_smoke and append_stream_smoke pass.

`line()` canonicalizes datetimes to milliseconds through the column store.
The *position* axes — bar, column, heatmap, box/violin grouping — resolve
outside that store, and had no datetime branch at all: they reached
`astype(float64)`, which yields the raw tick count in whatever unit the array
happened to carry.

That produced three different wrong answers for the same dates:

- datetime64[s..ns] and pandas: positions in nanoseconds, so the default bar
  width of "0.8" asked for a bar 0.8 ms wide. It rounds to zero pixels — a
  blank chart with an axis labelled 1.7e18.
- datetime64[D]: positions in day-counts, bars visible but the axis linear and
  labelled 19723.0.
- A list of `datetime.date` is object dtype, so it took the *category* path:
  three dates two months apart drawn as three evenly spaced bars.

Layering a line over datetime bars was worse than wrong, it crashed: the
line's ingested column flipped the axis to time while the bars sat at raw
nanoseconds, so the calendar tick code got a domain in the year 54,001,545 and
`to_svg()`, `to_png()` and `to_pdf()` all raised `ValueError: year must be in
1..9999`.

- `_axis_positions*` now canonicalize datetimes to ms, using the same helpers
  the store uses. `is_datetime_like` moves to `columns.py` so there is one
  probe rather than the two that existed and the two that were missing.
- `Figure._axis_temporal` records which axes hold datetimes — the sibling of
  `_axis_categories` — because rect marks ingest derived centers, so their
  columns are always kind="float" and `_axis_kind` cannot see the datetimes the
  caller passed. Registered at commit time, so a mark that raises afterwards
  leaves nothing behind, and captured in the checkpoint.
- `width=` on bar/column defaults to 0.8 of the *closest* gap between adjacent
  positions. On a category axis, whose positions are one unit apart, that is
  the same 0.8 as before, byte for byte. The closest gap rather than the mean:
  overlapping bars hide each other's data.

Behavior change, called out in the changelog: `bar(list_of_dates, y)` was a
categorical axis and is now temporal, so irregular date series show their real
gaps. Stringify to keep them categorical. Mixing categories and datetimes on
one axis now raises instead of letting one silently win.

docs/core-concepts/axes-and-scales.md already promised "datetime-like values
select the time scale automatically". Now the position axes do it too.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcb28630-3a68-4729-a438-a691b357a7e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alek/temporal-position-axes

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

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing alek/temporal-position-axes (d14a83a) with alek/raster-text-fidelity (e339201)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on alek/raster-text-fidelity (911c0cb) during the generation of this report, so 5131fd8 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Alek99
Alek99 merged commit 4020fe3 into alek/raster-text-fidelity Jul 26, 2026
23 checks passed
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