diff --git a/README.md b/README.md
index 20736918..6c5483bf 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,10 @@ stable (10 byte-identical frames), so progressive renderers are charged until
their last chunk lands.
-
+
+
+
+
XY holds **0.071 s at 10k and 0.081 s at 100M**, flat across four orders of
diff --git a/benchmarks/plot_ux.py b/benchmarks/plot_ux.py
index fa116110..67e88bdc 100644
--- a/benchmarks/plot_ux.py
+++ b/benchmarks/plot_ux.py
@@ -18,6 +18,7 @@
from typing import Any
import xy
+from xy._benchmark_theme import BENCHMARK_DARK_THEME, BENCHMARK_LIGHT_THEME
GIB = 2**30
@@ -68,6 +69,7 @@ def build(
sizes: list[int],
arms: list[str],
metric: str,
+ color_scheme: str = "light",
) -> xy.Chart:
marks: list[Any] = []
notes: list[Any] = []
@@ -149,9 +151,11 @@ def build(
domain = (0.0, 5.8)
decades = [1e4, 1e5, 1e6, 1e7, 1e8]
+ theme = BENCHMARK_DARK_THEME if color_scheme == "dark" else BENCHMARK_LIGHT_THEME
return xy.line_chart(
*marks,
*notes,
+ xy.theme(**theme),
xy.legend(show=True, loc="upper left"),
xy.modebar(show=False),
xy.tooltip(format={"y": ".3f"}),
@@ -183,6 +187,7 @@ def main() -> None:
parser.add_argument("--arms", default="", help="comma-separated subset")
parser.add_argument("--suffix", default="")
parser.add_argument("--scale", type=int, default=2)
+ parser.add_argument("--color-scheme", choices=("light", "dark"), default="light")
args = parser.parse_args()
rows: dict[tuple[str, int], dict[str, Any]] = {}
@@ -198,7 +203,7 @@ def main() -> None:
args.out_dir.mkdir(parents=True, exist_ok=True)
for metric, stem in (("time", "render-time"), ("memory", "python-memory")):
- chart = build(rows, sizes, arms, metric)
+ chart = build(rows, sizes, arms, metric, color_scheme=args.color_scheme)
path = args.out_dir / f"ux-{stem}{args.suffix}.png"
chart.to_png(str(path), scale=args.scale)
print(f"wrote {path}")
diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py
index 724a0608..f315206e 100644
--- a/docs/app/tests/test_docs_site.py
+++ b/docs/app/tests/test_docs_site.py
@@ -305,6 +305,26 @@ def test_docs_app_configures_the_reflex_xy_adapter() -> None:
assert any(isinstance(plugin, reflex_xy.XYPlugin) for plugin in config.plugins)
+def test_benchmark_demo_uses_the_shared_theme() -> None:
+ from xy_docs.demos import benchmark_charts
+
+ from xy._benchmark_theme import (
+ benchmark_chart_class,
+ benchmark_live_theme,
+ )
+
+ assert benchmark_chart_class() == benchmark_charts._CHART_CLASS
+
+ live_theme = benchmark_live_theme()
+ assert benchmark_charts._theme().style == {
+ "background": live_theme["background"],
+ "--chart-bg": live_theme["plot_background"],
+ "--chart-grid": live_theme["grid_color"],
+ "--chart-axis": live_theme["axis_color"],
+ "--chart-text": live_theme["text_color"],
+ }
+
+
def test_docs_app_does_not_override_the_builtin_toolbar_palette() -> None:
assert not any(key.startswith("--chart-modebar-") for key in _CHART_STYLE)
diff --git a/docs/app/xy_docs/demos/benchmark_charts.py b/docs/app/xy_docs/demos/benchmark_charts.py
index 82b10a7f..d0ca918b 100644
--- a/docs/app/xy_docs/demos/benchmark_charts.py
+++ b/docs/app/xy_docs/demos/benchmark_charts.py
@@ -6,6 +6,10 @@
import reflex_xy
import xy
+from xy._benchmark_theme import (
+ benchmark_chart_class,
+ benchmark_live_theme,
+)
XY_COLOR = "#6E56CF"
XY_EXACT_COLOR = "#A594E8"
@@ -19,13 +23,7 @@
("Plotly", PLOTLY_COLOR),
)
-_CHART_CLASS = (
- "w-full [--benchmark-bg:#ffffff] [--benchmark-plot:#fcfcfd] "
- "[--benchmark-grid:#e8e8ec] [--benchmark-axis:#d9d9e0] "
- "[--benchmark-text:#60646c] dark:[--benchmark-bg:#09090b] "
- "dark:[--benchmark-plot:#111113] dark:[--benchmark-grid:#27272a] "
- "dark:[--benchmark-axis:#3f3f46] dark:[--benchmark-text:#d4d4d8]"
-)
+_CHART_CLASS = benchmark_chart_class()
_CARD_CLASS = (
"w-full overflow-hidden rounded-xl border border-secondary-4 bg-white "
"shadow-[0_12px_32px_#1c20240f] dark:bg-black"
@@ -34,13 +32,7 @@
def _theme() -> xy.Theme:
"""Return the neutral benchmark theme shared by the docs site."""
- return xy.theme(
- background="var(--benchmark-bg, #ffffff)",
- plot_background="var(--benchmark-plot, #fcfcfd)",
- grid_color="var(--benchmark-grid, #e8e8ec)",
- axis_color="var(--benchmark-axis, #d9d9e0)",
- text_color="var(--benchmark-text, #60646c)",
- )
+ return xy.theme(**benchmark_live_theme())
def _legend() -> rx.Component:
diff --git a/docs/overview/benchmarks.md b/docs/overview/benchmarks.md
index 2f5ac325..ce205a65 100644
--- a/docs/overview/benchmarks.md
+++ b/docs/overview/benchmarks.md
@@ -113,6 +113,8 @@ export CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
.venv/bin/python benchmarks/summarize_ux.py /path/to/xy-ux-suite
.venv/bin/python benchmarks/plot_ux.py /path/to/xy-ux-suite --out-dir charts
+.venv/bin/python benchmarks/plot_ux.py /path/to/xy-ux-suite --out-dir charts \
+ --color-scheme dark --suffix=-dark
```
Keep results separated by environment. Hardware WebGL and SwiftShader rows are
diff --git a/python/xy/_benchmark_theme.py b/python/xy/_benchmark_theme.py
new file mode 100644
index 00000000..dbeee553
--- /dev/null
+++ b/python/xy/_benchmark_theme.py
@@ -0,0 +1,50 @@
+"""Shared visual tokens for static and interactive benchmark charts.
+
+This is an internal module for the repository's benchmark tooling and docs app,
+not part of the public ``xy`` API.
+"""
+
+from __future__ import annotations
+
+BENCHMARK_LIGHT_THEME = {
+ "background": "#ffffff",
+ "plot_background": "#fcfcfd",
+ "grid_color": "#e8e8ec",
+ "axis_color": "#d9d9e0",
+ "text_color": "#60646c",
+}
+BENCHMARK_DARK_THEME = {
+ "background": "#09090b",
+ "plot_background": "#111113",
+ "grid_color": "#27272a",
+ "axis_color": "#3f3f46",
+ "text_color": "#d4d4d8",
+}
+BENCHMARK_CSS_VARIABLES = {
+ "background": "--benchmark-bg",
+ "plot_background": "--benchmark-plot",
+ "grid_color": "--benchmark-grid",
+ "axis_color": "--benchmark-axis",
+ "text_color": "--benchmark-text",
+}
+
+
+def benchmark_live_theme() -> dict[str, str]:
+ """Return theme values backed by the docs app's light/dark CSS variables."""
+ return {
+ token: f"var({css_variable}, {BENCHMARK_LIGHT_THEME[token]})"
+ for token, css_variable in BENCHMARK_CSS_VARIABLES.items()
+ }
+
+
+def benchmark_chart_class() -> str:
+ """Return the docs class that assigns both color schemes to the CSS variables."""
+ light = (
+ f"[{css_variable}:{BENCHMARK_LIGHT_THEME[token]}]"
+ for token, css_variable in BENCHMARK_CSS_VARIABLES.items()
+ )
+ dark = (
+ f"dark:[{css_variable}:{BENCHMARK_DARK_THEME[token]}]"
+ for token, css_variable in BENCHMARK_CSS_VARIABLES.items()
+ )
+ return " ".join(("w-full", *light, *dark))
diff --git a/spec/assets/ux-render-time-dark.png b/spec/assets/ux-render-time-dark.png
new file mode 100644
index 00000000..58d544dd
Binary files /dev/null and b/spec/assets/ux-render-time-dark.png differ
diff --git a/spec/assets/ux-render-time.png b/spec/assets/ux-render-time.png
index 5be2a5a1..87d98dfd 100644
Binary files a/spec/assets/ux-render-time.png and b/spec/assets/ux-render-time.png differ
diff --git a/tests/test_benchmark_theme.py b/tests/test_benchmark_theme.py
new file mode 100644
index 00000000..3cba3ee1
--- /dev/null
+++ b/tests/test_benchmark_theme.py
@@ -0,0 +1,51 @@
+"""Shared benchmark theme contract."""
+
+from __future__ import annotations
+
+from benchmarks.plot_ux import build
+
+from xy._benchmark_theme import (
+ BENCHMARK_CSS_VARIABLES,
+ BENCHMARK_DARK_THEME,
+ BENCHMARK_LIGHT_THEME,
+ benchmark_chart_class,
+ benchmark_live_theme,
+)
+
+_THEME_STYLE_KEYS = {
+ "background": "background",
+ "plot_background": "--chart-bg",
+ "grid_color": "--chart-grid",
+ "axis_color": "--chart-axis",
+ "text_color": "--chart-text",
+}
+
+
+def test_static_benchmark_export_uses_shared_theme() -> None:
+ rows = {("xy", 10_000): {"status": "ok", "visible_complete_ms": 71}}
+ for color_scheme, expected_theme in (
+ ("light", BENCHMARK_LIGHT_THEME),
+ ("dark", BENCHMARK_DARK_THEME),
+ ):
+ spec, _ = (
+ build(rows, [10_000], ["xy"], "time", color_scheme=color_scheme)
+ .figure()
+ .build_payload()
+ )
+ style = spec["dom"]["style"]
+ assert {
+ token: style[style_key] for token, style_key in _THEME_STYLE_KEYS.items()
+ } == expected_theme
+
+
+def test_live_benchmark_theme_derives_both_color_schemes() -> None:
+ assert benchmark_live_theme() == {
+ token: f"var({css_variable}, {BENCHMARK_LIGHT_THEME[token]})"
+ for token, css_variable in BENCHMARK_CSS_VARIABLES.items()
+ }
+
+ classes = set(benchmark_chart_class().split())
+ assert "w-full" in classes
+ for token, css_variable in BENCHMARK_CSS_VARIABLES.items():
+ assert f"[{css_variable}:{BENCHMARK_LIGHT_THEME[token]}]" in classes
+ assert f"dark:[{css_variable}:{BENCHMARK_DARK_THEME[token]}]" in classes