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.

- Time until every point is on screen, 10k to 100M points, for XY, Matplotlib, and Plotly. Lower is better. + + + Time until every point is on screen, 10k to 100M points, for XY, Matplotlib, and Plotly. Lower is better. +

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