Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 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
bd60ee6
Bound the static legend to the polar gutter, not the plot rect
Alek99 Jul 30, 2026
54c1bc3
Preserve polar path order, cover wind-rose bins, restore the drag esc…
Alek99 Jul 30, 2026
02dd85e
Exempt a time radius from the centre-origin default
Alek99 Jul 30, 2026
cf06081
Merge main into alek/polar-axes
Alek99 Jul 30, 2026
21dce72
Mirror the polar label-room and legend-gutter fixes into the exporters
Alek99 Jul 30, 2026
4d208ca
Refuse horizontal bars under polar
Alek99 Jul 30, 2026
63e216f
Merge Sankey main into polar axes
Alek99 Jul 30, 2026
e871047
Merge latest polar fixes
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
target/
.venv/
# No trailing slash: a worktree commonly symlinks .venv at a shared
# interpreter, and `.venv/` matches directories only — the symlink was
# committable (and got committed twice).
.venv
smoke/
dist/
__pycache__/
Expand Down
109 changes: 109 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,116 @@ 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
- 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. Both static exporters
bound the legend to the plot rect unioned with that gutter (`legend_clip_rect`,
shared so they cannot drift): the raster bounded it to the plot rect alone,
which is outside the gutter by construction, so a native PNG of any polar
figure lost its legend entirely while the SVG kept one.
- 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
- 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ next, no dates implied:
- **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