From a048bf5e98bbc7c6bcaaefb5fe56d5282c8cc609 Mon Sep 17 00:00:00 2001 From: Alek Date: Fri, 7 Aug 2026 13:10:29 -0700 Subject: [PATCH 1/5] docs(reflex): lead with data-bound integration --- docs/app/tests/test_docs_site.py | 2 + .../test_framework_integration_examples.py | 12 +- docs/integrations/reflex.md | 154 ++++++++++++------ 3 files changed, 117 insertions(+), 51 deletions(-) diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index 9b54ec67..04ed6648 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -2762,7 +2762,9 @@ def test_other_api_owned_pages_append_focused_tables() -> None: "/styling/themes-and-tokens/": ("xy.theme",), "/integrations/reflex/": ( "reflex_xy.chart", + "reflex_xy.data", "reflex_xy.figure", + "reflex_xy.scatter_chart", "reflex_xy.inline", "reflex_xy.append", ), diff --git a/docs/app/tests/test_framework_integration_examples.py b/docs/app/tests/test_framework_integration_examples.py index 838e7718..27befc22 100644 --- a/docs/app/tests/test_framework_integration_examples.py +++ b/docs/app/tests/test_framework_integration_examples.py @@ -56,8 +56,9 @@ def test_framework_integration_examples_run_in_isolation(tmp_path: Path) -> None examples = _python_examples() assert set(examples) == { "Install and Configure", - "Fixed Charts", - "State-Backed Charts", + "Fixed Data", + "State-Backed Data", + "Compose Multiple Marks", "Events and Streaming", "Custom Chrome Slots", } @@ -77,12 +78,13 @@ def test_framework_integration_examples_run_in_isolation(tmp_path: Path) -> None calls = [] namespace.update(token="chart-token", next_x=3, next_y=5) reflex_xy.append = lambda *args, **kwargs: calls.append((args, kwargs)) - namespace["reflex_xy"] = reflex_xy + namespace["rxy"] = reflex_xy exec(compile(source, f"{{doc_path}}#{{heading}}", "exec"), namespace) - if heading in {{"Fixed Charts", "State-Backed Charts"}}: - component = namespace["index"]() + if heading in {{"Fixed Data", "State-Backed Data", "Compose Multiple Marks"}}: + factory = "trends" if heading == "Compose Multiple Marks" else "index" + component = namespace[factory]() assert isinstance(component, rx.Component) assert "XYChart" in str(component) elif heading == "Install and Configure": diff --git a/docs/integrations/reflex.md b/docs/integrations/reflex.md index 96cb77a5..14b28978 100644 --- a/docs/integrations/reflex.md +++ b/docs/integrations/reflex.md @@ -3,7 +3,9 @@ title: Reflex description: Render fixed and state-backed XY charts as first-class Reflex components. components: - reflex_xy.chart + - reflex_xy.data - reflex_xy.figure + - reflex_xy.scatter_chart - reflex_xy.inline - reflex_xy.append --- @@ -40,11 +42,11 @@ Then register the bundled plugin: ~~~python # rxconfig.py import reflex as rx -import reflex_xy +import reflex_xy as rxy config = rx.Config( app_name="dashboard", - plugins=[reflex_xy.XYPlugin()], + plugins=[rxy.XYPlugin()], ) ~~~ @@ -52,31 +54,28 @@ The plugin attaches XY's binary data plane to the Reflex app's existing Socket.IO server. It does not add another HTTP service or websocket endpoint to deploy. -## Fixed Charts +## Fixed Data -Pass a regular `xy.Chart` directly to `reflex_xy.chart` when its data does not -depend on state. The adapter compiles a content-addressed binary asset during -the frontend build, so the result works with `reflex export` and needs no -backend connection. +Pass concrete columns through `data=` when they do not depend on state. The +adapter compiles a content-addressed binary asset during the frontend build, so +the result works with `reflex export` and needs no backend connection. ~~~python import numpy as np import reflex as rx -import reflex_xy +import reflex_xy as rxy import xy t = np.linspace(0, 4 * np.pi, 800) def index() -> rx.Component: - return reflex_xy.chart( - xy.line_chart( - xy.line(t, np.sin(t), name="signal"), - xy.x_axis(label="t"), - title="Static payload", - width="100%", - height=280, - ), + return rxy.line_chart( + data={"t": t, "signal": np.sin(t)}, + x="t", + y="signal", + x_axis=xy.x_axis(label="t"), + title="Static payload", height="280px", ) ~~~ @@ -85,43 +84,54 @@ Static charts retain browser-local hover, pan, zoom, and density refinement. They do not dispatch backend event handlers because there is no live kernel to resolve semantic event payloads. -## State-Backed Charts +## State-Backed Data -Use `@reflex_xy.figure` when chart data depends on session state. The computed -var stores only an opaque token; numeric columns travel as binary frames over -the app's existing websocket rather than through Reflex state JSON. +Use `@rxy.data` when chart columns depend on session state. Declare the +chart where it renders and return only its columns from the state method. The +chart structure is validated when `reflex run` compiles the app, without +running the data method. At runtime the computed var holds only a typed handle; +numeric columns travel as binary frames over the app's existing websocket +rather than through Reflex state JSON. ~~~python +from typing import TypedDict + import numpy as np import reflex as rx -import reflex_xy -import xy +import reflex_xy as rxy + + +class CloudData(TypedDict): + x: np.ndarray + y: np.ndarray + magnitude: np.ndarray class Dashboard(rx.State): points: int = 20_000 hovered: dict = {} - @reflex_xy.figure - def cloud(self) -> xy.Chart: + @rxy.data + def cloud(self) -> CloudData: rng = np.random.default_rng(7) x = rng.normal(size=self.points) y = 0.6 * x + rng.normal(scale=0.6, size=self.points) - return xy.scatter_chart( - xy.scatter(x, y, density=True), - width="100%", - height=420, - ) + return {"x": x, "y": y, "magnitude": np.hypot(x, y)} @rx.event - def record_hover(self, row: dict): - self.hovered = row + def record_hover(self, event: rxy.PointHoverEvent): + self.hovered = event.get("data", {}) def index() -> rx.Component: return rx.vstack( - reflex_xy.chart( - Dashboard.cloud, + rxy.scatter_chart( + data=Dashboard.cloud, + x="x", + y="y", + color="magnitude", + colormap="viridis", + density=True, on_point_hover=Dashboard.record_hover, height="420px", ), @@ -130,8 +140,63 @@ def index() -> rx.Component: ) ~~~ -Figure builders may also be `async def`, following the same rules as Reflex -async computed vars. +The `TypedDict` return annotation lets the integration catch misspelled column +names while the page compiles. A plain mapping return type also works, but its +column names can only be checked when the data method first runs. Data methods +may be `async def` and may return `None` when no data is currently available. + +### Compose Multiple Marks + +For multiple marks, pass data-free XY nodes to `rxy.chart`. Channel values are +column-name strings, and every mark binds to the same data handle: + +~~~python +from typing import TypedDict + +import numpy as np +import reflex as rx +import reflex_xy as rxy +import xy + + +class TrendData(TypedDict): + x: np.ndarray + y: np.ndarray + magnitude: np.ndarray + + +class Trends(rx.State): + @rxy.data + def samples(self) -> TrendData: + x = np.linspace(0, 10, 1_000) + y = np.sin(x) + return {"x": x, "y": y, "magnitude": np.abs(y)} + + +def trends() -> rx.Component: + return rxy.chart( + xy.scatter("x", "y", color="magnitude", density=True), + xy.line("x", "magnitude", name="magnitude"), + xy.x_axis(label="feature A"), + xy.legend(), + data=Trends.samples, + height="420px", + ) +~~~ + +Both the flat factories such as `rxy.scatter_chart` and composed `rxy.chart` +build a data-free plan at page evaluation. Invalid chart options and unknown +columns in a typed schema therefore fail at compile time rather than producing +a blank chart in the browser. Changing state republishes only the columns under +the stable data handle, preserving the mounted chart's view and selection. + +Data handles are ordinary Reflex vars. They can be selected with `rx.cond`, or +collected in a typed `list[rxy.DataHandle[Schema]]` for `rx.foreach`, as long as +each source satisfies the chart's column schema. + +Use `@rxy.data` for every state-backed chart whose marks, axes, and other +structure can be declared in the page. `@rxy.figure` remains an escape hatch +only for the uncommon case where state changes that structure itself. ## Events and Streaming @@ -139,8 +204,7 @@ async computed vars. `on_animation_start`, and `on_animation_end` dispatch small semantic payloads through normal Reflex event handlers. Large chart buffers never enter those payloads. These props belong on the outer -`reflex_xy.chart(...)` component and work only with a live token source, such -as an `inline()` token or an `@reflex_xy.figure` var. +`rxy` chart factory and work with a live `@rxy.data` source. They are separate from the core callbacks accepted by `xy` chart containers. Core `on_hover`, `on_click`, `on_brush`, `on_select`, and `on_view_change` @@ -173,7 +237,7 @@ To extend a registered chart from an event or background task, append new points without rebuilding the component: ~~~python -reflex_xy.append(token, x=[next_x], y=[next_y]) +rxy.append(token, x=[next_x], y=[next_y]) ~~~ See [Real-time and streaming data](/docs/xy/guides/real-time-and-streaming-data/) @@ -183,13 +247,11 @@ for the mutation and snapshot contract. | Component source | Best for | Backend | | --- | --- | --- | -| A direct `xy.Chart`: `reflex_xy.chart(chart)` | Fixed, exportable charts | None | -| A module-scope `token = reflex_xy.inline(chart)`, then `reflex_xy.chart(token)` | Fixed data with kernel round-trips | XY registry | -| An `@reflex_xy.figure` var: `reflex_xy.chart(State.figure)` | Session and state-driven charts | Reflex + XY registry | +| Concrete columns: `rxy.scatter_chart(data={...}, ...)` | Fixed, compile-bound columns | None | +| An `@rxy.data` var: `rxy.scatter_chart(data=State.data, ...)` | State-driven columns with fixed, compile-validated structure | Reflex + XY registry | -`inline()` should run at module scope so every backend worker registers the -same content-addressed token. Despite its name, `inline()` is the live, -kernel-backed fixed-data tier; passing a Chart directly is the static tier. +Prefer the concrete-column form for fixed data and `@rxy.data` for state-backed +data. Both use the same compile-validated chart API; only the transport changes. ## Custom Chrome Slots @@ -222,6 +284,6 @@ objects never enter standalone HTML. For ordinary DOM customization, use the The Reflex adapter and callback payload details are still experimental. Pin `xy` when you need a stable integration contract, and build against -`reflex_xy.chart`, `@reflex_xy.figure`, and `reflex_xy.append` rather than -private transport or registry modules. +`rxy.chart`, `@rxy.data`, and `rxy.append` rather than private transport or +registry modules. ~~~ From 9b80e84201c0e560b93640fe6bc93e94dc549a21 Mon Sep 17 00:00:00 2001 From: Alek Date: Fri, 7 Aug 2026 14:09:59 -0700 Subject: [PATCH 2/5] docs(reflex): list line chart factory --- docs/app/tests/test_docs_site.py | 1 + docs/integrations/reflex.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index 04ed6648..75afd0b6 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -2765,6 +2765,7 @@ def test_other_api_owned_pages_append_focused_tables() -> None: "reflex_xy.data", "reflex_xy.figure", "reflex_xy.scatter_chart", + "reflex_xy.line_chart", "reflex_xy.inline", "reflex_xy.append", ), diff --git a/docs/integrations/reflex.md b/docs/integrations/reflex.md index 14b28978..3aae57cd 100644 --- a/docs/integrations/reflex.md +++ b/docs/integrations/reflex.md @@ -6,6 +6,7 @@ components: - reflex_xy.data - reflex_xy.figure - reflex_xy.scatter_chart + - reflex_xy.line_chart - reflex_xy.inline - reflex_xy.append --- From c2bfb8cdcb84393c67839e944a9ec9db1fb3e9ed Mon Sep 17 00:00:00 2001 From: Alek Date: Fri, 7 Aug 2026 14:16:57 -0700 Subject: [PATCH 3/5] docs(reflex): render live data examples --- docs/app/tests/test_docs_site.py | 15 +++++++++++++++ .../tests/test_framework_integration_examples.py | 2 +- docs/integrations/reflex.md | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index 75afd0b6..f0601cb9 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -2786,6 +2786,21 @@ def test_other_api_owned_pages_append_focused_tables() -> None: assert "Preview" not in adapter_api +def test_reflex_integration_renders_the_state_backed_data_example() -> None: + """Keep the primary @rxy.data example visible as a live chart demo.""" + page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/integrations/reflex/") + live_blocks = [ + block + for block in parse_document(page.content).blocks + if isinstance(block, CodeBlock) and {"demo", "exec"} <= set(block.flags) + ] + + assert len(live_blocks) == 2 + state_backed = next(block for block in live_blocks if "@rxy.data" in block.content) + assert "rxy.scatter_chart(" in state_backed.content + assert "XYChart" in str(render_xy_markdown_page(page)) + + @pytest.mark.parametrize( ("metadata", "exception"), ( diff --git a/docs/app/tests/test_framework_integration_examples.py b/docs/app/tests/test_framework_integration_examples.py index 27befc22..11279558 100644 --- a/docs/app/tests/test_framework_integration_examples.py +++ b/docs/app/tests/test_framework_integration_examples.py @@ -41,7 +41,7 @@ def _python_examples() -> dict[str, str]: continue if line.startswith("#"): heading = line.lstrip("#").strip() - elif line in {"```python", "~~~python"}: + elif line.startswith(("```python", "~~~python")): fence = line[:3] source = [] diff --git a/docs/integrations/reflex.md b/docs/integrations/reflex.md index 3aae57cd..d2246363 100644 --- a/docs/integrations/reflex.md +++ b/docs/integrations/reflex.md @@ -61,7 +61,7 @@ Pass concrete columns through `data=` when they do not depend on state. The adapter compiles a content-addressed binary asset during the frontend build, so the result works with `reflex export` and needs no backend connection. -~~~python +~~~python demo exec import numpy as np import reflex as rx import reflex_xy as rxy @@ -94,7 +94,7 @@ running the data method. At runtime the computed var holds only a typed handle; numeric columns travel as binary frames over the app's existing websocket rather than through Reflex state JSON. -~~~python +~~~python demo exec from typing import TypedDict import numpy as np From de0bfad7fa83d014b9a1ab55091b0dd431713a91 Mon Sep 17 00:00:00 2001 From: Alek Date: Fri, 7 Aug 2026 14:32:03 -0700 Subject: [PATCH 4/5] style(docs): apply formatter --- docs/app/tests/test_docs_site.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index f0601cb9..0965e491 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -2788,7 +2788,9 @@ def test_other_api_owned_pages_append_focused_tables() -> None: def test_reflex_integration_renders_the_state_backed_data_example() -> None: """Keep the primary @rxy.data example visible as a live chart demo.""" - page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/integrations/reflex/") + page = next( + page for page in discover_docs(DOCS_CONFIG) if page.route == "/integrations/reflex/" + ) live_blocks = [ block for block in parse_document(page.content).blocks From 8c51d0d28c3192871372a89a25232f5d10d24f3d Mon Sep 17 00:00:00 2001 From: Alek Date: Fri, 7 Aug 2026 14:46:21 -0700 Subject: [PATCH 5/5] docs(reflex): tighten live example coverage --- docs/app/tests/test_docs_site.py | 8 +++++++- docs/integrations/reflex.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index 0965e491..4196949b 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -2800,7 +2800,13 @@ def test_reflex_integration_renders_the_state_backed_data_example() -> None: assert len(live_blocks) == 2 state_backed = next(block for block in live_blocks if "@rxy.data" in block.content) assert "rxy.scatter_chart(" in state_backed.content - assert "XYChart" in str(render_xy_markdown_page(page)) + state_rendered = str( + XyDocsMarkdownTransformer( + virtual_filepath=str(page.source_path.resolve()), + filename=str(page.source_path), + ).code_block(state_backed) + ) + assert "XYChart" in state_rendered @pytest.mark.parametrize( diff --git a/docs/integrations/reflex.md b/docs/integrations/reflex.md index d2246363..a123e466 100644 --- a/docs/integrations/reflex.md +++ b/docs/integrations/reflex.md @@ -121,7 +121,7 @@ class Dashboard(rx.State): @rx.event def record_hover(self, event: rxy.PointHoverEvent): - self.hovered = event.get("data", {}) + self.hovered = {**event.get("data", {}), **event.get("datum", {})} def index() -> rx.Component: