Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9494eb3
Add a polar coordinate system (protocol v11)
Alek99 Jul 28, 2026
9718e8d
Polar area/radar and polar bars/wind rose
Alek99 Jul 28, 2026
25a7086
Polar styling parity and a symmetric plot rect
Alek99 Jul 28, 2026
279fde9
Bind the GLSL polar transform to the fixtures (polar_parity_smoke)
Alek99 Jul 28, 2026
40742ae
Bring polar spec and roadmap current with the shipped scope
Alek99 Jul 28, 2026
e0dd64c
Polar audit round: styling parity, zoom semantics, and structure clea…
Alek99 Jul 28, 2026
a356c22
Polar layout round 2, radial-zoom bounds cull, and pyplot routing
Alek99 Jul 28, 2026
79ec498
Clip polar fills and bars at the radial range instead of culling them
Alek99 Jul 28, 2026
7378c83
Polar sectors for unequal slice widths, and polar-aware point annotat…
Alek99 Jul 28, 2026
3f41ccf
Format the polar test additions
Alek99 Jul 28, 2026
fe10054
Complete pyplot polar projection coverage
Alek99 Jul 28, 2026
2593ddc
Cull out-of-range radii in exporter line/scatter, and two polar label…
Alek99 Jul 28, 2026
3294a38
Antialias polar wedges with an annular-sector SDF
Alek99 Jul 28, 2026
785262f
Close four polar customizability gaps found rebuilding the ECharts pi…
Alek99 Jul 29, 2026
8d887a2
Complete polar heatmap and axis depth
Alek99 Jul 29, 2026
43a3936
Fix radar_chart(fill=False), which crashed unconditionally
Alek99 Jul 29, 2026
511b934
Lead the tooltip with the series name, and speak polar on polar charts
Alek99 Jul 29, 2026
cec7372
Keep the polar clip out of the per-pixel blend hot path
Alek99 Jul 29, 2026
9d0abbe
Fix the eight confirmed review majors: exporters, pyplot rlim, wheel …
Alek99 Jul 29, 2026
34ac2d7
Let hover values reach pi-fraction angle labels
Alek99 Jul 29, 2026
749de59
xy.pie_chart: a pie whose tooltip shows the category and value, not t…
Alek99 Jul 29, 2026
466c6a0
Polar tooltips show values, not angles; free wheel/reset from dragMode
Alek99 Jul 29, 2026
8b18256
Even wedge spacing, wedge share in the tooltip, and three audit P1s
Alek99 Jul 29, 2026
f397721
Document wedge_gap so the docs API tables stay complete
Alek99 Jul 29, 2026
cc555d1
Drain rAF synchronously in the polar wheel-zoom probe
Alek99 Jul 29, 2026
3df81aa
Refuse polar secondary axes and non-linear theta; fix seam tick trimming
Alek99 Jul 29, 2026
3e9c0f2
Fix polar area path joins, wedge hover span, and two inert axis switches
Alek99 Jul 30, 2026
81c6108
Fix the pie zero-slice crash and three silently-wrong polar inputs
Alek99 Jul 30, 2026
00fe68a
Guard polar bar animation, pad negative radii, thin radial labels
Alek99 Jul 30, 2026
a3e28bf
Return matplotlib's theta offset for a southern zero location
Alek99 Jul 30, 2026
7449360
Fix the polar audit round: buffer leak, mobile chrome, wedge cost (#380)
Alek99 Jul 30, 2026
d47312a
Fix the CI failures from #380, plus polar CodSpeed coverage and the p…
Alek99 Jul 30, 2026
b025edd
Document polar charts
Alek99 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ jobs:
echo "using $CHROME"
python3 scripts/render_smoke_nonumpy.py "$CHROME"

- name: Polar GLSL parity smoke (stdlib + Chromium)
run: |
CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
python3 scripts/polar_parity_smoke.py "$CHROME"

- name: Streaming append smoke (stdlib + Chromium)
run: |
CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
Expand Down Expand Up @@ -196,6 +201,11 @@ jobs:
CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
.venv/bin/python scripts/smoke_render.py "$CHROME"

- name: Polar phase 6/7 live examples (Chromium + native exporters)
run: |
CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
.venv/bin/python scripts/polar_phase7_smoke.py "$CHROME"

- name: Browser lifecycle smoke (Chromium)
run: |
CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
Expand Down
123 changes: 123 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,130 @@ in the README).

## [Unreleased]

### Added
- Completed the phase-6/7 polar depth surface: `xy.polar_chart` now admits
heatmap, contour, and error-bar traces alongside line/scatter/area/bar; the
heatmap uses a fragment-stage polar inverse in the browser and the matching
bounded inverse raster for static export. Polar axes add partial-sector
layout, `hole`/radial origin, categorical theta, log/symlog radius, and
polygonal `grid_shape="linear"` rings. `xy.pyplot` exposes degree-based
`set/get_thetamin`, `set/get_thetamax`, and radial
`set/get_rorigin`. Generic segment/mesh marks, polar rule/band annotations,
LOD, facets/animation, and angular navigation/selection remain deferred.
- CodSpeed coverage for the polar coordinate system
(`benchmarks/test_codspeed_polar.py`, a new `polar_coordinate_system` benchmark
category): payload prep for a polar line, a wind rose and a pie, plus SVG,
native-PNG and polar-heatmap export. The polar increment previously moved no
benchmark at all, so the wedge-flattening cost and the polar payload path were
invisible to CI. The collected row count is now gated against
`spec/benchmarks/methodology.md` §8, so a renamed or deleted benchmark cannot
silently leave a stale row in the CodSpeed dashboard.

### Fixed
- Polar customization now stays consistent across the browser, SVG, and native
raster renderers: point-anchored annotations use the joint `(theta, r)`
projection, explicit chart padding survives the polar layout pass, gradient
fills reach native raster wedges, and annular sectors honor rounded corners
and strokes.
- `radar_chart(fill=False)` now renders area children as styled outlines,
translating their line color, width, opacity, curve, and dash props instead
of passing incompatible area props to the line renderer.
- Repeated data updates no longer leak GPU buffers. Trace teardown walked a
hand-kept list of geometry buffer names, so every rebuilt trace — each
state-driven update, each append that could not patch in place, each animated
spec swap — orphaned its style, direct-RGBA colour, stroke, corner-radius,
LOD-blend and dashed-line-length buffers. All three teardown paths (trace,
drill window, sample overlay) now read one shared `TRACE_GPU_BUFFERS` list,
pinned against the build paths by a test so a new channel cannot reintroduce
the leak.
- Chart titles reserve the lines they actually wrap into. The browser wrapped a
long title while layout measured one line, so a compact Wind Rose title lost
about 10 px off the top of the canvas. Titles now wrap at one shared width in
all three renderers, and the browser caps the title element at that same width.
Single-line titles are unchanged.
- A polar figure with a legend reserves a gutter for it and places it there,
instead of overlaying the disc. A default `upper right` legend covered a wind
rose's north-east sectors and its outer radial tick label; a disc inscribed in
its rect has no free corner to overlay. The gutter is 22% of the canvas width,
clamped to 120-200 px — derived from the canvas so all three renderers reserve
the identical box, and wide enough to hold an ordinary row rather than
ellipsize it. Compact widths take a 64 px band beneath the disc instead. An
authored `anchor` or four-tuple `padding` still wins.
- Compact vertical colorbars keep their two extreme tick labels, restacked above
and below the gradient. Collapsing them hid every number, leaving an unlabelled
gradient; only the interior ladder, the rotated title and the text-free minor
ticks drop now, and the box's own `title`/ARIA text still names the scale.
Stacking is what makes it free: a side gutter wide enough for `0.25` would cost
36 px of the plot width the compact collapse exists to protect.
- A time-valued radial axis autoranges from its data instead of from epoch zero,
which had squeezed every modern instant into a hairline ring at the rim. An
explicit `r_axis(margin=)` restores the outer pad it used to discard.
- `theta_axis(format=...)` now wins over the built-in degree/radian tick text in
every renderer, instead of shipping and being overwritten.
- A zero-width bar is legal and draws nothing, like `line_width=0`. A 0% progress
ring, an empty category in aggregated data, and a hand-rolled wedge at zero all
produce one, and each used to fail with "bar width must be positive". Negative
and non-finite widths are still refused.
- Data animations no longer rebuild the whole tick-label DOM on every frame; they
use the same 80 ms cadence view animations use, and force one settled rebuild
when the transition ends.
- A device-pixel-ratio change (browser zoom, or a window moving between displays)
now rescales the per-instance stroke widths and corner radii that are baked in
device pixels, so authored strokes and wedge corners keep their intended size
across a zoom. The DPR handler stays synchronous: a DPR change with no container
resize has no later event to piggyback on. A trace whose CPU style/radius mirror
no longer spans every row on the GPU — which is what a streaming tail append
leaves behind — is skipped rather than repaired in place, so the existing
append-time rebuild still does the renormalizing for it.
- `xy.pie_chart` appears in the generated chart-factory API reference alongside
the other polar compositions.
- A legend row too wide for its box wraps instead of growing a horizontal
scrollbar. The box is capped at `--xy-legend-max-width`, but its grid columns
were `max-content` and refused to shrink, so an over-wide row overflowed and
`overflow:auto` answered sideways — hiding the label it was meant to show.
Columns are now `minmax(0, max-content)` and the inline axis never scrolls.
Vertical scrolling is unchanged, and rows carry their full name in
`title`/ARIA for the ones the height cap clips.
- `xy.pie_chart` no longer prints the same number twice. Values that already sum
to 100 — how most pie data arrives — made `show_values` and `show_percent`
collide, so `[40, 30, 20, 10]` rendered `Direct 40 (40%)`: a legend row that
reads as repeated text, and long enough to overflow the box. The share keeps
the unit and the bare value is dropped, decided once for the whole pie so rows
stay uniform.

### Changed
- Default tooltips now lead with the hovered series name, and the radial row of
a polar readout is labelled `r` rather than presented as a Cartesian `y`. The
numeric angle row is gone from polar readouts: on most polar charts the angle
is where layout put the mark and the cursor is already on it, so it answered a
question nobody asked. Two things survive because they are not numeric angles
— an authored spoke label (a radar category reads `power`) and any row named
explicitly through `labels={"x": ...}`, which opts the angle back in formatted
through the theta axis's own text function. Compositions whose bearing *is*
data say so themselves: a wind rose band still reads its direction, and a pie
slice reads its category and value.
- Polar wedge subdivision is span-proportional: `segments(span) =
clamp(ceil(96 · |span| / turn), 2, 96)` in every renderer, over the *authored*
angular width. Sagitta is quadratic in the per-segment angle, so this holds the
flattening bound while a 16-sector wind-rose bar costs 14 vertices instead of
194 — the fixed full-turn count made ~50k polar bars build ~9.7M vertices a
frame. A full-turn wedge still uses 96.
- `xy.theta_axis`/`xy.r_axis` now refuse the Cartesian axis keywords no polar
renderer implements — `minor_tick_values`, `minor_style`,
`tick_label_min_gap`, `tick_label_anchor`, and the collision spellings of
`tick_label_strategy` (`auto`, `hide`, `rotate`, `stagger`, `preserve`) — each
with a pointer to the control that does work. They previously rode the wire and
were dropped by all three renderers, so the documented axis surface advertised
options that did nothing. `off` and `none` remain honoured. `xy.pyplot`'s
`projection="polar"` drops the same keywords instead of refusing, because every
matplotlib Axes carries an rcParam-derived minor style it never authored; that
drop is recorded in `spec/matplotlib/compat.md`.
- The renderer/spec protocol is now v12. Angular axes resolve
`sector`/`grid_shape` and radial axes resolve `hole` plus optional
`r_origin`; a cached v11 client would silently draw full-circle,
centre-origin Cartesian grid/segment fallbacks, so the protocol mismatch
rejects it before rendering. The native renderer ABI is now v47 for the
annular-sector display-list clip opcode.
- Contributor-only test, lint, type-check, and CodSpeed packages now live in
PEP 735 dependency groups instead of published package extras. The unused
Plotly-only `bench` extra was removed; cross-library benchmark environments
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,13 @@ next, no dates implied:
- **Categorical distributions:** strip, swarm, beeswarm, boxen, rug
- **Regression diagnostics:** trendline, residual, QQ, PP
- **Scatter matrix and joint plots:** SPLOM, pair grid, marginal histograms
- **Pie / donut:** in `xy.pyplot` today, promoting to `xy.pie_chart(xy.pie(...))`
- **Pie / donut:** `xy.pie_chart(labels, values)` ships in the composition API
alongside `xy.pyplot` and unequal-width core polar bars; nested donuts and
variable-radius composition remain
- **Candlestick / OHLC and finance overlays:** SMA, VWAP, Bollinger, RSI, MACD; prototyped, awaiting a fresh landing
- **Waterfall and funnel**
- **Treemap, sunburst, and icicle**
- **Radar / polar and gauge:** needs polar axes first
- **Gauge / indicator:** build on the shipped polar axes and composable radial marks
- **Slope, bump, and dumbbell**
- **3D and volume:** scatter, surfaces, meshes, isosurfaces, and volumetric views

Expand Down
14 changes: 14 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,20 @@ production split transport. Run `bench_animation.py` for real-Chrome
previous+next scene bound; browser clocks and GPU work do not belong in
CodSpeed simulation.

`test_codspeed_polar.py` attributes the polar coordinate system, which shipped
without a CodSpeed row of its own: three payload rows for the shapes with
materially different validation and emit paths (a 100k polar line, a 16-sector /
50k-observation wind rose, a 24-slice pie whose unequal widths take the four-edge
column path), and three export rows. SVG draws real `A` arcs and needs no
subdivision count, so it is the control for the native-PNG row, which flattens
every wedge at `config.polar_bar_segments(span, turn)` vertices — six segments
for a 22.5-degree sector rather than the full-turn 96, so a regression back to a
flat count appears as an arc-flattening step change. The last row is a polar
heatmap's bounded screen-space inverse raster, which has no Cartesian twin.
Browser wedge vertex counts, GPU buffer lifetime, and radial-zoom frame pacing
are wall-clock/WebGL measurements and stay in `bench_interaction.py` and the
polar smokes.

`test_codspeed_selection.py` covers the backend handlers the client's gesture
messages resolve to: hover pick readout with a categorical channel, zone-pruned
and full-scan box select at 1M points, and the cross-filter
Expand Down
9 changes: 9 additions & 0 deletions benchmarks/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@
"status": "tracked",
"goal": "Compute correct positive log domains from zone statistics with cost proportional to chunks, not points.",
},
{
"id": "polar_coordinate_system",
"name": "Polar coordinate system",
"why": "Radar, wind rose, pie/donut and gauge views are a whole chart family, and a polar wedge is the most expensive mark in the engine — one annular sector per bar instead of one quad.",
"metrics": "payload-prep time, wedge flattening cost, SVG/PNG export latency, inverse-raster latency",
"harness": "benchmarks/test_codspeed_polar.py",
"status": "tracked",
"goal": "Keep polar payload prep bounded by composition size rather than observation count, and keep wedge subdivision proportional to each wedge's own angular span.",
},
{
"id": "static_export",
"name": "Static export",
Expand Down
Loading
Loading