Give bars, heatmaps and distribution groups real datetime axes - #318
Conversation
`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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
…axes # Conflicts: # python/xy/components.py
Stacked on #316 (base:
alek/raster-text-fidelity).line()canonicalizes datetimes to milliseconds through the column store. Theposition axes —
bar,column,heatmap,box/violingrouping —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 arrayhappened to carry.
Three different wrong answers for the same dates
datetime64[s…ns], pandasSeries/DatetimeIndex1.7e18datetime64[D]19723.0list[datetime.date]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()andto_pdf()all raisedValueError: year must be in 1..9999.Changes
_axis_positions*canonicalize datetimes to ms using the same helpers thestore uses.
is_datetime_likemoves tocolumns.pyso there is oneprobe rather than the two that existed and the two that were missing —
pyplot/_axes.pywas importing one of them across module boundaries.Figure._axis_temporalrecords which axes hold datetimes, the sibling of_axis_categories. Rect marks ingest derived centers, so their columns arealways
kind="float"and_axis_kindcannot see the datetimes the callerpassed. 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=onbar/columndefaults to 0.8 of the closest gap betweenadjacent 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 (proportionalspacing, no
categories). That matchesline()on identical input and is whatdocs/core-concepts/axes-and-scales.mdhas always promised — "datetime-likevalues select the time scale automatically" — but irregular date series will
now show their real gaps. Stringify (
d.isoformat()) to keep datescategorical; 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/violingrouping,heatmap, both bar orientations, and thepyplot shim's
ax.bar/ax.barhall share_axis_positions*— and thelayered-chart crash, which the original report did not mention. Fixing at
ingest rather than in the renderers covers all four renderers at once.
Verification
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.
tyat the 13-diagnostic baseline;abi_smokeandappend_stream_smokepass.