Skip to content
Open
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
10 changes: 9 additions & 1 deletion docs/engineering/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ or a CSS `px` value such as `"3px"`.
| `tick_length` | Non-negative pixel length |
| `tick_size` / `tick_label_size`, `label_size` | Positive pixel font size |
| `tick_direction` | `"in"`, `"out"`, or `"inout"` |
| `tick_label_anchor` | `"start"`, `"center"`, or `"end"` (mpl `ha` aliases `"left"`/`"right"`/`"middle"` normalize) — which label edge pins to the tick; rotated labels pivot about the pinned edge. Also a first-class `x_axis`/`y_axis` option. X defaults to `"center"`; y defaults to the tick-side edge (`"end"` left of the plot, `"start"` right of it). Honored by static SVG/PNG exports. |

```python
xy.x_axis(
Expand Down Expand Up @@ -231,7 +232,7 @@ Set them on `.xy` or any ancestor:

| Token | Themes | Default |
| --- | --- | --- |
| `--chart-bg` | Plot background | transparent |
| `--chart-bg` | Plot-rect background only (`theme(plot_background=)`, mpl `axes.facecolor`) | transparent |
| `--chart-text` | Title, tick/axis titles, legend, annotation labels | inherited text (canvas labels: `currentColor` @ 85%) |
| `--chart-grid` | Grid lines (canvas) | `currentColor` @ 14% |
| `--chart-axis` | Axis lines (canvas), modebar glyphs | `currentColor` @ 55% |
Expand All @@ -245,6 +246,13 @@ Set them on `.xy` or any ancestor:
| `--chart-annotation-text` | Annotation label color | falls back to `--chart-text` |
| `--chart-cursor` / `--chart-cursor-pan` | Plot cursor (box-zoom / pan) | `crosshair` / `grab` |

The **figure background** (matplotlib's `figure.facecolor` — the whole card
including margins, title, and tick labels) is not a token: `theme(background=)`
sets the root element's CSS `background` directly, and the plot rect shows it
unless `plot_background` (`--chart-bg`) paints the rect separately. Static SVG
and PNG exports reproduce both fills (solid colors; gradients stay
browser-only), so a dark card exports dark.

The compact toolbar appears while the chart is hovered or one of its controls
has keyboard focus. Drag its grip to move it within the chart. Zoom and
selection modes are grouped into menus; completed lasso selections expose up
Expand Down
296 changes: 296 additions & 0 deletions examples/demo-advance.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7fb27b941602401d91542211134fc71a",
"metadata": {},
"source": [
"# xy — demo\n",
"\n",
"Millions of points, interactive, in a notebook. Pan by dragging, zoom with the wheel, double-click to reset.\n",
"Shift-drag a scatter to box-select. One declarative API over one engine: the Reflex-flavored `xy.scatter_chart(...)` composition."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "acae54e37e7d407bbb7b55eff062a284",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"kernel backend: native\n"
]
}
],
"source": [
"import numpy as np\n",
"\n",
"import xy\n",
"import xy.kernels\n",
"\n",
"rng = np.random.default_rng(0)\n",
"print(\"kernel backend:\", xy.kernels.BACKEND)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "65c3a9b6-8b8a-483d-b158-b9699ab63188",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e48482622da54282876ce9934baaa513",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<xy.widget.FigureWidget object at 0x7e4597963bf0>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"models = [\n",
" \"GPT-5.6 Sol Ultra\",\n",
" \"GPT-5.6 Sol\",\n",
" \"Claude Mythos 5\",\n",
" \"GPT-5.6 Terra\",\n",
" \"Claude Fable 5\",\n",
" \"GPT-5.5\",\n",
" \"GPT-5.6 Luna\",\n",
" \"Claude Opus 4.8\",\n",
" \"Gemini 3.1 Pro Preview\",\n",
"]\n",
"scores = [91.9, 88.8, 88.0, 84.3, 84.3, 83.4, 82.5, 78.9, 70.7]\n",
"bar_colors = [\n",
" \"#eaf1fe\", # blue-1\n",
" \"#eaf1fe\", # blue-1\n",
" \"#47484f\", # gray-5\n",
" \"#cedffe\", # blue-2\n",
" \"#47484f\", # gray-5\n",
" \"#5477c4\", # blue-4\n",
" \"#a3befa\", # blue-3\n",
" \"#47484f\", # gray-5\n",
" \"#47484f\", # gray-5\n",
"]\n",
"\n",
"xy.bar_chart(\n",
" *[\n",
" xy.bar(\n",
" x=[m], y=[s], color=c, width=0.85, corner_radius=8, stroke=\"#f0f1f2\", stroke_width=1.5\n",
" )\n",
" for m, s, c in zip(models, scores, bar_colors, strict=True)\n",
" ],\n",
" *[\n",
" xy.text(\n",
" m,\n",
" s,\n",
" f\"{s:.1f}%\",\n",
" dx=0,\n",
" dy=-8,\n",
" anchor=\"middle\",\n",
" color=\"#ffffff\",\n",
" style={\"font_size\": \"15px\", \"font_weight\": \"500\"},\n",
" )\n",
" for m, s in zip(models, scores, strict=True)\n",
" ],\n",
" # tick_label_anchor=\"end\" pins each slanted name's trailing edge at its\n",
" # tick (mpl ha='right'), so labels hang below the axis like the reference.\n",
" xy.x_axis(label=None, tick_label_angle=-30, tick_label_anchor=\"end\", style={\"tick_length\": 6}),\n",
" xy.y_axis(\n",
" label=\"Score\",\n",
" domain=(0, 100),\n",
" style={\"tick_length\": 6},\n",
" tick_values=[0, 25, 50, 75, 100],\n",
" tick_labels=[\"0%\", \"25%\", \"50%\", \"75%\", \"100%\"],\n",
" ),\n",
" xy.legend(show=False),\n",
" # background= is the figure background (mpl figure.facecolor): it paints\n",
" # the whole card — margins, title, tick labels — and shows through the\n",
" # plot rect, so no separate plot_background is needed for a flat card.\n",
" xy.theme(\n",
" background=\"#000000\",\n",
" text_color=\"#ffffff\",\n",
" grid_color=\"transparent\",\n",
" axis_color=\"#f0f1f2\",\n",
" ),\n",
" styles={\n",
" \"title\": {\n",
" \"text_align\": \"left\",\n",
" \"padding_left\": \"40px\",\n",
" \"padding_top\": \"18px\",\n",
" \"font_size\": \"22px\",\n",
" \"font_weight\": \"700\",\n",
" },\n",
" \"tick_label\": {\"font_size\": \"13px\"},\n",
" \"axis_title\": {\"font_size\": \"15px\"},\n",
" },\n",
" # width=\"100%\" fills the notebook output row - VS Code paints a white\n",
" # backdrop behind widget outputs, so a fixed-width card would sit on white.\n",
" title=\"TerminalBench 2.1\",\n",
" width=\"100%\",\n",
" height=680,\n",
" padding=[90, 40, 160, 90],\n",
")"
]
},
{
"cell_type": "markdown",
"id": "076acb1b",
"metadata": {},
"source": [
"## Reference recreation — ExploitBench (dark line-and-marker benchmark)\n",
"\n",
"Five model series as **line + scatter pairs**: `xy.line` has no `marker=`, so the\n",
"dots are a separate `xy.scatter` per series; the scatter carries the `name`, so\n",
"legend entries get dot swatches and the unnamed lines stay out of the legend.\n",
"The dotted frontier rules are `xy.hline` with `style={\"dash\": ...}`. Rule labels\n",
"have no anchor control (they pin start-anchored at the plot's right edge and run\n",
"outward, clipping at the figure edge), so the frontier names are separate\n",
"end-anchored `xy.text` annotations pinned just inside the right edge, lifted above\n",
"each line with `dy`. The two single-model pins are `xy.marker`\n",
"(`symbol=\"diamond\"` / `\"square\"`) with side text.\n",
"\n",
"**Known gaps vs the reference (not worked around):** xy's legend `loc` only\n",
"anchors *inside* the plot rect — there is no outside-the-axes placement, so the\n",
"legend sits at `upper center` in the plot instead of above it; and there is no\n",
"image annotation, so the brand logo in the top-right corner is dropped.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b2f259d",
"metadata": {},
"outputs": [],
"source": [
"K = 1_000.0\n",
"\n",
"series = [\n",
" (\"GPT-5.6 Sol\", \"#eaf1fe\", [(30, 28.0), (62, 52.0), (95, 63.0), (105, 70.5), (118, 74.0)]),\n",
" (\"GPT-5.6 Terra\", \"#cedffe\", [(32, 22.0), (105, 41.5), (135, 46.0), (162, 53.0)]),\n",
" (\"GPT-5.6 Luna\", \"#a3befa\", [(35, 17.0), (80, 23.5), (108, 30.5)]),\n",
" (\"GPT-5.5\", \"#f6c3cb\", [(55, 28.0), (88, 40.3), (98, 47.8), (120, 48.0)]),\n",
" (\"GPT-5.4\", \"#ef6fa8\", [(85, 26.5), (125, 30.8), (135, 31.2), (160, 33.5), (200, 38.0)]),\n",
"]\n",
"\n",
"marks = []\n",
"for name, color, pts in series:\n",
" xs = [p[0] * K for p in pts]\n",
" ys = [p[1] for p in pts]\n",
" marks.append(xy.line(x=xs, y=ys, color=color, width=3))\n",
" marks.append(xy.scatter(x=xs, y=ys, color=color, size=10, opacity=1.0, name=name))\n",
"\n",
"# Rule labels have no anchor control, so the frontier names are end-anchored\n",
"# text annotations pinned just inside the plot's right edge, lifted clear of\n",
"# each dotted line with dy.\n",
"ref_label_style = {\"font_size\": \"17px\", \"font_weight\": \"700\"}\n",
"pin_style = {\"vertical_align\": \"middle\", \"font_size\": \"17px\", \"font_weight\": \"700\"}\n",
"\n",
"chart = xy.scatter_chart(\n",
" *marks,\n",
" xy.hline(78.2, color=\"#f6e8cd\", width=3, style={\"dash\": \"2.5,9\"}),\n",
" xy.hline(40.3, color=\"#f2a07b\", width=3, style={\"dash\": \"2.5,9\"}),\n",
" xy.text(\n",
" 548 * K, 78.2, \"Mythos 5\", dy=-14, anchor=\"end\", color=\"#f6e8cd\", style=ref_label_style\n",
" ),\n",
" xy.text(\n",
" 548 * K, 40.3, \"Opus 4.8\", dy=-14, anchor=\"end\", color=\"#f2a07b\", style=ref_label_style\n",
" ),\n",
" xy.marker(\n",
" 310 * K,\n",
" 74.0,\n",
" text=\"Mythos Preview\",\n",
" color=\"#f6e8cd\",\n",
" symbol=\"diamond\",\n",
" size=14,\n",
" stroke_width=0,\n",
" dx=18,\n",
" dy=0,\n",
" style=pin_style,\n",
" ),\n",
" xy.marker(\n",
" 225 * K,\n",
" 28.5,\n",
" text=\"Opus 4.7\",\n",
" color=\"#ef8a51\",\n",
" symbol=\"square\",\n",
" size=13,\n",
" stroke_width=0,\n",
" dx=18,\n",
" dy=0,\n",
" style=pin_style,\n",
" ),\n",
" xy.x_axis(\n",
" label=\"Output tokens\",\n",
" domain=(0, 560 * K),\n",
" tick_values=[100 * K, 200 * K, 300 * K, 400 * K, 500 * K],\n",
" tick_labels=[\"100K\", \"200K\", \"300K\", \"400K\", \"500K\"],\n",
" style={\"tick_length\": 6},\n",
" ),\n",
" xy.y_axis(\n",
" label=\"Cap percent\",\n",
" domain=(12, 84),\n",
" tick_values=[20, 40, 60, 80],\n",
" tick_labels=[\"20%\", \"40%\", \"60%\", \"80%\"],\n",
" style={\"tick_length\": 6},\n",
" ),\n",
" # loc anchors inside the plot rect; the reference's above-the-axes legend\n",
" # placement doesn't exist in xy yet (see the markdown cell above).\n",
" xy.legend(loc=\"upper center\", ncols=3),\n",
" xy.theme(\n",
" background=\"#000000\",\n",
" text_color=\"#ffffff\",\n",
" grid_color=\"transparent\",\n",
" axis_color=\"#f0f1f2\",\n",
" ),\n",
" styles={\n",
" \"title\": {\n",
" \"text_align\": \"left\",\n",
" \"padding_left\": \"60px\",\n",
" \"padding_top\": \"40px\",\n",
" \"font_size\": \"24px\",\n",
" \"font_weight\": \"700\",\n",
" },\n",
" \"tick_label\": {\"font_size\": \"16px\", \"font_family\": \"ui-monospace, monospace\"},\n",
" \"axis_title\": {\"font_size\": \"17px\"},\n",
" \"legend\": {\"font_size\": \"17px\"},\n",
" },\n",
" title=\"ExploitBench\",\n",
" width=1160,\n",
" height=1150,\n",
" padding=[100, 60, 110, 110],\n",
")\n",
"chart"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "xy (3.12.13.final.0)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
10 changes: 4 additions & 6 deletions examples/demo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -461,20 +461,18 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"id": "7efdda61-c484-454c-8247-489567bb8888",
"metadata": {},
"outputs": [],
"source": []
"source": "## Reference recreation — TerminalBench 2.1 (dark benchmark bars)\n\nA benchmark-leaderboard bar chart: black card, per-bar palette colors, rounded\nstroked bars, value labels above each bar, a percent y-axis, and slanted\ncategory labels hanging from tick marks. Per-bar colors come from composing\n**one `xy.bar` mark per category** (categories merge across marks in\nfirst-appearance order); the value labels are `xy.text` annotations anchored at\neach category; `tick_label_anchor=\"end\"` pins each rotated name's trailing edge\nat its tick so the labels hang below the axis; the dark card is\n`xy.theme(background=\"#000000\")` — the figure background (matplotlib's\n`figure.facecolor`), painting the whole card including margins and labels.\n`plot_background` would tint only the plot rect (`axes.facecolor`)."
},
{
"cell_type": "code",
"execution_count": null,
"id": "65c3a9b6-8b8a-483d-b158-b9699ab63188",
"metadata": {},
"outputs": [],
"source": []
"source": "models = [\n \"GPT-5.6 Sol Ultra\",\n \"GPT-5.6 Sol\",\n \"Claude Mythos 5\",\n \"GPT-5.6 Terra\",\n \"Claude Fable 5\",\n \"GPT-5.5\",\n \"GPT-5.6 Luna\",\n \"Claude Opus 4.8\",\n \"Gemini 3.1 Pro Preview\",\n]\nscores = [91.9, 88.8, 88.0, 84.3, 84.3, 83.4, 82.5, 78.9, 70.7]\nbar_colors = [\n \"#eaf1fe\", # blue-1\n \"#eaf1fe\", # blue-1\n \"#47484f\", # gray-5\n \"#cedffe\", # blue-2\n \"#47484f\", # gray-5\n \"#5477c4\", # blue-4\n \"#a3befa\", # blue-3\n \"#47484f\", # gray-5\n \"#47484f\", # gray-5\n]\n\nxy.bar_chart(\n *[\n xy.bar(\n x=[m], y=[s], color=c, width=0.85, corner_radius=8, stroke=\"#f0f1f2\", stroke_width=1.5\n )\n for m, s, c in zip(models, scores, bar_colors, strict=True)\n ],\n *[\n xy.text(\n m,\n s,\n f\"{s:.1f}%\",\n dx=0,\n dy=-8,\n anchor=\"middle\",\n color=\"#ffffff\",\n style={\"font_size\": \"15px\", \"font_weight\": \"500\"},\n )\n for m, s in zip(models, scores, strict=True)\n ],\n # tick_label_anchor=\"end\" pins each slanted name's trailing edge at its\n # tick (mpl ha='right'), so labels hang below the axis like the reference.\n xy.x_axis(label=None, tick_label_angle=-30, tick_label_anchor=\"end\", style={\"tick_length\": 6}),\n xy.y_axis(\n label=\"Score\",\n domain=(0, 100),\n style={\"tick_length\": 6},\n tick_values=[0, 25, 50, 75, 100],\n tick_labels=[\"0%\", \"25%\", \"50%\", \"75%\", \"100%\"],\n ),\n xy.legend(show=False),\n # background= is the figure background (mpl figure.facecolor): it paints\n # the whole card — margins, title, tick labels — and shows through the\n # plot rect, so no separate plot_background is needed for a flat card.\n xy.theme(\n background=\"#000000\",\n text_color=\"#ffffff\",\n grid_color=\"transparent\",\n axis_color=\"#f0f1f2\",\n ),\n styles={\n \"title\": {\n \"text_align\": \"left\",\n \"padding_left\": \"40px\",\n \"padding_top\": \"18px\",\n \"font_size\": \"22px\",\n \"font_weight\": \"700\",\n },\n \"tick_label\": {\"font_size\": \"13px\"},\n \"axis_title\": {\"font_size\": \"15px\"},\n },\n # width=\"100%\" fills the notebook output row - VS Code paints a white\n # backdrop behind widget outputs, so a fixed-width card would sit on white.\n title=\"TerminalBench 2.1\",\n width=\"100%\",\n height=680,\n padding=[90, 40, 160, 90],\n)"
}
],
"metadata": {
Expand All @@ -498,4 +496,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
Binary file added examples/exploitbench.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions js/src/20_theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ const XY_CHROME_CSS = `
@media (prefers-reduced-motion:reduce){:where(.xy [data-xy-slot="modebar"]){transition-duration:0s!important}}
@media (forced-colors:active){:where(.xy [data-xy-slot="modebar"],.xy [data-xy-slot="tooltip"]){border:1px solid CanvasText}:where(.xy [data-xy-slot="modebar_button"].xy-active){outline:2px solid Highlight}:where(.xy [data-xy-slot="canvas"]:focus){outline:2px solid Highlight}}
}
/* VS Code's Jupyter webview wraps ipywidget outputs in an opaque white card so
widgets that assume a light page stay legible on dark editor themes — the
same role as matplotlib's always-opaque white figure patch, so unthemed
charts keep it. A chart that brings its own background (theme(background=),
marked data-xy-own-bg) would sit in a white frame instead, so only those
outputs drop the backdrop. !important because the host rule this overrides
carries higher specificity; it stays outside the base layer because it
overrides host CSS rather than providing an overridable default. */
.cell-output-ipywidget-background:has(.xy[data-xy-own-bg]){background:transparent!important}
`;

// Inject XY_CHROME_CSS once per DOM root (document head or shadow root), so
Expand Down
Loading
Loading