Skip to content

Docs: add polar, radar, radial bar, pie, and wind rose guides - #371

Draft
Alek99 wants to merge 18 commits into
alek/polar-axesfrom
agent/polar-chart-docs
Draft

Docs: add polar, radar, radial bar, pie, and wind rose guides#371
Alek99 wants to merge 18 commits into
alek/polar-axesfrom
agent/polar-chart-docs

Conversation

@Alek99

@Alek99 Alek99 commented Jul 28, 2026

Copy link
Copy Markdown
Member

Part 2 of the stack:

  1. Polar coordinate system: heatmap, sectors, log r, and error bars (protocol v12) #370 — polar coordinate system
  2. Docs: add polar, radar, radial bar, pie, and wind rose guides #371 — documentation and examples (this PR)

Summary

  • split the polar family into focused pages for Polar Overview, Radar, Radial Bar, Pie & Donut, and Wind Rose
  • add a dedicated Radial Bar cookbook with a compact six-bar example plus three polished live dashboard blocks: Allocation Overview, Training Summary, and Cache Tiers
  • give the first radial example six named series in distinct purple shades, visible degree/radial axes, and a right-side native legend while keeping its toolbar hidden
  • keep radial data outside chart construction, author explicit tracks and foreground sweeps, demonstrate full and partial sectors, and hide toolbars across all four live radial previews
  • add a dedicated Pie & Donut cookbook with a compact filled pie plus four polished live dashboard blocks: Market Share, Progress Rings, Revenue Mix, and Reliability Score
  • make the first pie example a true base=0 pie driven by an external PIE_DATA variable; angles, widths, percentages, and native legend labels update from the raw values
  • use a four-step purple palette anchored on the docs/sidebar primary #6e56cf
  • keep every Pie & Donut preview toolbar-free with xy.modebar(show=False) while retaining hover, wheel zoom, reset, and export APIs
  • give the dashboard blocks consistent 630 × 360 cards, balanced chart sizing, aligned labels and legends, constant-pixel separators, rounded sectors, center metrics, and clean supporting typography
  • separate Pie & Donut in the Polar Charts sidebar and add its own code-native gallery tile
  • replace the oversized Chart Gallery dropdown with a direct Overview link and sibling family accordions: Core Charts, Distributions, Density & Fields, Specialized, Polar Charts, and Components
  • refine Wind Rose into eight broad compass bins with three stacked speed bands plus directional spokes
  • add a live outlined-radar example and document the parent fix for radar_chart(fill=False), including line color, width, opacity, curve, and dash inheritance
  • document the latest parent tooltip behavior: named series lead the default readout, polar fields use θ/r, and θ reuses authored tick labels or the declared angular unit
  • document the final parent surface: polar heatmap, contour, and error-bar marks; partial-sector layout; display-space holes and data-space radial origins; categorical theta; log/symlog radius; polygonal grids; annotations; gradients; rounded/stroked sectors; export parity; and performance boundaries
  • align the README, changelog, generated API contracts, public cross-guides, roadmaps, and Matplotlib compatibility specs with the implementation

Radial Bar block details

  • Basic Radial Bar Chart: dependency-free RADIAL_DATA, six named purple-shaded bars, degree and radial axes, a right-side legend, and no toolbar
  • Allocation Overview: five compact progress rings, centered percentages, and an aligned exact-value allocation table
  • Training Summary: hero distance and goal rail, three supporting KPI rings, and a narrow operational-stat column
  • Cache Tiers: four nested semicircular capacity bars, a 2 × 2 metric matrix, gradients, and a four-item exact-value legend

Pie block details

  • Basic Pie Chart: toolbar-free, data-driven four-slice filled pie in sidebar-matched purple shades with category percentages in a native legend positioned to the left of the chart
  • Market Share: labeled six-segment donut, centered ecosystem value, and aligned two-column legend
  • Progress Rings: paired 40-position rounded dash rings with centered percentages and captions
  • Revenue Mix: per-slice gradients, rounded separators, dashed center metric, and exact value rows
  • Reliability Score: 240° qualitative gauge, dotted guide, score/status copy, and proportional threshold scale

The polished blocks keep exportable chart geometry inside XY and compose dashboard UI such as center labels and legends with Reflex. Hidden theta/r axes use tick_label_strategy="none" so the chart canvas fills each card without invisible tick gutters.

Stack

Validation

  • docs site tests — 89 passed (2 existing large-scatter soft-ceiling warnings)
  • polar chart tests — 80 passed
  • Ruff check and format check on the modified Python docs files — passed
  • whitespace/diff validation — passed
  • previous full polar API/static/client regression suite — 130 passed, including browser/WebGL probes
  • type-surface tests — 17 passed
  • public API verification — passed
  • protocol-12 browser client rebuild — passed
  • production Reflex compile and build — passed
  • sitemap, Markdown asset, and compiled HTML route validators — passed
  • live browser verification — all four Radial Bar previews and all five Pie & Donut previews expose zero chart-control toolbars; the basic radial legend/axes, allocation rings, training KPIs, and nested cache semicircles remain centered, unclipped, and legible

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: ef5a0c9f-e757-4f6c-a287-b063ebfcbbe3

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 agent/polar-chart-docs

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

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing agent/polar-chart-docs (e96da38) with alek/polar-axes (ee413d6)

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.

@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch 3 times, most recently from b301195 to 830b333 Compare July 28, 2026 21:02
@Alek99 Alek99 changed the title Docs: add polar chart guide Docs: add polar, radar, and wind rose guide Jul 28, 2026
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from 830b333 to c34b339 Compare July 28, 2026 21:34
Alek99 added 10 commits July 28, 2026 15:08
Polar is one coordinate system, not a family of chart types — which is how
both incumbents work: matplotlib has a PolarAxes projection that ordinary
plot/scatter/bar/fill calls render into, and Plotly composes radar, spider and
wind roses out of three polar traces. So this adds a coordinate system and
lets the existing mark registry render through it. MARK_KINDS gains no entries
and no _emit_<K> is rewritten.

`xy.polar_chart(...)` sets a chart-level `coords: "polar"`; each mark's first
channel becomes the angle and its second the radius, so `xy.line` and
`xy.scatter` are reused verbatim. `xy.theta_axis(unit=, zero=, direction=)`
and `xy.r_axis(...)` configure the two axes — sugar over the x/y axes rather
than new axis ids, since four separate places require an id to start with
'x'/'y' and the interaction axis policies are built on that grammar.

The (theta, r) -> pixel transform is specified normatively in
spec/design/polar-axes.md §3 and implemented twice: once in GLSL (xyPolarPos)
and once in Python (_svg._PolarProjection, which both exporters share). Prose
does not bind those; tests/fixtures/polar_transform.json does. The fixtures are
authored from the spec rather than generated from either implementation, and
every case is checkable by inspection — theta=0 lands due right, zero="N" with
clockwise puts 90 degrees due east. This is deliberately stronger than the
existing tick-math arrangement, where 30_ticks.ts and its hand port in _svg.py
are bound by nothing executable.

Details worth knowing:

- The y term differs by sign between the two implementations on purpose: screen
  space grows downward, GL clip space grows upward. A mirrored chart is
  otherwise entirely plausible-looking, so a fixture pins it.
- All three point-family shaders are transformed, not just POINT_VS. Scatter
  silently switches to POINT_SIMPLE_VS on its fast path, and PICK_VS feeds the
  hover hit test — untransformed, the picture stays right while hover reports
  the wrong row.
- _PolarProjection reports affine=False. Several emitters bake a straight-line
  data->pixel map into Rust behind `sx.affine and sy.affine`, which a polar
  chart on linear axes would otherwise satisfy while being non-affine.
- Data lines are chords between projected points (Plotly semantics), which is
  what makes radar edges straight; grid rings and the frame are true arcs. SVG
  draws rings as <circle>, and the raster path flattens the same rings to
  polylines because its display list has no arc opcode.
- Angular ticks need their own ladder: niceStep's [1, 2, 2.5, 5, 10] cannot
  reach 15/30/45/90, so degrees came out as 0/50/100/150.
- The radial axis starts at the centre and the angular axis spans a full turn.
  An autoscaled radial axis puts the smallest datum at the centre; an
  autoscaled angular axis puts spokes at arbitrary angles.

Scope is line and scatter. Every other kind is refused at build time with an
error naming the supported set, rather than approximated — the rect, area,
segment and mesh shaders expand geometry in pixel space after the coordinate
map, so under polar they would draw chord-edged shapes where arcs belong.
Polar traces ship tier="direct": M4 buckets on a monotonic screen-x column,
which a spiral is not, and density binning in (theta, r) distorts by area near
the origin.

Protocol 10 -> 11: a v10 client ignores `coords` and draws the columns as
cartesian x/y, so it must reject the payload rather than render a plausible
wrong picture.
Extends the polar coordinate system to two more mark families, both of which
Plotly composes rather than implements: radar/spider out of a filled
Scatterpolar, and wind roses out of Barpolar.

Area (and so radar): the quad interpolates in DATA space and projects the
result, rather than interpolating already-projected clip coordinates, so the
two radial edges run along true radii while the inner and outer edges come out
as chords between projected corners — which is what makes a radar polygon's
edges straight.

`xy.radar_chart(categories, ...)` closes each series itself, because the seam
is easy to get wrong: appending the first *angle* makes the closing segment
sweep backwards through the whole circle, so the closing sample goes at a full
turn instead. Spokes are labelled with the categories.

Bars: a polar bar is an annular sector, which four corners cannot express.
BAR_VS now sweeps a triangle strip of POLAR_BAR_SEGMENTS+1 vertex pairs across
the bar's angular span; SVG draws the same wedge with real `A` arcs; the raster
path flattens it, since its display list has no arc opcode. Subdivision is
fixed rather than view-adaptive — bar counts are small, and a view-dependent
count would have to be recorded per §28 rather than chosen silently.

`xy.wind_rose(directions, speeds)` bins in Python (the arrangement `hist`
already uses) and stacks polar bars, defaulting to the compass convention
(zero="N", clockwise) that makes 90 degrees read as east. Band edges round to
three significant figures, and the top edge rounds *up* so it still covers the
fastest observation instead of putting "27.2197" in the legend next to "2.77".

Two bugs this turned up, both found by comparing renderers rather than by
tests:

- The disc clip was reusing the id that also bounds every legend, so a legend
  sitting outside the circle vanished from the SVG while the raster still drew
  it. Marks now clip to the disc through a second id.
- Angular tick labels called the angle formatter directly, so authored
  `tick_labels` — the category names on a radar chart — lost to pi notation.
  Both exporters now route through `_tick_text`, and `_fmt_axis` gained the
  angle branch its JS twin already had.

Bars name their axis uniforms u_p/u_v rather than u_x/u_y, so `_drawBars`
needed the polar uniform upload explicitly; without it a wind rose drew
cartesian rectangles inside correct polar chrome.

Also lands the start of pyplot `projection="polar"` — the Axes projection
state, the PolarAxes method surface (set_theta_zero_location,
set_theta_direction, set_rlim/rmin/rmax, set_rgrids, set_thetagrids), and
`coords` flowing into the built chart. Sector limits raise NotImplementedError
naming the spec rather than silently ignoring the call. Routing is not yet
wired end to end.
Styling audit fixes, all found by exercising knobs against all three
renderers with distinctive values:

- The polar tick-label writers in both static exporters read only the
  chart-level `tick_label` slot, never the axis's own
  tick_label_color/tick_color. That silently disabled the `text=False` and
  `show=False` shorthands (which work by setting tick_label_color transparent)
  and any explicit per-axis label colour — while the browser client honoured
  them. Both exporters now apply the same axis-first precedence the cartesian
  labels use, per axis, so the theta and r labels style independently.

- The plot rect kept the cartesian tick-label gutters (L=76 R=38 T=36 B=66 on
  a 400x400 chart), which exist to hold edge-hugging labels a polar chart does
  not have — its labels ring the disc. The disc sat off-centre (cx=219) and
  smaller than it needed to be (r=143). `_inset_polar_plot` now gives those
  gutters back symmetrically: radius 143 -> 167 on a 400x400 chart, centred.
  Reservations that still mean something keep their room — the title band, a
  colorbar's right gutter, and the left gutter when the radial axis has a
  title (which is drawn there and would otherwise land at x = -10, off the
  canvas).

- The client re-cuts its rect identically (`_recutPolarPlot`), and bumps its
  `_topAxisRoom` the same way the exporters bump `top_axis_room` — without
  that the figure title anchored onto the topmost angular label.

Regression tests pin the per-axis independence (ring colour vs spoke colour,
theta text=False vs r text=False) in both SVG attributes and raster pixels.
tests/test_polar_transform.py and the fixture file both named
scripts/polar_parity_smoke.py as the consumer that binds the client's GLSL to
the shared fixtures — but the script did not exist, so the two-implementation
contract was only half enforced: the Python projection was fixture-bound while
xyPolarPos was verified by eye.

The probe renders one single-point scatter trace per fixture sample, each in a
unique saturated colour, reads the pixels back, and compares each colour's
centroid to the fixture value within 1.5 px. Two details make it exact:

- The fixture stores positions for its authored plot rect; the client computes
  its own. They reconcile without a third copy of the transform because a
  point's offset from the centre in units of the disc radius is
  rect-independent — pure arithmetic on fixture data rescales it to the
  runtime canvas.
- Points sit at 60% of each fixture radius so no sprite clips at the canvas
  edge; a half-clipped disc's centroid shifts inward, which would read as a
  transform error. Under a linear radial scale the rescale stays exact.

Scatter colour rides the channel dict (`t.color.color`), not `style.color` —
the probe's first version set only the style and every point rendered in the
palette fallback, which is worth a comment because hand-rolled specs will hit
it again.

Runs in the stdlib-only CI lane next to the other Chromium smokes.
CLAUDE.md treats a change as incomplete while its spec is stale, and three
increments had outrun the design doc: the shader table still called AREA_VS
and bars future, §7 still said line+scatter only, and the deferred table still
listed area/radar and bars/wind rose. Also records the plot-rect re-cut, the
POLAR_BAR_SEGMENTS contract, the radar full-turn closure rule, and the parity
probe's name now that it exists. Roadmap items 18/27/29/32/34 move to their
shipped statuses.
…nups

Two parallel audits (styling/customizability across the three renderers, and
structure/abstraction against the repo's conventions) plus a reported zoom
problem. Findings were verified by reproducing each one before fixing.

Interaction — the reported problem. Polar inherited the full cartesian gesture
set, so a drag panned the theta range and rescaled the disc as if the chart
were rectilinear, and wheel zoom anchored at the cursor's screen HEIGHT rather
than its radius. Polar now resolves its own axis policy: pan disabled, zoom
radial-only, and box-zoom/select/brush/crosshair off (a screen rectangle
neither matches a (theta, r) region nor reads as one — polar-axes.md §8). The
wheel anchor is the cursor's normalized radius, and xyPolarPos returns NaN
below the radial minimum so zoom-in culls those points instead of reflecting
them through the centre.

Renderer parity (each was: one renderer honoured it, another silently did not):

- A colormapped or size-channelled polar scatter took a SECOND Rust affine fast
  path — `affine_channel_points` — that projected (theta, r) as cartesian x/y:
  a diagonal line of points outside the frame ring. All six such gates now go
  through one `affine_fast_path(sx, sy, polar)` predicate rather than a
  `polar is None` conjunct repeated per site, which is how this one was missed.
- Cartesian edge tick marks leaked into polar in SVG and the client (the raster
  drew none). Guarded in both; tick_length/width/direction are documented as
  ignored under polar rather than half-drawn.
- `curve="smooth"` was honoured by the client and skipped by both exporters —
  two visibly different shapes from one chart. The client now chords too, for
  the reason the exporters already did.
- A constant `base=` on polar bars was dropped by the client (full pie slices
  from the centre) because the cartesian baseline uniform is clip-space. Bars
  now carry a data-space baseline uniform.
- PDF export of ANY polar chart raised: the converter's clip subset was
  rect-only. It now accepts a single <circle> clip, emitted as four Bezier
  quarter-arcs, and tolerates inert data-* markers.
- `tick_label_strategy="off"` and `tick_label_angle` were ignored by the polar
  label writers in all three renderers.
- radar_chart silently replaced its category spokes with numeric angles as soon
  as any theta_axis child was supplied; an authored axis now merges.
- The theta-axis title was drawn below the canvas edge because the rect re-cut
  reclaimed the bottom gutter it lives in.

Structure:

- Restore @cached_measurements to render_raster. Inserting a helper above it
  had silently transplanted the decorator onto that helper, costing every
  raster export its text-measurement cache.
- The polar label placement was copied between the two exporters despite a
  docstring claiming otherwise. Extracted `polar_tick_label_layout` in _svg.py
  returning renderer-neutral placements; each exporter keeps only its sink.
- Hoisted the GLSL cartesian/polar dispatch, pasted verbatim into four shaders,
  into POLAR_XYPOS_GLSL. AREA_VS/BAR_VS stay separate on purpose — they
  interpolate in data space before projecting.
- Fixed a dead guard in the rect re-cut (clamping before testing made the
  small-chart escape unreachable), renamed _inset_polar_plot to
  _recut_polar_plot to match its client twin, hoisted the client's duplicated
  rlabel/gap/compass constants, and corrected mirror comments that named
  symbols which do not exist (xyPolar, THETA_ZERO "in 30_ticks.ts").
- POLAR_DIRECT_CEILING was a constant the spec advertised and nothing enforced;
  polar traces past it now refuse with the reason.
- radar_chart's `fill=` was a documented no-op: it now rebuilds area children
  as line outlines. Non-area/line marks and column-name values raise instead of
  failing anonymously inside numpy. Dropped a no-op setdefault in wind_rose and
  a docstring frozen at the line+scatter increment.
Interactive testing and the styling audit's layout dimension caught four more:

- Zooming in drew marks past the outer ring into the rect corners (the GL
  canvas is the plot RECT; the disc clip only existed in the SVG exporter).
  xyPolarPos now culls rn > 1 the same way it culls rn < 0 — NaN position,
  which is the client's equivalent of the exporters' disc clipPath. Verified
  by pixel readback: zoomed to [0.59, 0.84], zero lit pixels outside the ring.
- A horizontal colorbar hangs off the plot's bottom edge, so the rect re-cut
  extending the plot downward pushed it clean off the canvas. The bottom band
  is kept whole when a horizontal colorbar (or a theta title) claims it, in
  both the exporters and the client.
- The re-cut's small-chart escape fell back to the cartesian rect, whose own
  40px floor can exceed a tiny canvas — an 80x80 chart drew its circle out to
  x=86. Too-small charts now take the largest centred box the canvas itself
  allows. (Also fixes the guard that clamped before testing, making the escape
  unreachable.)
- The angular label allowance was a fixed 30px, so authored radar category
  names ("EAST-NORTH-EAST") were hard-clipped at the canvas edge. The room is
  now measured from the widest authored label, capped so a pathological label
  shrinks the disc rather than erasing it. Generated angle text keeps the
  floor. Mirrored in the client.

Also finishes the pyplot polar routing: set_theta_zero_location /
set_theta_direction / set_theta_offset collected into _polar_options which
nothing read — the write-only state the structure review flagged. The options
now land on the built figure's theta axis, so
subplot(projection="polar") + compass conventions render correctly end to end.
Interactive testing found the NaN cull was the wrong tool for filled marks.
Culling is right for a point or a line vertex — there is no honest position
outside the range — but a fill or a bar has an EXTENT, and its visible extent
at a given angle is [base, top] intersected with [r_lo, r_hi]. Culling on one
out-of-range endpoint threw the whole primitive away:

- a radar polygon vanished the moment radial zoom lifted r_lo above its
  baseline, because every quad's inner corner went NaN;
- a wind-rose bar disappeared whole as soon as its tip crossed the outer ring,
  rather than clipping at the ring the way matplotlib and Plotly do.

AREA_VS and BAR_VS now clamp their radial span into the visible annulus, and a
span entirely outside collapses to zero and draws nothing. Both static
exporters clamp identically — the wedge builders bound outer and inner radius
(the raster has no disc clip to save it), and the area emitters clamp their r
columns before projecting, which also stops a below-minimum baseline mirroring
through the centre INSIDE the disc where the SVG clip cannot catch it.

Radial zoom now anchors at the CENTRE rather than the cursor's radius. The
cursor anchor was the natural reading of "anchor where you point", but on a
disc it lifts r_lo and carves a hole in the middle — an annulus view that
reads as broken rather than as zoom, and only looked right when the pointer
happened to be dead centre. Scaling the maximum about a fixed minimum is
Plotly's radial semantics and stays legible from any cursor position. Applied
at _zoomAt, so the wheel, the modebar buttons and axis-band gestures agree.
…ions

Probing customizability by rebuilding four ECharts-style donut/gauge designs
(evilcharts.com pie blocks) turned up the two things that stood between the
polar system and a pie chart.

Unequal angular widths. A bar's width may vary per bar; equal widths take the
compact path (one scalar width) while unequal ones ship four edge columns —
which under polar ARE an annular sector: (x0, x1) is the angular span and
(y0, y1) the radial one. That path was not polar-capable, so a donut drew as
cartesian rectangles inside polar chrome. RECT_VS now sweeps a sector exactly
as BAR_VS does, and both exporters build the same wedge (SVG real arcs, raster
flattened). This is what makes pie/donut a composition rather than a chart
type: a slice is one bar carrying its own width.

Point-anchored annotations. Centre text is `(any angle, r = 0)`, and the
separable scales read that as the bottom-left corner — a donut's centre label
landed outside the disc. `text`, `label`, `marker` and `arrow` now project
jointly through the transform in both exporters. `rule` and `band` deliberately
do not: a theta rule is a spoke, an r rule is a ring, a band is an annulus or a
sector, and drawing them as straight cartesian bars would be a wrong picture
rather than a missing one. Recorded in the spec's deferred table along with
sector layout — a gauge drawn as a partial arc still gets a full-circle plot
rect, so the unused portion is dead space.

All four reference blocks now reproduce: a 6-slice donut with a 52% hole, 3°
padding and centre text; dotted progress rings (40 sectors, 85-92% band); a
revenue donut with a 62% hole and right-hand legend; and a -30°..210° gauge
with four colour bands. Build script and a side-by-side page live outside the
repo, under /tmp/evil.
CI's ruff format gate caught tests/test_polar_charts.py: the last test block
was appended after the formatting pass, and only ruff check ran before the
commit. No behaviour change.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from c34b339 to 68260ed Compare July 28, 2026 22:33
…/arc fixes

Review fixes for three confirmed defects:

- Out-of-range radial data was neither culled nor clamped by the exporters'
  line and scatter paths. Below r_lo a point normalizes negative and mirrors
  through the centre to a position INSIDE the disc — no clip can hide it —
  and above r_hi the raster path (which has no disc clip) drew past the
  outer ring. The client shader NaN-culls both. _PolarProjection.visible_mask
  now applies the same predicate (same 1e-6 epsilon) in both exporters:
  scatter drops the rows, lines split into visible runs so a chord with a
  culled endpoint is dropped whole in every renderer. Spec §8 updated — it
  previously claimed the exports clip at the ring, which only SVG's
  above-range half actually delivered — and now also records the fill/bar
  clamp semantics it never stated.

- A full-turn sector (a 100% donut slice) rendered as nothing in SVG: the A
  arc's endpoints coincide and SVG omits such segments entirely. Spans of a
  full turn or more now draw each circle as two half-turn arcs, the inner
  ring wound oppositely so nonzero fill keeps the hole open. The raster
  polygon and the BAR_VS sweep were already correct.

- _fmt_angle/fmtAngle hardcoded degree precision to a step of 1, so an
  authored 22.5-degree grid labelled itself 22deg/68deg (round-half-even).
  The tick step now threads through from fmtAxis/_fmt_axis on both sides.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from 68260ed to 8711bbc Compare July 28, 2026 23:00
@Alek99 Alek99 changed the title Docs: add polar, radar, and wind rose guide Docs: add polar, radar, radial bar, and wind rose guides Jul 28, 2026
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch 2 times, most recently from 64f89ff to 2a06549 Compare July 28, 2026 23:35
Radial bar edges rendered jagged in the client: the GL context runs with
antialias: false, so every smooth edge is fragment-shader coverage — and
the polar wedge branch switched the rect SDF off (v_half = 1e6), leaving
all four edges of every wedge hard-aliased. Wide slices additionally
showed the 24-segment arc flattening as visible facets.

POLAR_WEDGE_GLSL now places the strip vertices XY_POLAR_AA px outside the
true sector (computed from the same uniforms as xyPolarPos, in pixel form,
because xyPolarPos's rn > 1 cull would eat the expanded outer vertices),
and RECT_FS trims the expansion back against the true annular-sector SDF
in device px. The fringe gets room to ramp on both sides of each edge, and
because the expanded chords stay outside the true outer arc the trimmed
arc is exactly round rather than faceted. Collapsed spans (radial clamp,
zero width) cull explicitly — the expansion would otherwise leave a ghost
sliver where nothing drew before. A full turn skips angular expansion and
angular coverage: its two ends are one seam, not edges.

POLAR_BAR_SEGMENTS rises 24 -> 96, sized so a full-turn wedge's chord
sagitta stays inside the expansion up to a ~1400-device-px disc; the
raster flattens the same count through its coverage-scanline fill, and
SVG's real arcs never needed one.

Verified: polar GLSL parity smoke green, full suite 3,541 passing, and
pixel-level headless-Chrome crops of a wind rose, a 100% donut ring and a
90-degree slice at dpr 1 and 2; cartesian bars (shared RECT_FS) unchanged.
render_smoke_nonumpy times out on this machine on the unmodified HEAD
bundle too — pre-existing local SwiftShader issue, covered by CI.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch 2 times, most recently from 72324ed to c637e59 Compare July 29, 2026 00:02
…e blocks

Rebuilding evilcharts' four ECharts pie blocks (market-share donut, dotted
progress rings, gradient revenue donut, banded gauge) as a customizability
probe surfaced four defects. All four were silent — every block built and
exported without a warning.

- The client projected NO annotation through the polar transform:
  js/src/51_annotations.ts had zero polar references and routed every kind
  through the separable _dataPxX/_dataPxY. Both exporters place them
  correctly, so the browser strung a donut's slice labels out in a horizontal
  row in theta order and dropped centre text at the left edge. This is the
  divergence the coordinate system exists to prevent, and the fix had landed
  only on the export side. A new _dataPxPoint mirrors the exporters' point()
  helper for text/label/marker/arrow/callout and the authored-scatter glyph
  pass; rule/band stay cartesian and deferred, as they are in Python.

- `padding=` was discarded under polar. _recut_polar_plot symmetrised the
  authored gutters away and gave the disc the whole canvas, so the band every
  donut composition reserves for its legend or caption vanished — measured on
  one 400x420 chart, cartesian moved the plot bottom 384 -> 280 while polar
  moved 383 -> 380. An authored box is now only inset by the label room.

- A gradient `fill=` reached the SVG and the browser but the raster painted it
  flat: the polar branches called cmd.fill(poly, flat) and never consulted
  style["fill"] the way the cartesian path does.

- `corner_radius` was accepted, shipped on the wire as style.corner_radius,
  and ignored by all three renderers — a silent approximation. It is now
  defined in the unrolled (arc, radial) frame, where a wedge is a rectangle
  and the standard rounded-rect profile applies: the client evaluates it in
  the annular-sector SDF, the exporters sample the same profile through
  _rounded_wedge_points. Plain wedges keep their exact A arcs. Three of the
  four blocks depend on this (6px dots, 12px slices, 10px bands).

Wedge strokes in the client fall out of the same SDF, which now carries the
stroke ring the rectangle path always had.

Suite 3,547 passing, polar GLSL parity smoke green, ruff/ty clean. Remaining
from the probe and unchanged here: sector layout (a 240-degree gauge still
gets a full-circle rect), polar rule/band, and polar select.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from c637e59 to f19544d Compare July 29, 2026 00:31
`xy.radar_chart(..., fill=False)` — the documented switch for outline radars,
and the whole "Lines Variant" family in the competitor gallery — raised
`KeyError: 'width'` for every input. Swapping only `kind` from "area" to
"line" handed `_apply_line` an area's prop dict, and the two marks do not
share a vocabulary: an area carries line_color/line_width/line_opacity where
a line carries color/width/opacity. `_radar_outline` now translates them,
falling back to the fill colour when the stroke was never given one.

The existing `test_radar_fill_false_outlines_instead_of_filling` passed
throughout because it asserts on the child mark kinds and never builds the
figure, so the crash sat one `.figure()` call beyond its reach.

Found by rebuilding the evilcharts ECharts radar blocks. The raster
marker/arrow/callout half of this fix landed independently in 3d41a74.

Suite 3,547 passing, ruff/ty clean.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from f19544d to 13c1325 Compare July 29, 2026 00:56
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch 2 times, most recently from c88219b to 1d057dd Compare July 29, 2026 01:40
@Alek99 Alek99 changed the title Docs: add polar, radar, radial bar, and wind rose guides Docs: add polar, radar, radial bar, pie, and wind rose guides Jul 29, 2026
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch from 1d057dd to 4c29fed Compare July 29, 2026 01:56
Hovering a donut slice reported "x: 102.6, y: 0.94" for a slice whose whole
identity is "Cloudpeak $13B". Three separate reasons, all fixed here.

- The default readout never showed the series name. The hover row carries
  `trace` (an id) and \_defaultTooltipItems emitted only x/y/color/size, so a
  mark was identified purely by its coordinates. It now leads with the trace's
  name when it has one, which is what every comparable library does and the
  only label that means anything on a pie.

- Under polar the channels were still called x and y. They are now theta and r
  (an explicit `labels=` override still wins).

- Angular values were raw numbers: "x: 1.5708" on a radar spoke the chart
  itself labels "power", and no degree sign anywhere. The angular value now
  goes through the axis's own text function, so degrees read "338deg",
  radians read as pi-fractions, and an authored tick label wins outright.
  Authored labels match with a tolerance of an eighth of the spoke spacing:
  the hovered angle arrives as decoded offset-encoded f32 while the tick was
  authored in f64, so pi/2 missed its own label by ~1e-7 and fell back to a
  number.

Verified by hovering real charts in headless Chrome across polar line,
scatter, area, polar bar, radar and a six-slice donut; cartesian readouts are
unchanged apart from now naming their series. Suite 3,597 passing, polar GLSL
parity smoke green, ruff clean.
@Alek99
Alek99 force-pushed the agent/polar-chart-docs branch 5 times, most recently from 2b7ccbe to e7429d8 Compare July 29, 2026 04:07
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