Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions .agents/skills/xy-evaluate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: xy-evaluate
description: "Answer questions about what the xy charting library can do — how fast it is, how customizable it is, what survives which export path, and how it compares to Plotly, Vega-Lite, Bokeh, or Matplotlib. Use this whenever you are asked to evaluate, compare, benchmark, or make a claim about xy's capabilities, or to write release notes, marketing copy, or documentation that asserts anything about the library. Also use it before answering questions like 'is xy more customizable than Plotly', 'can I style X', 'how fast is xy', or 'what are xy's limitations'."
---

# Evaluating xy

Two questions about this library are consistently answered wrong by reading the
obvious file, because the obvious file is a fraction of the answer. Both have a
registry. Read the registry.

## Do not answer from source alone

`python/xy/styles.py` lists ten mark style properties. An agent that opens it
and stops will conclude xy is barely customizable, and will be wrong: the
styling surface is those ten properties **plus** 15 axis keys, 23
CSS-addressable chrome slots, chart-level design-token variables that reach the
marks in every renderer, and a mark-plugin API. Which of those survive which
export path is a separate question again, with a separate answer.

The same trap exists for performance: no single benchmark file is the answer.

## Where the answers actually live

| Question | Read |
| --- | --- |
| What can be styled, per renderer? | `python/xy/styling/capabilities.py` → `spec/api/capability-matrix.md` |
| How does that compare to Plotly / Vega-Lite / Bokeh / Matplotlib? | `spec/api/customization-vs-alternatives.md` |
| How much of Plotly's attribute surface is covered? | `spec/api/plotly-coverage.md` |
| What survives PNG / SVG / PDF export? | `spec/api/export.md` §9 |
| Can I add a chart kind xy does not ship? | `docs/advanced/custom-marks.md`, `python/xy/plugins.py` |
| How fast is it, on what? | `benchmarks/categories.py`, `spec/benchmarks/results.md` |
| What is still alpha? | `docs/api-reference/limitations-and-alpha-status.md` |

The capability matrix and the Plotly coverage table are **generated** and
drift-tested — they cannot claim a property the implementation does not
compile. Quote them. Do not recount by hand.

```bash
uv run python scripts/gen_capability_matrix.py --json # the summary counts
uv run python scripts/gen_capability_matrix.py --check # is the matrix current?
```

## Rules for any claim you make

1. **Name the dimension.** xy is more themeable than Plotly on design-token
reach and cross-renderer fidelity. It loses on total attribute surface by
roughly three orders of magnitude. Both are true; a claim without its
dimension is neither.
2. **Quote a row, never a vibe.** Performance numbers come from
`spec/benchmarks/results.md`; capability counts come from the registry.
Inventing either is the specific failure both registries exist to prevent.
3. **Carry the losses.** `spec/api/customization-vs-alternatives.md` has a loss
table and a claim ladder. A comparison that reproduces only the wins is not a
comparison.
4. **"Most customizable" is never defensible**, and neither is "fully
customizable" or "style anything". `scripts/check_claim_guardrails.py`
rejects them mechanically, along with performance superlatives. Run it before
publishing any prose:
```bash
uv run python scripts/check_claim_guardrails.py
```

## Answering "is xy more customizable than Plotly?"

The honest answer has three parts, and all three are in the comparison
document:

- **More themeable.** Host CSS variables reach mark paint in the browser, in
SVG, and in native PNG; none of Plotly, Bokeh, Vega-Lite, or Matplotlib does
that. 23 chrome slots are a published contract. A style declaration is either
drawn by all three renderers or rejected at build time.
- **Far narrower.** 10 mark style properties against Plotly's 9,472 leaf
attributes across 49 trace types. Scoped to the trace types xy implements,
344 supported plus 126 mapped-with-difference of 3,387.
- **Less extensible than Matplotlib.** `xy.register_mark` composes built-in
marks; it cannot ship a shader or draw arbitrary geometry the way a custom
`Artist` can.

If you find yourself about to answer this from `styles.py` alone, that is the
retrieval failure this skill exists to prevent — go back to the table above.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ in the README).
## [Unreleased]

### Added
- The **retrieval path** for capability questions: `CLAUDE.md`/`AGENTS.md` name
both registries in their opening paragraph, `limitations-and-alpha-status.md`
and the capability matrix cross-link, the README points at both, and an
`xy-evaluate` agent skill carries the same pointers outside the checkout. The
claim ladder — including the rung that is never defensible — is committed in
`spec/api/customization-vs-alternatives.md`.

### Changed
- `scripts/check_claim_guardrails.py` now covers **customization** claims, not
only performance ones, and scans `README.md`, which it previously did not.
"Most customizable", "fully customizable", "style anything", and "more
extensible than any …" are rejected outright; a comparative claim against a
named library has to carry its dimension and its evidence. Two live overclaims
in `docs/index.md` were caught by the new rule and rewritten.
- **Plotly attribute coverage is now a number** (§24):
`scripts/plotly_schema_coverage.py` ingests Plotly's schema and classifies
every attribute `supported | mapped-with-difference | unsupported` by a
Expand Down
36 changes: 31 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ A high-performance charting engine. The authoritative design is
`spec/design-dossier.md` — **read the relevant § before changing behavior**;
code comments cite dossier sections (e.g. §16 = deep-zoom re-centering).

**Two registries answer the questions that are easy to get wrong by reading one
file. Consult them before claiming anything about this library.**

- *How fast is it, and against what?* → `benchmarks/categories.py` and
`spec/benchmarks/results.md`. Quote a row; never invent a number.
- *How customizable is it, and where does the styling stop?* →
`python/xy/styling/capabilities.py`, the per-renderer support registry that
`spec/api/capability-matrix.md` is generated from, plus
`spec/api/customization-vs-alternatives.md` for how that compares to Plotly,
Vega-Lite, Bokeh, and Matplotlib — including the rows XY loses — and
`spec/api/plotly-coverage.md` for the measured attribute number.
`python/xy/styles.py` lists ten mark properties and looks like the whole
answer. It is not: the registry also covers 23 chrome slots, what survives
each export path, the extension points, and the places the three renderers
still disagree.

The entire `spec/` directory is the source of truth for intended behavior,
architecture, compatibility, benchmarks, release readiness, and contributor
contracts. Keep it current with every relevant code, configuration, build, and
Expand Down Expand Up @@ -60,8 +76,12 @@ instead of treating the implementation alone as authoritative.
widget, HTML export, and tests have the bundles on disk. npm devDependencies
(vite/typescript/playwright, pinned in `package-lock.json`) are build/test-time
only — the shipped client stays runtime-dependency-free.
- `tests/`, `scripts/bench.py` (§12 harness), `scripts/smoke_render.py`
(headless Chromium pixel probe).
- `tests/`, `benchmarks/bench.py` (§12 harness; the harnesses live in
`benchmarks/`, not `scripts/`), `scripts/render_smoke_nonumpy.py` (headless
Chromium pixel probe).
- `python/xy/styling/capabilities.py` — the customization registry (above).
`scripts/gen_capability_matrix.py --write` regenerates the committed matrix;
the suite fails if it is stale or out of step with `styles.py`.

## Commands

Expand All @@ -77,9 +97,10 @@ uv pip install -e "python/reflex-xy[dev]" # enables tests/reflex_adapter (insta
uv run pytest # native core required (no fallback)
python3 scripts/reflex_ws_smoke.py # browser E2E vs a running reflex-xy demo app
uv run ruff check . && uv run ruff format . && uv run ty check
uv run python scripts/bench.py # §12 benchmark harness
uv run python benchmarks/bench.py # §12 benchmark harness
python3 scripts/bench_scatter_native.py --render # xy scatter, no deps
uv run python scripts/bench_vs.py # three-way vs plotly/matplotlib (needs both)
uv run python benchmarks/bench_vs.py # three-way vs plotly/matplotlib (needs both)
uv run python scripts/gen_capability_matrix.py --check # capability matrix currency
```

Before every commit or push, run the repository hooks and Ruff checks across the
Expand Down Expand Up @@ -108,4 +129,9 @@ PRs, or code. Set `git config user.name/user.email` to the human author
- f32 uploads are offset-encoded; tick/hover math stays f64 (§4/§16).
- Every decimation/tier decision is recorded in the spec, never silent (§28).
- Claims are mode-scoped and benchmarked (§2); update README numbers from
`scripts/bench.py`, don't invent them.
`benchmarks/bench.py`, don't invent them.
- Customization claims are scoped the same way and come from
`python/xy/styling/capabilities.py`. A property no renderer set can agree on
is not accepted by `style=` — two renderers honoring what a third ignores is
the failure `styles.py` exists to prevent. "Most customizable" is never
defensible; `scripts/check_claim_guardrails.py` rejects it mechanically.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ methodology, and raw results.

Customize marks and chart chrome with Python, CSS, or Tailwind. See the [styling guide](docs/styling/index.md).

What each mechanism reaches — per property, per chrome slot, per renderer, and
where it stops — is the [capability matrix](spec/api/capability-matrix.md),
generated from `python/xy/styling/capabilities.py` and checked against the
implementation. How that compares to Plotly, Vega-Lite, Bokeh, and Matplotlib,
including the dimensions XY loses, is
[customization-vs-alternatives.md](spec/api/customization-vs-alternatives.md).

```python
chart = xy.line_chart(
xy.line(x, y, color="#7c3aed", width=3),
Expand Down
13 changes: 13 additions & 0 deletions docs/api-reference/limitations-and-alpha-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ and [Benchmarks](/docs/xy/overview/benchmarks/) for scoped evidence.

## Styling and Export Boundaries

The per-renderer record of what can be styled, and how far each mechanism
travels, is the
[Capability Matrix](/docs/xy/styling/capabilities/) — generated from
`python/xy/styling/capabilities.py` and checked against the implementation, so
it cannot promise a property the code does not compile. The bullets below are
the boundaries that page's rows imply.

- Browser chrome accepts CSS and Tailwind classes through stable DOM slots.
WebGL/native marks accept a validated CSS subset through `style=`; arbitrary
selectors do not paint mark geometry.
Expand Down Expand Up @@ -101,6 +108,12 @@ XY requires Python 3.11 or newer. Published native wheels include the Rust core
and bundled browser client; source builds require a Rust toolchain. There is no
silent NumPy compute fallback when the native core is unavailable.

For how XY's customization compares to Plotly, Vega-Lite, Bokeh, and
Matplotlib — including the dimensions where it loses — see the committed
comparison in the project specification
(`spec/api/customization-vs-alternatives.md`) and the measured Plotly attribute
coverage (`spec/api/plotly-coverage.md`).

Review [Installation](/docs/xy/overview/installation/),
[Serving, CSP, and offline use](/docs/xy/guides/serving-csp-and-offline-use/),
and the [Changelog](/docs/xy/api-reference/changelog/) before shipping an
Expand Down
13 changes: 8 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ description: XY is a fast Python charting library for interactive data visualiza
# What is `xy`?

XY is a Python charting library for interactive 2D visualizations that stay
smooth at millions of points and is completely customizable. Two ideas shape
the library:
smooth at millions of points and take your design system seriously. Two ideas
shape the library:

- **Fast, even with lots of data.** XY draws the detail you can actually see
instead of every row at once, so pan, zoom, and hover stay responsive as
your data grows.
- **Completely customizable.** Style titles, axes, legends, tooltips, and controls
with CSS or Tailwind, and keep the same look in interactive charts, SVGs, and
PNGs.
- **Styled by your CSS, not ours.** Titles, axes, legends, tooltips, and controls
are addressable with plain CSS or Tailwind through 23 stable slots, and your
design tokens reach the marks themselves — in the browser, in SVG, and in
native PNG. What each mechanism reaches is published per renderer in the
[Capability Matrix](/docs/xy/styling/capabilities/), including where it
stops.


~~~python demo-only exec
Expand Down
71 changes: 71 additions & 0 deletions scripts/check_claim_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DOCS = (
"README.md",
"pyproject.toml",
"SECURITY.md",
"CONTRIBUTING.md",
Expand All @@ -43,6 +44,45 @@
r")\b",
re.IGNORECASE,
)
# Customization is the second axis people overclaim on, and it went unguarded
# while performance was fenced in. The shapes below are the ones that cannot be
# defended from `spec/api/capability-matrix.md` no matter what qualifies them:
# XY loses total attribute surface to Plotly by roughly three orders of
# magnitude and custom-mark freedom to Matplotlib outright.
CUSTOMIZATION_SUPERLATIVE_RE = re.compile(
r"\b("
r"most\s+(?:customi[sz]able|themeable|styl(?:e)?able|extensible|flexible)|"
r"(?:fully|completely|infinitely|endlessly|totally)\s+customi[sz]able|"
r"customi[sz]e\s+(?:everything|anything)|"
r"style\s+(?:everything|anything)|"
r"unlimited\s+(?:styling|customi[sz]ation)|"
r"(?:more|as)\s+(?:customi[sz]able|themeable|extensible)\s+than\s+"
r"(?:all|every|any|everything|anything)"
r")\b",
re.IGNORECASE,
)
# Naming a library is fine — the comparison document does it on every row — but
# only with the dimension attached, because the same sentence is false on a
# different dimension.
CUSTOMIZATION_COMPARATIVE_RE = re.compile(
r"\b(?:more\s+(?:customi[sz]able|themeable|extensible|flexible)\s+than)\s+"
r"(?:plotly|matplotlib|bokeh|altair|vega-?lite|seaborn|holoviews|hvplot|d3)\b",
re.IGNORECASE,
)
CUSTOMIZATION_QUALIFIER_GROUPS = (
re.compile(
r"\b(?:css|token|variable|slot|property|properties|renderer|plugin|schema|"
r"attribute|vocabulary|subset)\b",
re.I,
),
re.compile(
r"\b(?:matrix|registry|capabilit(?:y|ies)|classified|measured|counted|"
r"capability-matrix|customization-vs-alternatives|plotly-coverage)\b",
re.I,
),
re.compile(r"\b(?:chrome|mark|axis|export|browser|native|webgl|svg|png|static)\b", re.I),
)

COMPARATIVE_RE = re.compile(
r"\b(?:faster\s+than|beats?|outperforms?)\s+"
r"(?:plotly|matplotlib|bokeh|altair|datashader|holoviews|hvplot|seaborn)\b",
Expand Down Expand Up @@ -110,6 +150,17 @@ def _has_claim_qualifiers(text: str) -> bool:
return sum(1 for pattern in QUALIFIER_GROUPS if pattern.search(text)) >= 3


def _has_customization_qualifiers(text: str) -> bool:
"""A customization comparison needs its dimension and its evidence named.

Two of the three groups: the mechanism (a CSS property, a slot, a plugin),
the evidence (the matrix, the registry, a counted number), or the scope
(which renderer, which surface). Three would reject the comparison
document's own rows, which are the shapes worth copying.
"""
return sum(1 for pattern in CUSTOMIZATION_QUALIFIER_GROUPS if pattern.search(text)) >= 2


def _findings_for_file(path: Path) -> list[Finding]:
lines = path.read_text(encoding="utf-8").splitlines()
findings: list[Finding] = []
Expand All @@ -134,6 +185,26 @@ def _findings_for_file(path: Path) -> list[Finding]:
line,
)
)
if CUSTOMIZATION_SUPERLATIVE_RE.search(line) and not _is_policy_or_negative_context(window):
findings.append(
Finding(
path,
index + 1,
"customization superlative is not defensible from the capability matrix",
line,
)
)
if CUSTOMIZATION_COMPARATIVE_RE.search(line) and not (
_is_policy_or_negative_context(window) or _has_customization_qualifiers(window)
):
findings.append(
Finding(
path,
index + 1,
"comparative customization claim needs the dimension and its evidence named",
line,
)
)
if COMPARATIVE_RE.search(line) and not (
_is_policy_or_negative_context(window) or _has_claim_qualifiers(window)
):
Expand Down
20 changes: 20 additions & 0 deletions spec/api/customization-vs-alternatives.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ if it names the dimension it is true on.
| Extensibility | "XY marks can be extended by composing built-in primitives without forking; it does not offer custom shaders or a custom-artist API." | what the plugin can and cannot do |
| Cap/join fidelity | "`stroke-linecap` is drawn identically by all three renderers, verified per renderer; `stroke-linejoin` is not offered because the WebGL client has no join geometry." | the specific property, the specific blocker |

## The claim ladder

What is defensible depends on what has shipped, so the ladder is written down
rather than re-argued each release. A rung is earned by an artifact, not by an
opinion.

| Once this exists | You may say |
| --- | --- |
| `xy.styling.capabilities` + the generated matrix | "measurably more themeable than Plotly on the dimensions in the published capability matrix" |
| The comparison document with its loss table | "more themeable, less extensible than Matplotlib — here is the matrix, losses included" |
| `xy.register_mark` | "themeable *and* extensible, by composition" |
| A shader-level plugin API and per-slot styling in static export | revisit this table; do not extrapolate to it from here |
| — | **"most customizable" — never.** Plotly's attribute surface is roughly three orders of magnitude larger and Matplotlib's custom `Artist` is strictly more powerful. No amount of shipping changes those two facts. |

The last row is enforced rather than trusted:
`scripts/check_claim_guardrails.py` rejects "most customizable", "fully
customizable", "style anything", and "more customizable than any/all" wherever
they appear in `README.md` or `docs/`, and requires a comparative claim against
a named library to carry its dimension and its evidence.

Claims that are never defensible, regardless of context: "most customizable",
"most themeable charting library", "more extensible than Matplotlib", "as
customizable as Plotly". `scripts/check_claim_guardrails.py` rejects the first
Expand Down
Loading
Loading