Skip to content

3/7 Mark plugins: contribute a chart kind without forking (§24 v0) - #304

Closed
Alek99 wants to merge 1 commit into
claude/customization-capability-matrix-vckjxg-2-export-slot-contractfrom
claude/customization-capability-matrix-vckjxg-3-mark-plugin-api
Closed

3/7 Mark plugins: contribute a chart kind without forking (§24 v0)#304
Alek99 wants to merge 1 commit into
claude/customization-capability-matrix-vckjxg-2-export-slot-contractfrom
claude/customization-capability-matrix-vckjxg-3-mark-plugin-api

Conversation

@Alek99

@Alek99 Alek99 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Stacked on #303. This is the one that changes the answer rather than the documentation.

Why this is the unlock

The dossier has carried "no extensibility story" as an open risk since the audit, and it is the one dimension where xy loses outright rather than trades: Matplotlib has custom Artists, Bokeh has custom models, xy had a hardcoded dict of nineteen appliers. Every other comparison row is a trade; this one was a zero.

xy.register_mark(
    xy.MarkPlugin(name="hilo", columns=("t", "low", "high"), calc=_calc, build=_build)
)
chart = xy.chart(xy.mark("hilo", t="day", low="lo", high="hi", data=frame))

The design, and why it is a design rather than a limitation

build returns built-in Mark objects. It never receives the Figure, the trace list, or the column store.

That containment is the whole point. Because a plugin's output is ordinary traces, it inherits — for free, and with no way to get them wrong — decimation on large inputs (§28), picking, hover, the a11y summary (§20), the wire protocol's f32 discipline (§29), and all three export paths including the two with no browser. §24's third requirement, hover/a11y descriptors, turns out to need nothing at all for the same reason.

The custom-shader half of §24 stays deferred, and the argument above is exactly why. A plugin carrying its own GLSL inherits none of that and has to re-earn all of it. That is a position, not a backlog item.

Composition is one level deep — plugins compose built-ins, not each other — which keeps the registry a lookup instead of a dependency graph with cycles.

Registry rules

Refuses to shadow a built-in kind, and refuses to silently replace another plugin: two libraries registering "candlestick" is a conflict their user needs to see, not a race that import order settles.

Also

Records the dividing line in spec/api/chart-kind-contract.md: a kind needing a new GPU primitive is a core kind and pays the six-touch-point checklist; a kind that only composes existing ones is plugin territory and pays nothing.

12 new tests, including one asserting the context object exposes exactly five fields and no route to the engine. uv run pytest 2338 passed.


Generated by Claude Code

The dossier has listed "no extensibility story" as an open risk since the
audit, and it was the one dimension where XY lost outright rather than traded:
Matplotlib has custom Artists, Bokeh has custom models, and XY had a hardcoded
dict of nineteen appliers. This ships the half of §24 that can be shipped
honestly.

A plugin declares columns, a `calc` over them, and a `build` that returns
built-in `Mark` objects. `build` never receives the `Figure`, the trace list, or
the column store — it returns marks, and `_plugin_applier` runs them through the
same appliers, the same axis assignment, and the same post-processing as marks
written by hand.

That containment is the design, not a safety rail bolted on. Because a plugin's
output is ordinary traces, it inherits decimation on large inputs, picking,
hover, the a11y summary, and all three export paths without writing a line for
any of them — and it cannot draw anything the engine could not already draw.
§24's third requirement, hover/a11y descriptors, turns out to need nothing at
all for the same reason.

The custom-shader half of §24 stays deferred, and the argument above is exactly
why: a plugin carrying its own GLSL inherits none of that and has to re-earn all
of it. Deferring is the position, not the backlog.

Composition is one level deep. Plugins compose built-ins, not each other, which
keeps the registry a lookup instead of a dependency graph with cycles. The
registry also refuses to shadow a built-in kind and refuses to silently replace
another plugin — two libraries registering "candlestick" is a conflict their
user needs to see, not a race that import order settles.

Also records the dividing line in the chart-kind contract: a kind that needs a
new GPU primitive is a core kind and pays the six-touch-point checklist; a kind
that only composes existing ones is plugin territory and pays nothing.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: c2c6996b-76ed-427b-bbf9-46a24e58a9b4

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 claude/customization-capability-matrix-vckjxg-3-mark-plugin-api

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

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