Skip to content

Sankey: ribbon primitive + layout engine - #375

Open
Alek99 wants to merge 3 commits into
mainfrom
alek/sankey
Open

Sankey: ribbon primitive + layout engine#375
Alek99 wants to merge 3 commits into
mainfrom
alek/sankey

Conversation

@Alek99

@Alek99 Alek99 commented Jul 29, 2026

Copy link
Copy Markdown
Member

A first-class Sankey for xy: xy.sankey_chart(links).

xy.sankey_chart([
    ("Inflow", "Equities", 78000), ("Inflow", "Bonds", 46000), ("Inflow", "Cash", 24000),
    ("Equities", "Growth", 61000), ("Equities", "Income", 17000),
    ("Bonds", "Income", 28000), ("Bonds", "Reserve", 18000), ("Cash", "Reserve", 24000),
])

Two halves:

The ribbon primitive — a flow band between two vertical spans, a colour at each end, gradient along the flow. New mark kind because no composition can express it: the seam-free triangle_mesh route in both exporters requires one uniform fill (np.all(fills == fills[0])), and the client's MESH_VS reads colour per instance (flat-shaded by construction). The chart-kind contract gains a normative ribbon geometry contract (wire shape, curveBumpX cubic, flow-axis paint, deferred picking) so a fourth renderer implements from one text; its "a ribbon is plugin territory" sentence is amended with the evidence.

  • SVG: exact cubics + a two-point userSpaceOnUse gradient helper (already in the PDF allowlist — PDF export works)
  • Raster: flattened through _scene.ribbon_polygon (the single reference the golden tests pin both exporters to), painted via cmd.grad's arbitrary gradient vector
  • Client: instanced triangle strip, 24 segments per edge to match the raster flattening, per-vertex colour mix; CPU hover by bisecting the monotone cubic (picking deferred — absent rather than wrong)

The layout (python/xy/_sankey.py) — longest-path layering with a cycle refusal that names the cycle, max(inflow, outflow) node values, alternating barycentre crossing-minimisation sweeps, value-proportional heights on one shared scale, endpoint stacking ordered by the opposite end. Nodes draw as a second ribbon trace: a band whose spans are equal is a rectangle, so the whole diagram is one primitive.

Protocol v10 → v11 in lockstep (markOf() falls back to scatter, so a stale client would silently render ribbons as a point cloud). ⚠️ PR #370 also claims v11 off the same base — whichever merges second rebases to v12.

18 new tests (layout invariants, refusals, wire shape, golden geometry including a rect-fall-through tripwire and a raster ink probe). Suite 3,466 passing, capability matrix regenerated, verified visually in all three renderers.

Deferred, recorded in the contract: GPU picking, hover highlight, legend swatches for two-colour bands, style.fill gradients on ribbon, cycle auto-breaking, LOD, mpl sankey shim.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added ribbon marks for flow bands with optional endpoint colors, flow-aligned gradients, opacity, and stroked outlines.
    • Added Sankey diagrams via xy.sankey_chart and xy.sankey with automatic layout, labels, and alignment controls.
    • Enabled ribbon rendering across WebGL, SVG, and raster/PNG, including end-to-end gradients.
    • Updated docs and gallery/API references for Sankey/ribbon.
  • Bug Fixes
    • Updated the wire protocol to v11 for correct ribbon payload handling.
    • Improved ribbon/Sankey tooltip semantics and hover/anchor behavior; refined ribbon styling behavior and stroke defaults.
  • Tests
    • Expanded test coverage for Sankey/ribbon rendering, exports, styling, geometry, and tooltips.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds ribbon and sankey chart support across Python APIs, wire payloads, SVG, raster, and WebGL renderers. Introduces Sankey layout computation with alignment modes, ribbon geometry tessellation, dual-color gradient channels, CPU hover containment testing, semantic tooltips, protocol v11, capability documentation, and comprehensive validation coverage.

Changes

Ribbon and Sankey rendering

Layer / File(s) Summary
Sankey layout with alignment modes and validation
python/xy/_sankey.py
Implements longest-path node layering, multi-mode horizontal alignment (left/right/center/justify), height computation for sink-relative positioning, and node-padding feasibility validation that rejects overpacked configurations.
Ribbon geometry contract and trace model
python/xy/_scene.py, python/xy/_trace.py
Defines cubic edge tessellation and polygon flattening for raster output, adds dual-color channel field to Trace for per-band gradient support, establishing the shared geometry contract across renderers.
Python public API and factories
python/xy/components.py, python/xy/__init__.py
Adds ribbon(...) and sankey(...) public mark factories, sankey_chart(...) chart constructor, internal _apply_ribbon and _apply_sankey compilation helpers, and applier registry dispatch for both mark kinds.
Figure methods, range handling, and styling
python/xy/_figure.py, python/xy/marks.py, python/xy/_payload.py, python/xy/styles.py
Introduces fluent Figure.ribbon and Figure.sankey methods, specialized ribbon range column selection, direct-tier payload emission with geometry validation, and style rules supporting fill-opacity and strokes while rejecting full-fill gradients.
SVG cubic path and gradient rendering
python/xy/_svg.py
Routes ribbon traces to dedicated SVG renderer, emits cubic paths with flow-aligned gradients, handles per-stop opacity when endpoint alphas differ, applies stroke outline with alpha composition.
Raster PNG tessellation and gradient emission
python/xy/_raster.py
Routes ribbon traces to raster path, uses reference polygon for segment flattening, emits solid fill when RGBA endpoints match or gradient with two stops when they differ, applies stroked outline.
WebGL shaders and rendering infrastructure
js/src/40_gl.ts, js/src/50_chartview.ts
Adds shader sources (RIBBON_VS, RIBBON_FS) for instanced triangle strips with cubic curve evaluation and per-band color interpolation; implements _buildRibbonMark, _drawRibbons, and ribbonProg for GPU rendering with axis mode uniforms and outline parameters.
WebGL hover, containment testing, and tooltips
js/src/50_chartview.ts, js/src/52_tooltip.ts, js/src/55_marks.ts, js/src/60_entries.ts
Adds _ribbonHover CPU-side cubic containment hit-testing, updates hover and tooltip handling for semantic rows, mark kind registration with color theming and GPU picking disabled, standalone hydration guards.
Protocol, tests, and validation documentation
js/src/00_header.ts, python/xy/config.py, spec/design/wire-protocol.md, tests/test_*.py, spec/api/, docs/
Bumps protocol version to 11, extends Sankey layout tests for alignment and padding validation, adds geometry/alpha/stroke tests, UI regression test for tooltip anchoring, updates capability matrix, wire contract, styling guide, roadmap, gallery, and API reference documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Figure
  participant SankeyLayout
  participant PayloadWriter
  participant Renderer as SVG/Raster/WebGL
  User->>Figure: sankey(links, layout_options)
  Figure->>SankeyLayout: compute_layout(links, align, iterations)
  SankeyLayout-->>Figure: nodes with layer/value/bounds
  Figure->>Figure: ribbon(src_lo/hi, dst_lo/hi, color, color_target)
  Figure->>PayloadWriter: emit ribbon trace (direct tier)
  PayloadWriter-->>Renderer: geometry + dual color channels
  Renderer-->>User: cubic path or polygon with gradient/outline
Loading

Possibly related PRs

  • reflex-dev/xy#310: Capability-matrix infrastructure for renderer divergences directly used by this PR's ribbon outline divergence documentation.
  • reflex-dev/xy#311: Tooltip rendering refactor in js/src/52_tooltip.ts extended here for ribbon semantic rows and dynamic anchor positioning.
  • reflex-dev/xy#341: Wire-protocol version constants updated in both, but this PR adds ribbon-specific v11 protocol semantics.

Suggested reviewers: sselvakumaran, farhanaliraza

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately highlights the main additions: the ribbon primitive and Sankey layout engine.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alek/sankey

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

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing alek/sankey (91655cc) with main (bd1d36e)

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.

A first-class Sankey, in two halves.

The `ribbon` primitive: a flow band between two vertical spans, carrying a
colour at each end with the gradient running along the flow. It is a new
mark kind because no composition can express it: the seam-free triangle_mesh
path in both exporters is gated on one uniform fill, so per-triangle colour
reintroduces an antialiasing seam on every shared edge, and the client's
MESH_VS reads colour per instance, so a mesh is flat-shaded by construction.
The chart-kind contract gains a normative ribbon geometry section (wire
shape, the curveBumpX cubic with control points at the horizontal midpoint,
flow-axis paint, deferred picking) and its "a ribbon is plugin territory"
sentence is amended with the evidence.

Renderers: SVG emits exact cubics with a new two-point user-space gradient
helper (already inside the PDF converter's allowlist); the raster flattens
through _scene.ribbon_polygon — the single geometry reference the golden
tests pin both exporters to — and paints through cmd.grad's arbitrary
gradient vector; the client sweeps an instanced triangle strip of the same
24 segments per edge, mixing the two end colours by flow progress. Hover
resolves on the CPU by bisecting the monotone cubic at the cursor's x and
testing containment; picking stays off (the id pass is point-geometry only),
so selection is absent rather than wrong.

The layout (python/xy/_sankey.py): longest-path layering with cycle refusal
that names the cycle, node value = max(inflow, outflow), alternating
barycentre sweeps for crossing minimisation, value-proportional heights on
one shared scale, and endpoint stacking ordered by the opposite end. Nodes
draw as a second ribbon trace — a band whose two spans are equal is a
rectangle — so the whole diagram is one primitive.

Public surface: xy.ribbon, xy.sankey, xy.sankey_chart(links) with hidden
axes and a y-inverted unit box. PROTOCOL_VERSION 10 -> 11 in lockstep with
the client: markOf() falls back to scatter for unknown kinds, so a v10
client would silently render ribbons as a point cloud.

NOTE: PR #370 (polar) also claims protocol v11 off the same base; whichever
merges second rebases to v12. Same for the roadmap rows.

Tests: 18 new (layout invariants, refusal messages, wire shape, golden
geometry incl. a rect-fall-through tripwire and a raster ink probe against
the reference polygon). Suite 3,466 passing, ruff clean, capability matrix
regenerated. Verified visually in all three renderers.

Deferred, recorded in the contract: GPU picking, hover highlight, legend
swatches for two-colour bands, style.fill gradients on ribbon, cycle
auto-breaking, LOD, matplotlib.sankey shim (recorded in shim-todo.md).
@Alek99
Alek99 marked this pull request as ready for review July 29, 2026 07:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (1)
python/xy/marks.py (1)

574-595: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

link_opacity coercion precedes its own error message.

float(link_opacity) on a non-numeric value raises a bare TypeError/ValueError instead of the named sankey link_opacity message right below it. Wrap it (or route through _validate.opacity) so the failure names the argument like every other mark.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/marks.py` around lines 574 - 595, The link_opacity validation in
the Sankey mark should normalize invalid values through the established opacity
validator or an equivalent guarded conversion, ensuring non-numeric and
out-of-range inputs raise an error that names “sankey link_opacity.” Update the
link_alpha validation block while preserving the existing accepted range of (0,
1].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/50_chartview.ts`:
- Around line 3866-3902: The _drawRibbons method does not write the shared axis
scale uniforms required for log and symlog rendering. In js/src/50_chartview.ts
lines 3866-3902, add uniform writes for u_xmode, u_xconstant, u_ymode, and
u_yconstant using _axisMode and _axisConstant with g.xAxis and g.yAxis, matching
_drawRects and _drawBars. No direct change is needed in js/src/40_gl.ts lines
657-713; its shared-uniform shader design already provides the required
contract.

In `@js/src/55_marks.ts`:
- Around line 169-188: Update the ribbon refreshColor handler to also re-resolve
g.colorTarget from the target trace color using parseColor, matching how
_buildRibbonMark initializes it and how g.color is refreshed. Ensure theme
changes update both ends of the ribbon gradient consistently.

In `@python/xy/_payload.py`:
- Line 849: Pass the required pw argument to _transition_entry at this call
site, keeping sel_arg in the sel parameter so transition_keys are narrowed by
the null-row selection. Match the argument ordering used by _emit_rect and the
signature _transition_entry(entry, t, pw, sel=None, key_values=None).

In `@python/xy/_sankey.py`:
- Around line 216-222: Clamp each column’s computed usable room to a
non-negative value before calculating scale, so the min ratio cannot become
negative when node_padding and count exceed the available space. Update the
usable comprehension near scale computation; preserve the existing zero-total
handling and non-finite scale fallback.

In `@python/xy/_svg.py`:
- Around line 3998-4008: The ribbon SVG path around `same`, `gradient_vector`,
and `alpha` must preserve differing endpoint alpha values instead of always
using `fills[i]`’s alpha. Update the gradient emission to represent or
consistently approximate interpolation from `a[3]` to `b[3]` (extending
`gradient_vector` for per-stop opacity if appropriate), while retaining the
solid-fill behavior when endpoints match and documenting any unavoidable
approximation.
- Around line 4009-4013: Update the ribbon SVG attribute generation near _css so
a positive stroke_width uses the trace color as the stroke fallback when
stroke_css is unset, matching _raster.py’s _emit_ribbon and the area/error_band
outline’s line_color behavior. Preserve explicit stroke colors and continue
omitting the outline when stroke_width is not positive.

In `@python/xy/styles.py`:
- Around line 294-298: Extend the target-key mapping used by the style compiler
for `stroke` and `stroke-width` to include ribbon kinds, alongside the existing
`_POINT_KINDS`, `_RECT_KINDS`, `_MESH_KINDS`, and `"box"` handling. Ensure
ribbon styles compile to `stroke` and `stroke_width` so `marks.ribbon` consumes
them as outline properties rather than mapping them to fill color and width.

In `@spec/api/styling.md`:
- Line 1218: Update the ribbon row’s Dash capability in spec/api/styling.md from
supported to unsupported, matching the current implementations of _emit_ribbon
and _ribbon_marks and the generated capability matrix. Keep the remaining ribbon
capability declarations unchanged.

---

Nitpick comments:
In `@python/xy/marks.py`:
- Around line 574-595: The link_opacity validation in the Sankey mark should
normalize invalid values through the established opacity validator or an
equivalent guarded conversion, ensuring non-numeric and out-of-range inputs
raise an error that names “sankey link_opacity.” Update the link_alpha
validation block while preserving the existing accepted range of (0, 1].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3265e591-4220-4295-9634-df1a3f53b8d9

📥 Commits

Reviewing files that changed from the base of the PR and between 22ba6e0 and 41cf87e.

📒 Files selected for processing (27)
  • docs/styling/capabilities.md
  • js/src/00_header.ts
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/55_marks.ts
  • python/xy/__init__.py
  • python/xy/_figure.py
  • python/xy/_payload.py
  • python/xy/_raster.py
  • python/xy/_sankey.py
  • python/xy/_scene.py
  • python/xy/_svg.py
  • python/xy/_trace.py
  • python/xy/components.py
  • python/xy/config.py
  • python/xy/marks.py
  • python/xy/styles.py
  • spec/api/capability-matrix.md
  • spec/api/chart-kind-contract.md
  • spec/api/chart-roadmap.md
  • spec/api/styling.md
  • spec/design/wire-protocol.md
  • spec/matplotlib/shim-todo.md
  • tests/pyplot/test_tick_side_rendering.py
  • tests/test_api_parity.py
  • tests/test_sankey.py
  • tests/test_type_surface.py

Comment thread js/src/50_chartview.ts
Comment thread js/src/55_marks.ts
Comment thread python/xy/_payload.py Outdated
Comment thread python/xy/_sankey.py
Comment thread python/xy/_svg.py Outdated
Comment thread python/xy/_svg.py Outdated
Comment thread python/xy/styles.py
Comment thread spec/api/styling.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/app/xy_docs/api_reference.py`:
- Line 87: Update the marks API table in MARKS to include xy.ribbon alongside
the existing xy.sankey entry, ensuring the ribbon primitive is exposed in the
generated API reference.

In `@docs/charts/sankey.md`:
- Around line 69-74: Update the fenced Python examples in the Sankey
documentation, including the block around the sankey_chart example and the
second block at the corresponding later section, by replacing each opening and
closing backtick fence with the configured tilde style: ~~~python and ~~~.
Preserve the example contents unchanged.

In `@python/xy/interaction.py`:
- Around line 263-266: Update the row_dict logic in interaction.py to detect and
process specialized Sankey ribbon/semantic rows before accessing t.x.values or
t.y.values. In that early path, merge source, target, and value from the ribbon
row, while preserving the existing tooltip_rows handling and generic coordinate
projection for non-ribbon traces.
- Around line 263-266: Update the tooltip_rows handling in the interaction
payload construction to exclude numeric flow values from JSON instead of passing
them through _json_scalar. Retain only semantic labels and references in out,
while preserving numeric tooltip data in the binary f32 interaction payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 431d8950-3b18-4148-8459-9da06413bf9e

📥 Commits

Reviewing files that changed from the base of the PR and between 41cf87e and 4b39558.

📒 Files selected for processing (21)
  • docs/app/scripts/check_html_routes.py
  • docs/app/tests/test_docs_site.py
  • docs/app/xy_docs/api_reference.py
  • docs/app/xy_docs/config.py
  • docs/app/xy_docs/gallery.py
  • docs/charts/sankey.md
  • docs/components/marks.md
  • docs/overview/gallery.md
  • docs/styling/mark-styles.md
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/52_tooltip.ts
  • js/src/60_entries.ts
  • python/xy/_payload.py
  • python/xy/_scene.py
  • python/xy/_trace.py
  • python/xy/interaction.py
  • python/xy/marks.py
  • spec/api/chart-kind-contract.md
  • tests/test_sankey.py
  • tests/test_ui_issue_regressions.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • python/xy/_scene.py
  • python/xy/_trace.py
  • js/src/50_chartview.ts
  • spec/api/chart-kind-contract.md
  • python/xy/_payload.py
  • tests/test_sankey.py
  • python/xy/marks.py

Comment thread docs/app/xy_docs/api_reference.py
Comment thread docs/charts/sankey.md Outdated
Comment thread python/xy/interaction.py Outdated
…ents

Review pass over the ribbon primitive and Sankey layout:

- WebGL: _drawRibbons writes the shared u_xmode/u_xconstant/u_ymode/
  u_yconstant uniforms RIBBON_VS reads, so log/symlog axes no longer
  render ribbons with linear geometry; theme refresh re-resolves
  color_target and the outline stroke, not just the source end.
- WebGL outlines: RIBBON_FS draws stroke/stroke-width/stroke-opacity as
  an inset border along the curved edges — the capability registry
  claimed webgl=full for stroke while the client drew nothing. The
  end-face difference vs the exporters' closed-path stroke is recorded
  in the divergence registry and the ribbon contract.
- styles: ribbon joins the stroke/stroke-width target sets, so
  style={"stroke": ...} reaches the outline instead of repainting the
  band, and stroke-width is no longer dropped.
- SVG/raster parity: stroke_width without a stroke colour falls back to
  the trace colour in SVG (the raster already did); both exporters fold
  opacity*stroke_opacity into the outline; flat-fill collapse compares
  all four channels, and endpoint alphas interpolate along the flow
  (SVG per-stop stop-opacity, raster RGBA gradient stops).
- Sankey layout: align="right" and align="center" are implemented
  (d3-sankey semantics) instead of silently behaving as "left"; an
  over-packed node_padding is refused by name instead of inverting
  every span through a negative shared scale.
- interaction: semantic tooltip rows replace the x/y placement
  projection in pick results, per the contract; the contract records
  that tooltip_rows values are deliberately JSON scalars (small-N
  semantic readouts, not bulk geometry).
- docs/spec: xy.ribbon exposed in the marks API reference and marks.md;
  styling.md ribbon row corrects the Stroke/Dash cells; sankey.md fence
  style (MD048) and alignment prose; capability matrix regenerated for
  the new divergence entry.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
js/src/50_chartview.ts (2)

3844-3859: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release ribbon-owned GPU buffers during trace teardown.

t0Buf, t1Buf, rgbaBuf, and rgba2Buf are allocated here, but _destroyTraceResources does not delete them. Rebuilds/spec updates will accumulate WebGL buffers.

Proposed fix
     this._deleteBuffers(g, [
       "xBuf", "yBuf", "cBuf", "sBuf", "selBuf", "baseBuf",
-      "x0Buf", "x1Buf", "x2Buf", "y0Buf", "y1Buf", "y2Buf",
+      "x0Buf", "x1Buf", "x2Buf", "y0Buf", "y1Buf", "y2Buf",
+      "t0Buf", "t1Buf", "rgbaBuf", "rgba2Buf",
       "posBuf", "value1Buf", "value0Buf",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/src/50_chartview.ts` around lines 3844 - 3859, Update
_destroyTraceResources to delete the ribbon-owned GPU buffers t0Buf, t1Buf,
rgbaBuf, and rgba2Buf when present, matching the allocations performed in the
ribbon setup block. Ensure teardown clears these resources during rebuilds and
spec updates without altering unrelated trace cleanup.

3924-3955: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Perform ribbon hit-testing in transformed axis coordinates.

The shader curves and interpolates after xyMap; this method does both in raw data space. On log or symlog axes, hover containment diverges from the rendered ribbon. Transform dataX/dataY and all decoded endpoints through _axisCoord() before bisection and edge interpolation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/src/50_chartview.ts` around lines 3924 - 3955, Update _ribbonHover to
perform all hit-testing in transformed axis coordinates: pass dataX and dataY,
plus every decoded x0/x1/y0/y1/t0/t1 endpoint, through _axisCoord() before
containment checks, bisection, and edge interpolation. Preserve the existing
ribbon iteration and interpolation logic while ensuring comparisons match the
shader’s xyMap behavior on log and symlog axes.
🧹 Nitpick comments (1)
python/xy/_sankey.py (1)

142-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a deque for the reverse Kahn queue.

pop(0) is O(n) per iteration, making _heights quadratic for wide graphs. Use deque.popleft() to keep the traversal linear in nodes plus links.

Suggested fix
+from collections import deque
+
-    queue = [n.index for n in nodes if outdegree[n.index] == 0]
+    queue = deque(n.index for n in nodes if outdegree[n.index] == 0)
     while queue:
-        current = queue.pop(0)
+        current = queue.popleft()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/_sankey.py` around lines 142 - 144, Update the queue initialization
and consumption in _heights to use collections.deque, replacing queue.pop(0)
with queue.popleft(). Preserve the existing reverse Kahn traversal and enqueue
behavior while ensuring queue operations remain linear-time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/50_chartview.ts`:
- Around line 3894-3898: Preserve per-band colors for implicit ribbon outlines
by adding an explicit-stroke flag in _drawRibbons and setting it based on
whether g.stroke is supplied; retain the existing fallback uniform for explicit
strokes. In js/src/50_chartview.ts lines 3894-3898, set the new uniform
alongside u_stroke. In js/src/40_gl.ts lines 708-735, update the ribbon outline
shader to derive the stroke color from v_rgba when the flag is unset, while
continuing to use u_stroke for explicit stroke colors.

In `@python/xy/styling/capabilities.py`:
- Around line 228-236: The WebGL ribbon endpoint-face limitation is incorrectly
classified as a default divergence. Reclassify or move ribbon_outline_end_faces
in the capabilities registry as a style-specific limitation, then regenerate
spec/api/capability-matrix.md so the WebGL ribbon stroke, stroke-opacity, and
stroke-width status and wording consistently indicate partial support; update
python/xy/styling/capabilities.py lines 228-236 and
spec/api/capability-matrix.md line 132.

In `@tests/test_sankey.py`:
- Around line 200-203: Update the Sankey specification generation and
serialization around tooltip_rows so numeric tooltip values are removed from the
JSON spec and transported through a raw f32 buffer with row references instead.
Retain canonical tooltip values as f64 on the CPU, and revise the assertions in
the test for exact_link and exact_node to verify the buffer-backed
representation and row references rather than only the absence of x/y fields.

---

Outside diff comments:
In `@js/src/50_chartview.ts`:
- Around line 3844-3859: Update _destroyTraceResources to delete the
ribbon-owned GPU buffers t0Buf, t1Buf, rgbaBuf, and rgba2Buf when present,
matching the allocations performed in the ribbon setup block. Ensure teardown
clears these resources during rebuilds and spec updates without altering
unrelated trace cleanup.
- Around line 3924-3955: Update _ribbonHover to perform all hit-testing in
transformed axis coordinates: pass dataX and dataY, plus every decoded
x0/x1/y0/y1/t0/t1 endpoint, through _axisCoord() before containment checks,
bisection, and edge interpolation. Preserve the existing ribbon iteration and
interpolation logic while ensuring comparisons match the shader’s xyMap behavior
on log and symlog axes.

---

Nitpick comments:
In `@python/xy/_sankey.py`:
- Around line 142-144: Update the queue initialization and consumption in
_heights to use collections.deque, replacing queue.pop(0) with queue.popleft().
Preserve the existing reverse Kahn traversal and enqueue behavior while ensuring
queue operations remain linear-time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 77b813c4-ceb7-4dfb-9509-1b45320f45b7

📥 Commits

Reviewing files that changed from the base of the PR and between 4b39558 and 91655cc.

📒 Files selected for processing (17)
  • docs/app/xy_docs/api_reference.py
  • docs/charts/sankey.md
  • docs/components/marks.md
  • docs/styling/capabilities.md
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/55_marks.ts
  • python/xy/_raster.py
  • python/xy/_sankey.py
  • python/xy/_svg.py
  • python/xy/interaction.py
  • python/xy/styles.py
  • python/xy/styling/capabilities.py
  • spec/api/capability-matrix.md
  • spec/api/chart-kind-contract.md
  • spec/api/styling.md
  • tests/test_sankey.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • spec/api/styling.md
  • docs/components/marks.md
  • docs/styling/capabilities.md
  • spec/api/chart-kind-contract.md
  • docs/charts/sankey.md

Comment thread js/src/50_chartview.ts
Comment on lines +3894 to +3898
gl.uniform1f(u("u_opacity"), this._fillOpacity(g.trace.style) * (g._legendDim ?? 1));
const stroke = g.stroke || g.color;
gl.uniform4f(u("u_stroke"), stroke[0], stroke[1], stroke[2], stroke[3]);
gl.uniform1f(u("u_strokeWidth"), (g.strokeWidth || 0) * this.dpr);
gl.uniform1f(u("u_strokeOpacity"), this._strokeOpacity(g.trace.style || {}) * (g._legendDim ?? 1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve per-band colors for implicit ribbon outlines. When style.stroke is absent, _drawRibbons falls back to uniform g.color; direct-RGBA ribbons therefore outline every band with the trace fallback rather than its interpolated endpoint color.

  • js/src/50_chartview.ts#L3894-L3898: add a uniform flag indicating whether an explicit stroke color was supplied.
  • js/src/40_gl.ts#L708-L735: when that flag is unset, derive the stroke from v_rgba instead of u_stroke, preserving each band’s source-to-target gradient.
📍 Affects 2 files
  • js/src/50_chartview.ts#L3894-L3898 (this comment)
  • js/src/40_gl.ts#L708-L735
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/src/50_chartview.ts` around lines 3894 - 3898, Preserve per-band colors
for implicit ribbon outlines by adding an explicit-stroke flag in _drawRibbons
and setting it based on whether g.stroke is supplied; retain the existing
fallback uniform for explicit strokes. In js/src/50_chartview.ts lines
3894-3898, set the new uniform alongside u_stroke. In js/src/40_gl.ts lines
708-735, update the ribbon outline shader to derive the stroke color from v_rgba
when the flag is unset, while continuing to use u_stroke for explicit stroke
colors.

Comment on lines +228 to +236
RendererDivergence(
id="ribbon_outline_end_faces",
what="A ribbon outline at the band's two vertical end faces",
webgl="absent (the outline is an inset border along the curved edges)",
svg="stroked (the closed path includes the end faces)",
native="stroked (the closed path includes the end faces)",
visible_when="stroke on a bare ribbon whose ends do not abut node bands",
tracked_by="the ribbon geometry contract's Outline clause",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Classify the WebGL ribbon-outline limitation as style-specific.

This occurs only after callers select stroke, so it is not a default divergence. It also conflicts with the matrix declaring WebGL ribbon stroke, stroke-opacity, and stroke-width as full; either render the endpoint faces or mark/document WebGL stroke support as partial.

  • python/xy/styling/capabilities.py#L228-L236: move or reclassify ribbon_outline_end_faces so the default-divergence registry remains accurate.
  • spec/api/capability-matrix.md#L132-L132: regenerate the matrix with matching style-specific capability status and wording.
📍 Affects 2 files
  • python/xy/styling/capabilities.py#L228-L236 (this comment)
  • spec/api/capability-matrix.md#L132-L132
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/styling/capabilities.py` around lines 228 - 236, The WebGL ribbon
endpoint-face limitation is incorrectly classified as a default divergence.
Reclassify or move ribbon_outline_end_faces in the capabilities registry as a
style-specific limitation, then regenerate spec/api/capability-matrix.md so the
WebGL ribbon stroke, stroke-opacity, and stroke-width status and wording
consistently indicate partial support; update python/xy/styling/capabilities.py
lines 228-236 and spec/api/capability-matrix.md line 132.

Comment thread tests/test_sankey.py
Comment on lines +200 to +203
# Semantic rows REPLACE the coordinate projection: the x/y slots hold a
# ribbon's internal placement (its target span), never a data readout.
for row in (exact_link, exact_node):
assert "x" not in row and "y" not in row

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep tooltip numeric values out of the JSON spec.

tooltip_rows remains part of spec, and its value entries are JSON floats. Removing internal x/y projections does not meet the wire contract. Ship numeric tooltip values through a raw f32 buffer with row references, retain canonical f64 values CPU-side, and assert that representation here.

As per coding guidelines, “Never place JSON numbers on the wire; transport data as raw f32 buffers” and “Keep canonical data CPU-side as f64.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sankey.py` around lines 200 - 203, Update the Sankey specification
generation and serialization around tooltip_rows so numeric tooltip values are
removed from the JSON spec and transported through a raw f32 buffer with row
references instead. Retain canonical tooltip values as f64 on the CPU, and
revise the assertions in the test for exact_link and exact_node to verify the
buffer-backed representation and row references rather than only the absence of
x/y fields.

Source: Coding guidelines

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