What XY can be styled and extended with, per renderer. Generated from
python/xy/styling/capabilities.py, which tests/test_capability_registry.py
pins to styles.py and dom.py, so it cannot list a property the
implementation does not compile or omit one it does.
full means the renderer draws the property as specified. partial means it
draws something the notes have to qualify. none means it does not draw it —
which is sometimes deliberate, and the notes say which.
- 11 mark style properties across 22 mark kinds, drawn by all three renderers.
- 48 stable chrome slots, CSS- and Tailwind-addressable in the browser; 10 of them reach the native writers — nine through
styles={slot: ...}itself, androotthrough the chart-levelstyle=token bag. - 1 shipped extension point.
- 1 known default divergence between renderers, listed below rather than left to be discovered.
The subset a mark's style= mapping accepts. Anything outside it raises
before data is ingested, so no renderer silently drops a declaration another
one honors.
| property | vocabulary | mark kinds | webgl | svg | native | status |
|---|---|---|---|---|---|---|
opacity |
css | area, bar, box, column, contour, ecdf, error_band, errorbar, funnel, heatmap, hexbin, hist, histogram, line, ribbon, scatter, segments, stairs, stem, step, triangle_mesh, violin |
full | full | full | shipped |
fill |
svg | area, bar, box, column, error_band, hist, histogram, scatter, triangle_mesh, violin |
full | full | full | shipped |
fill-opacity |
svg | area, bar, box, column, error_band, funnel, heatmap, hexbin, hist, histogram, ribbon, scatter, triangle_mesh, violin |
full | full | full | shipped |
stroke |
svg | area, bar, box, column, contour, ecdf, error_band, errorbar, funnel, hist, histogram, line, ribbon, scatter, segments, stairs, stem, step, triangle_mesh |
full | full | full | shipped |
stroke-opacity |
svg | area, bar, box, column, contour, ecdf, error_band, errorbar, funnel, hist, histogram, line, ribbon, scatter, segments, stairs, stem, step, triangle_mesh |
full | full | full | shipped |
stroke-width |
svg | area, bar, box, column, contour, ecdf, error_band, errorbar, funnel, hist, histogram, line, ribbon, scatter, segments, stairs, stem, step, triangle_mesh |
full | full | full | shipped |
stroke-dasharray |
svg | area, ecdf, line, stairs, step |
full | full | full | shipped |
stroke-linecap |
svg | ecdf, line, stairs, step |
full | full | full | shipped |
border-radius |
css | bar, column, hist, histogram |
full | full | full | shipped |
wedge-gap |
xy | bar, column, hist, histogram |
full | full | full | shipped |
marker-shape |
xy | scatter |
full | full | full | shipped |
opacity— Multiplies the mark's own alpha in every renderer.fill— A plain color compiles to the mark's paint; alinear-gradient(...)compiles to a gradient and is accepted only by area and rect kinds, which are the ones with a gradient program.fill-opacity— Independent ofopacity; the two multiply.stroke— The paint for line-like geometry, and the border for filled marks.stroke-width— CSS px; a bare number is px, matching the chrome style convention.stroke-dasharray— 2-8 positive px lengths, ornone. The WebGL client tracks arc length on the CPU so dashes stay continuous across segments and constant on screen through zoom.stroke-linecap— Line family only — a cap is open-path geometry. XY's default isround, not CSS'sbutt, because the native rasterizer has always drawn round and is the reference for static export. Verified per renderer: a Rust coverage test, a rasterized-ink test, and three Chromium screenshots that hash differently per cap.border-radius— Rect kinds only.corner_radius=(tip, base)rounds the two ends separately.wedge-gap— Gap between neighbouring polar wedges, in px. Rect kinds undercoords="polar"only; ignored elsewhere. Deliberately a LENGTH rather than an angle: an angular pad's seam isr * dthetawide, so it tapers to nothing at the hole and reads as uneven spacing. The angular inset therefore grows as the radius shrinks, which is the same construction as d3's padAngle/padRadius pair. An XY vocabulary name: CSS has no gap between two arcs.marker-shape— 17 shapes, drawn as analytic signed-distance fields in all three renderers. An XY vocabulary name: CSS has no shape keyword for a non-DOM point mark, and the CSS spelling andsymbol=compile to the same value.
Stable data-xy-slot names that accept class_names= and styles= in the
browser. The native raster and vector writers have no cascade, so they read a
property subset rather than a stylesheet: the chart-level style= token bag,
plus styles={slot: ...} for the slots that name chrome a file actually
contains. class_names= reaches neither — a class selects a rule out of a
stylesheet an exported file does not have. That boundary is
contracted in export.md §9 and pinned by
tests/test_export_style_survival.py.
| slot | browser | native raster | native vector |
|---|---|---|---|
root |
full | partial | partial |
title |
full | partial | partial |
chrome |
full | none | none |
canvas |
full | none | none |
annotation_layer |
full | none | none |
labels |
full | none | none |
legend |
full | partial | partial |
legend_title |
full | partial | partial |
legend_item |
full | none | none |
legend_swatch |
full | none | none |
legend_label |
full | partial | partial |
colorbar |
full | partial | partial |
colorbar_bar |
full | none | none |
colorbar_extension |
full | none | none |
colorbar_line |
full | none | none |
colorbar_tick |
full | partial | partial |
colorbar_minor_tick |
full | none | none |
colorbar_title |
full | partial | partial |
tooltip |
full | none | none |
tooltip_title |
full | none | none |
tooltip_row |
full | none | none |
tooltip_label |
full | none | none |
tooltip_value |
full | none | none |
modebar |
full | none | none |
modebar_drag_handle |
full | none | none |
modebar_control_group |
full | none | none |
modebar_separator |
full | none | none |
modebar_button |
full | none | none |
modebar_icon |
full | none | none |
modebar_zoom_value |
full | none | none |
modebar_indicator |
full | none | none |
modebar_selection_icon |
full | none | none |
modebar_menu |
full | none | none |
modebar_menu_separator |
full | none | none |
modebar_menu_icon |
full | none | none |
modebar_menu_label |
full | none | none |
modebar_history_controls |
full | none | none |
selection |
full | none | none |
crosshair_x |
full | none | none |
crosshair_y |
full | none | none |
badge |
full | none | none |
badge_item |
full | none | none |
axis_band |
full | none | none |
axis_line |
full | none | none |
tick_mark |
full | none | none |
tick_label |
full | partial | partial |
axis_title |
full | partial | partial |
annotation_label |
full | none | none |
root(viachart style=) —styles={'root': ...}is browser-only, but the chart-levelstyle=token bag targets the same element and every renderer reads it (spec['dom']['style']). Prefer it for anything that must survive export.title(viastyles={'title': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.legend(viastyles={'legend': ...} / xy.legend(style=...) / --chart-legend-bg) — The frame box. Both spellings and the theme token now converge on one merged declaration block before the writers see it, so what agrees in the browser agrees in a PNG.background,boxShadow,borderRadius,--xy-legend-frame-alpha, andpadding/rowGapinemare honored; an explicit background paints opaque, as it does in the browser.legend_title(viastyles={'legend_title': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.legend_label(viastyles={'legend_label': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.colorbar(viastyles={'colorbar': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.colorbar_tick(viastyles={'colorbar_tick': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.colorbar_title(viastyles={'colorbar_title': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.tick_label(viastyles={'tick_label': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.axis_title(viastyles={'axis_title': ...}) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (fill, orcolor). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
Ways to add behavior the core does not ship, without forking it.
| extension point | status | entry point | limits |
|---|---|---|---|
| mark_plugin_composition | shipped | xy.register_mark / xy.MarkPlugin / xy.mark |
composes built-in marks only, one level deep; cannot reach the Figure, the trace list, or the column store; cannot add a GPU primitive |
| mark_plugin_shader | planned | — |
— |
| custom_renderer | planned | — |
— |
- mark_plugin_composition — A calc over declared columns plus a build that returns built-in marks. Its output is ordinary traces, so it reuses the built-in rendering, picking, and export paths rather than reimplementing them.
- mark_plugin_shader — §24's WGSL/GLSL snippet pair. Deferred: a plugin with its own shader reuses none of the built-in rendering, picking, or export paths and would have to reimplement them.
- custom_renderer — No way to add a fourth renderer or replace one of the three.
Differences in the defaults, which no style property selects — invisible until someone diffs two exports. Listed here for that reason.
| what | webgl | svg | native | visible when | tracked by |
|---|---|---|---|---|---|
| Interior vertices of a wide polyline | the notch two overlapping segment quads leave | round (the writer names it explicitly) | round (the capsule distance field fills the vertex) | stroke-width above ~4px at a sharp angle | no style property selects a join; the default is the whole contract |
uv run python scripts/gen_capability_matrix.py --write--check fails if this file is stale, and the test suite runs it.