reflex-xy 0.0.1a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ target/
2
+ .venv/
3
+ smoke/
4
+ dist/
5
+ __pycache__/
6
+ *.pyc
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ node_modules/
10
+ *.egg-info/
11
+ .web/
12
+ **/assets/xy/*.xyf
13
+ # Generated render-client bundles (§33): built by `node js/build.mjs` and
14
+ # force-included into the wheel/sdist by hatch_build.py, never committed.
15
+ python/xy/static/index.js
16
+ python/xy/static/standalone.js
17
+ **/assets/external/
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to **reflex-xy** (the Reflex adapter for xy) are
4
+ documented here. The format follows
5
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ The adapter versions independently of the xy core: its releases are tagged
8
+ `reflex-xy-vX.Y.Z` (the core keeps bare `vX.Y.Z`), and the distribution
9
+ version is derived from that tag — there is no number to bump in a file.
10
+ Cutting a release is: date the entry below, tag, push the tag. The release
11
+ gate (`scripts/check_release_version.py --package reflex-xy`) blocks the
12
+ publish unless this file carries a dated entry for the tagged version.
13
+ Pre-releases work the same way with a canonical PEP 440 suffix on the tag
14
+ (`reflex-xy-v0.0.1a1` → version `0.0.1a1`, likewise `bN`/`rcN`) and their own
15
+ dated entry here; pip only installs them on explicit request.
16
+
17
+ ## [0.0.1a1] - 2026-07-24
18
+
19
+ ## [0.0.1a1] — 2026-07-25
20
+
21
+ ### Added
22
+ - First packaged release line of the adapter: `reflex_xy.chart()` components,
23
+ `@reflex_xy.figure` state vars with rebuild-from-state recovery,
24
+ `XYPlugin`/`setup()` app wiring, the `/_xy` socket.io data-plane namespace
25
+ (binary columns on the app's own websocket), semantic hover/click/select/
26
+ view events, fixed-data tiers (direct `Chart` + `inline()` tokens), and
27
+ streaming `append`.
28
+ - The distribution version is derived from `reflex-xy-vX.Y.Z` git tags
29
+ (uv-dynamic-versioning with `pattern-prefix = "reflex-xy-"`) instead of a
30
+ number in `pyproject.toml`; canonical pre-release tags (`aN`/`bN`/`rcN`)
31
+ publish too, and builds between tags are versioned `<next>.devN+<commit>`,
32
+ which PyPI rejects by design. The adapter and core
33
+ tag namespaces are mutually invisible by pattern anchoring: this derivation
34
+ never sees bare `v*` tags, and the core's never sees `reflex-xy-*` ones.
35
+ Releases publish via the dedicated
36
+ `.github/workflows/release-reflex-xy.yml` workflow — deliberately separate
37
+ from the core's cross-compile pipeline, since the adapter is one pure wheel
38
+ plus one sdist with no build matrix — verified by
39
+ `scripts/verify_reflex_xy_dist.py`, gated on this changelog, published with
40
+ PyPI trusted publishing, and validated alongside the other workflows by
41
+ `make check-ci`. `reflex_xy.__version__` now reports the installed
42
+ distribution's version (an uninstalled tree reports `0.0.0`).
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflex-xy
3
+ Version: 0.0.1a1
4
+ Summary: Reflex integration for xy: WebGL charts over the app's own websocket
5
+ Project-URL: Repository, https://github.com/reflex-dev/xy
6
+ Project-URL: Changelog, https://github.com/reflex-dev/xy/blob/main/python/reflex-xy/CHANGELOG.md
7
+ License: Apache-2.0
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: reflex>=0.9.6
10
+ Requires-Dist: xy
11
+ Provides-Extra: dev
12
+ Requires-Dist: aiohttp>=3.9; extra == 'dev'
13
+ Requires-Dist: uvicorn>=0.23; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # reflex-xy
17
+
18
+ [xy](https://github.com/reflex-dev/xy) figures as first-class
19
+ [Reflex](https://reflex.dev) components: WebGL rendering, million-point
20
+ interactivity, and streaming updates — with chart data riding the app's
21
+ **existing websocket**, not a sidecar API.
22
+
23
+ Status: **prototype** implementing `spec/design/reflex-integration.md`.
24
+
25
+ ## How it works
26
+
27
+ - **Control plane (Reflex-native).** The only chart state is a token string,
28
+ minted by a `@reflex_xy.figure` computed var. Semantic events —
29
+ `on_point_hover(event)`, `on_point_click(event)`, `on_select_end(event)`, and
30
+ `on_view_change(event)` — arrive as ordinary Reflex
31
+ event handlers with small JSON payloads.
32
+ - **Data plane (xy-native).** A second socket.io namespace (`/_xy`)
33
+ multiplexed onto the app's own engine.io websocket ships the spec as JSON
34
+ and every data column as a binary frame (no JSON numbers, no base64, no
35
+ extra endpoints to reverse-proxy). Pan/zoom/hover round-trips go straight
36
+ to the figure kernel and never touch Reflex state.
37
+ - **No figure server.** Figures live in a per-process registry as
38
+ *rebuildable caches*: the token encodes `(client, state, var)`, so any
39
+ backend worker can re-run the builder against Reflex state (already
40
+ distributed via redis in prod) when a reconnect lands on it.
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ # rxconfig.py
46
+ import reflex as rx
47
+ import reflex_xy
48
+
49
+ config = rx.Config(app_name="dash", plugins=[reflex_xy.XYPlugin()])
50
+ ```
51
+
52
+ ```python
53
+ # dash/dash.py
54
+ import numpy as np
55
+ import reflex as rx
56
+ import xy
57
+ import reflex_xy
58
+
59
+
60
+ class Dash(rx.State):
61
+ points: int = 200_000
62
+ hovered: dict = {}
63
+
64
+ @reflex_xy.figure
65
+ def chart(self) -> xy.Chart:
66
+ rng = np.random.default_rng(7)
67
+ xs = rng.normal(size=self.points)
68
+ ys = xs * 0.6 + rng.normal(scale=0.6, size=self.points)
69
+ return xy.scatter_chart(xy.scatter(xs, ys), width="100%", height=460)
70
+
71
+ @rx.event
72
+ def on_hover(self, row: dict):
73
+ self.hovered = row
74
+
75
+
76
+ def index() -> rx.Component:
77
+ return rx.vstack(
78
+ reflex_xy.chart(Dash.chart, on_point_hover=Dash.on_hover, height="460px"),
79
+ rx.text(Dash.hovered.to_string()),
80
+ width="100%",
81
+ )
82
+
83
+
84
+ app = rx.App()
85
+ ```
86
+
87
+ Change `points` in an event handler and the chart re-publishes itself to
88
+ every subscriber — the token never changes, so nothing re-renders except
89
+ pixels.
90
+
91
+ Builders can be `async def` (they become reflex `AsyncComputedVar`s, same
92
+ rule as `rx.var`) — await a database, HTTP endpoint, or dataframe store:
93
+
94
+ ```python
95
+ @reflex_xy.figure
96
+ async def remote(self) -> xy.Chart:
97
+ rows = await fetch_rows(self.query)
98
+ return xy.line_chart(xy.line(rows.t, rows.value))
99
+ ```
100
+
101
+ Streaming: `reflex_xy.append(token, x=[...], y=[...])` from any handler or
102
+ background task pushes an incremental update over the same socket.
103
+
104
+ ## Events and cross-filtering
105
+
106
+ Live token-backed charts emit versioned, bounded dictionaries with a stable
107
+ token and canonical row IDs. Selection rows can update ordinary State, which
108
+ causes dependent `@reflex_xy.figure` builders to republish without changing
109
+ their tokens or remounting the chart:
110
+
111
+ ```python
112
+ class Dash(rx.State):
113
+ selected_groups: list[str] = []
114
+
115
+ @rx.event
116
+ def filter_groups(self, event: dict):
117
+ selection = event["selection"]
118
+ self.selected_groups = [] if selection["cleared"] else sorted({
119
+ row["color_category"] for row in selection["rows"]
120
+ })
121
+
122
+ def index():
123
+ return rx.grid(
124
+ reflex_xy.chart(Dash.groups, on_select_end=Dash.filter_groups),
125
+ reflex_xy.chart(Dash.filtered_detail),
126
+ )
127
+ ```
128
+
129
+ Selection JSON is capped and reports `total_count` plus `truncated`; call
130
+ `reflex_xy.resolve_selection(event)` in the handler when all canonical rows
131
+ are required. Hover and view changes are latest-wins throttled (view changes
132
+ stream live during a gesture and always end on a `final`-phase event), and
133
+ view/selection state survives dependent republishes without feedback events.
134
+ The complete envelopes, limits, clear/shared-handler/viewport examples, and
135
+ static-versus-live availability are documented in
136
+ `docs/engineering/design/reflex-integration.md`.
137
+
138
+ ## Fixed-data charts
139
+
140
+ For a chart that doesn't depend on state, skip the state var entirely —
141
+ pass the Chart straight in:
142
+
143
+ ```python
144
+ def index() -> rx.Component:
145
+ return reflex_xy.chart(
146
+ xy.line_chart(xy.line(t, np.sin(t)), width="100%", height=220),
147
+ height="220px",
148
+ )
149
+ ```
150
+
151
+ That compiles the figure to a content-addressed binary asset at page build
152
+ and renders it with **zero backend involvement** — client-side hover,
153
+ pan/zoom, and density re-bin, same as `Figure.to_html()` exports; works
154
+ under `reflex export`. When a fixed chart still needs kernel round-trips
155
+ (deep drilldown into millions of points), register it once at module scope
156
+ instead:
157
+
158
+ ```python
159
+ cloud = reflex_xy.inline(xy.scatter_chart(xy.scatter(x, y))) # module scope
160
+
161
+ def index() -> rx.Component:
162
+ return reflex_xy.chart(cloud, height="460px")
163
+ ```
164
+
165
+ `inline()` tokens are content-addressed, so every backend worker derives
166
+ the same one — no state, no coordination. The escalation path is:
167
+ direct Chart (static) → `inline()` (fixed data, live kernel) →
168
+ `@reflex_xy.figure` (per-session, state-driven).
169
+
170
+ ## Demo
171
+
172
+ The repository's [`examples/reflex/`](../../examples/reflex) is a runnable
173
+ showcase of every linking method: a drillable figure var with hover / click /
174
+ box-select events, a histogram driven by a slider and cross-filtered by the
175
+ selection, a streaming line, a detail chart recomputed from `on_view_change`,
176
+ and the two fixed-data tiers (a direct `xy.Chart` and an `inline()` token).
177
+ Each section shows its own source, read live with `inspect.getsource`.
178
+
179
+ For serving the same charts without Reflex, [`examples/fastapi/`](../../examples/fastapi)
180
+ renders standalone HTML and a live 100M-point drilldown from a plain FastAPI app.
181
+
182
+ ## Versioning & releases
183
+
184
+ `reflex-xy` versions independently of `xy`: the distribution version is
185
+ derived from `reflex-xy-vX.Y.Z` git tags (the core uses bare `vX.Y.Z`), and
186
+ releases publish from the repository's `release-reflex-xy.yml` workflow after
187
+ a dated entry lands in this package's [CHANGELOG](CHANGELOG.md). Builds
188
+ between tags carry a `.devN+<commit>` version that PyPI rejects by design.
@@ -0,0 +1,173 @@
1
+ # reflex-xy
2
+
3
+ [xy](https://github.com/reflex-dev/xy) figures as first-class
4
+ [Reflex](https://reflex.dev) components: WebGL rendering, million-point
5
+ interactivity, and streaming updates — with chart data riding the app's
6
+ **existing websocket**, not a sidecar API.
7
+
8
+ Status: **prototype** implementing `spec/design/reflex-integration.md`.
9
+
10
+ ## How it works
11
+
12
+ - **Control plane (Reflex-native).** The only chart state is a token string,
13
+ minted by a `@reflex_xy.figure` computed var. Semantic events —
14
+ `on_point_hover(event)`, `on_point_click(event)`, `on_select_end(event)`, and
15
+ `on_view_change(event)` — arrive as ordinary Reflex
16
+ event handlers with small JSON payloads.
17
+ - **Data plane (xy-native).** A second socket.io namespace (`/_xy`)
18
+ multiplexed onto the app's own engine.io websocket ships the spec as JSON
19
+ and every data column as a binary frame (no JSON numbers, no base64, no
20
+ extra endpoints to reverse-proxy). Pan/zoom/hover round-trips go straight
21
+ to the figure kernel and never touch Reflex state.
22
+ - **No figure server.** Figures live in a per-process registry as
23
+ *rebuildable caches*: the token encodes `(client, state, var)`, so any
24
+ backend worker can re-run the builder against Reflex state (already
25
+ distributed via redis in prod) when a reconnect lands on it.
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ # rxconfig.py
31
+ import reflex as rx
32
+ import reflex_xy
33
+
34
+ config = rx.Config(app_name="dash", plugins=[reflex_xy.XYPlugin()])
35
+ ```
36
+
37
+ ```python
38
+ # dash/dash.py
39
+ import numpy as np
40
+ import reflex as rx
41
+ import xy
42
+ import reflex_xy
43
+
44
+
45
+ class Dash(rx.State):
46
+ points: int = 200_000
47
+ hovered: dict = {}
48
+
49
+ @reflex_xy.figure
50
+ def chart(self) -> xy.Chart:
51
+ rng = np.random.default_rng(7)
52
+ xs = rng.normal(size=self.points)
53
+ ys = xs * 0.6 + rng.normal(scale=0.6, size=self.points)
54
+ return xy.scatter_chart(xy.scatter(xs, ys), width="100%", height=460)
55
+
56
+ @rx.event
57
+ def on_hover(self, row: dict):
58
+ self.hovered = row
59
+
60
+
61
+ def index() -> rx.Component:
62
+ return rx.vstack(
63
+ reflex_xy.chart(Dash.chart, on_point_hover=Dash.on_hover, height="460px"),
64
+ rx.text(Dash.hovered.to_string()),
65
+ width="100%",
66
+ )
67
+
68
+
69
+ app = rx.App()
70
+ ```
71
+
72
+ Change `points` in an event handler and the chart re-publishes itself to
73
+ every subscriber — the token never changes, so nothing re-renders except
74
+ pixels.
75
+
76
+ Builders can be `async def` (they become reflex `AsyncComputedVar`s, same
77
+ rule as `rx.var`) — await a database, HTTP endpoint, or dataframe store:
78
+
79
+ ```python
80
+ @reflex_xy.figure
81
+ async def remote(self) -> xy.Chart:
82
+ rows = await fetch_rows(self.query)
83
+ return xy.line_chart(xy.line(rows.t, rows.value))
84
+ ```
85
+
86
+ Streaming: `reflex_xy.append(token, x=[...], y=[...])` from any handler or
87
+ background task pushes an incremental update over the same socket.
88
+
89
+ ## Events and cross-filtering
90
+
91
+ Live token-backed charts emit versioned, bounded dictionaries with a stable
92
+ token and canonical row IDs. Selection rows can update ordinary State, which
93
+ causes dependent `@reflex_xy.figure` builders to republish without changing
94
+ their tokens or remounting the chart:
95
+
96
+ ```python
97
+ class Dash(rx.State):
98
+ selected_groups: list[str] = []
99
+
100
+ @rx.event
101
+ def filter_groups(self, event: dict):
102
+ selection = event["selection"]
103
+ self.selected_groups = [] if selection["cleared"] else sorted({
104
+ row["color_category"] for row in selection["rows"]
105
+ })
106
+
107
+ def index():
108
+ return rx.grid(
109
+ reflex_xy.chart(Dash.groups, on_select_end=Dash.filter_groups),
110
+ reflex_xy.chart(Dash.filtered_detail),
111
+ )
112
+ ```
113
+
114
+ Selection JSON is capped and reports `total_count` plus `truncated`; call
115
+ `reflex_xy.resolve_selection(event)` in the handler when all canonical rows
116
+ are required. Hover and view changes are latest-wins throttled (view changes
117
+ stream live during a gesture and always end on a `final`-phase event), and
118
+ view/selection state survives dependent republishes without feedback events.
119
+ The complete envelopes, limits, clear/shared-handler/viewport examples, and
120
+ static-versus-live availability are documented in
121
+ `docs/engineering/design/reflex-integration.md`.
122
+
123
+ ## Fixed-data charts
124
+
125
+ For a chart that doesn't depend on state, skip the state var entirely —
126
+ pass the Chart straight in:
127
+
128
+ ```python
129
+ def index() -> rx.Component:
130
+ return reflex_xy.chart(
131
+ xy.line_chart(xy.line(t, np.sin(t)), width="100%", height=220),
132
+ height="220px",
133
+ )
134
+ ```
135
+
136
+ That compiles the figure to a content-addressed binary asset at page build
137
+ and renders it with **zero backend involvement** — client-side hover,
138
+ pan/zoom, and density re-bin, same as `Figure.to_html()` exports; works
139
+ under `reflex export`. When a fixed chart still needs kernel round-trips
140
+ (deep drilldown into millions of points), register it once at module scope
141
+ instead:
142
+
143
+ ```python
144
+ cloud = reflex_xy.inline(xy.scatter_chart(xy.scatter(x, y))) # module scope
145
+
146
+ def index() -> rx.Component:
147
+ return reflex_xy.chart(cloud, height="460px")
148
+ ```
149
+
150
+ `inline()` tokens are content-addressed, so every backend worker derives
151
+ the same one — no state, no coordination. The escalation path is:
152
+ direct Chart (static) → `inline()` (fixed data, live kernel) →
153
+ `@reflex_xy.figure` (per-session, state-driven).
154
+
155
+ ## Demo
156
+
157
+ The repository's [`examples/reflex/`](../../examples/reflex) is a runnable
158
+ showcase of every linking method: a drillable figure var with hover / click /
159
+ box-select events, a histogram driven by a slider and cross-filtered by the
160
+ selection, a streaming line, a detail chart recomputed from `on_view_change`,
161
+ and the two fixed-data tiers (a direct `xy.Chart` and an `inline()` token).
162
+ Each section shows its own source, read live with `inspect.getsource`.
163
+
164
+ For serving the same charts without Reflex, [`examples/fastapi/`](../../examples/fastapi)
165
+ renders standalone HTML and a live 100M-point drilldown from a plain FastAPI app.
166
+
167
+ ## Versioning & releases
168
+
169
+ `reflex-xy` versions independently of `xy`: the distribution version is
170
+ derived from `reflex-xy-vX.Y.Z` git tags (the core uses bare `vX.Y.Z`), and
171
+ releases publish from the repository's `release-reflex-xy.yml` workflow after
172
+ a dated entry lands in this package's [CHANGELOG](CHANGELOG.md). Builds
173
+ between tags carry a `.devN+<commit>` version that PyPI rejects by design.
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["hatchling", "uv-dynamic-versioning>=0.11"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "reflex-xy"
7
+ dynamic = ["version"]
8
+ description = "Reflex integration for xy: WebGL charts over the app's own websocket"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "Apache-2.0" }
12
+ dependencies = [
13
+ "xy",
14
+ # Prototype dependency surface. The adapter needs: Component/Var/EventHandler
15
+ # (reflex-base), plus App/State/state_manager access, which today live in the
16
+ # full `reflex` distribution. Revisit when the reflex-base split grows a
17
+ # public state/app surface (spec/design/reflex-integration.md §4).
18
+ "reflex>=0.9.6",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ # tests/reflex_adapter drives the data plane over a real websocket:
23
+ # uvicorn serves the socket app, aiohttp backs socketio.AsyncClient.
24
+ dev = [
25
+ "aiohttp>=3.9",
26
+ "uvicorn>=0.23",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/reflex-dev/xy"
31
+ Changelog = "https://github.com/reflex-dev/xy/blob/main/python/reflex-xy/CHANGELOG.md"
32
+
33
+ # Same tag-derived versioning policy as the xy core (see the root
34
+ # pyproject.toml for the full rationale), in a tag namespace of its own:
35
+ # the adapter releases from `reflex-xy-vX.Y.Z` tags, cut by
36
+ # .github/workflows/release-reflex-xy.yml. `pattern-prefix` inserts
37
+ # `reflex-xy-` after the default pattern's `^` anchor, which keeps the two
38
+ # release lines disjoint in both directions — this derivation only sees
39
+ # `reflex-xy-v*` tags, while the core's bare-`v` pattern (and the docs-deploy
40
+ # CalVer tags) can never match them.
41
+ [tool.hatch.version]
42
+ source = "uv-dynamic-versioning"
43
+
44
+ [tool.uv-dynamic-versioning]
45
+ vcs = "git"
46
+ style = "pep440"
47
+ pattern-prefix = "reflex-xy-"
48
+ # Builds off a tagged commit get the bare version; anything else gets
49
+ # `<next>.devN+<commit>`, unpublishable to PyPI on purpose — only a tag can
50
+ # produce an uploadable version.
51
+ bump = true
52
+ # Only reached by a source tree with neither git metadata nor PKG-INFO; such
53
+ # a build still succeeds, at an obviously unreal version rather than a
54
+ # plausible-but-wrong one (unpacked sdists resolve from their own PKG-INFO).
55
+ fallback-version = "0.0.0"
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["reflex_xy"]
59
+ # reflex_xy/assets/XYChart.jsx ships as ordinary package data; the render
60
+ # client is NOT packaged — it links out of the installed xy wheel at
61
+ # app compile time (see reflex_xy/assets/__init__.py), so it can never
62
+ # drift from the kernel that produces its payloads.
@@ -0,0 +1,166 @@
1
+ """reflex-xy: xy figures as first-class Reflex components.
2
+
3
+ The integration in one paragraph (full design:
4
+ spec/design/reflex-integration.md in the xy repo): chart data rides
5
+ the app's *existing* websocket as a second socket.io namespace — binary
6
+ columns, no JSON numbers, no extra endpoints to proxy. Figures live in a
7
+ per-process registry keyed by tokens; the tokens live in Reflex state. A
8
+ `@reflex_xy.figure` state method is both the chart definition and the
9
+ recovery recipe: any worker can rebuild the figure from state when a
10
+ reconnect lands somewhere new, so there is no central figure store to
11
+ operate.
12
+
13
+ Quickstart::
14
+
15
+ # rxconfig.py
16
+ config = rx.Config(app_name="dash", plugins=[reflex_xy.XYPlugin()])
17
+
18
+ # dash/dash.py
19
+ import numpy as np
20
+ import reflex as rx
21
+ import xy
22
+ import reflex_xy
23
+
24
+ class Dash(rx.State):
25
+ points: int = 200_000
26
+
27
+ @reflex_xy.figure
28
+ def chart(self) -> xy.Chart:
29
+ rng = np.random.default_rng(7)
30
+ xs = rng.normal(size=self.points)
31
+ ys = xs * 0.6 + rng.normal(scale=0.6, size=self.points)
32
+ return xy.scatter_chart(xy.scatter(xs, ys), width="100%", height=460)
33
+
34
+ def index() -> rx.Component:
35
+ return reflex_xy.chart(Dash.chart, height="460px")
36
+
37
+ app = rx.App()
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import hashlib
43
+ import json
44
+ from typing import Any
45
+
46
+ from .app import XYPlugin, append, clear_selection, reset_view, select, set_view, setup
47
+ from .component import chart
48
+ from .events import (
49
+ CanonicalRowIdGroup,
50
+ DataBounds,
51
+ Modifiers,
52
+ PointClickEvent,
53
+ PointData,
54
+ PointHoverEvent,
55
+ ScreenPoint,
56
+ SelectEndEvent,
57
+ SelectionPayload,
58
+ ViewChangeEvent,
59
+ )
60
+ from .namespace import XY_NAMESPACE, XYNamespace
61
+ from .registry import FigureRegistry, _figure_of, registry
62
+ from .selections import resolve_selection
63
+ from .vars import AsyncFigureVar, FigureVar, figure
64
+
65
+ __all__ = [
66
+ "XY_NAMESPACE",
67
+ "AsyncFigureVar",
68
+ "CanonicalRowIdGroup",
69
+ "DataBounds",
70
+ "FigureRegistry",
71
+ "FigureVar",
72
+ "Modifiers",
73
+ "PointClickEvent",
74
+ "PointData",
75
+ "PointHoverEvent",
76
+ "ScreenPoint",
77
+ "SelectEndEvent",
78
+ "SelectionPayload",
79
+ "ViewChangeEvent",
80
+ "XYNamespace",
81
+ "XYPlugin",
82
+ "append",
83
+ "chart",
84
+ "clear_selection",
85
+ "figure",
86
+ "inline",
87
+ "register",
88
+ "registry",
89
+ "release",
90
+ "reset_view",
91
+ "resolve_selection",
92
+ "select",
93
+ "set_view",
94
+ "setup",
95
+ ]
96
+
97
+
98
+ def __getattr__(name: str) -> str:
99
+ """Resolve ``__version__`` lazily from the installed distribution.
100
+
101
+ The version is not written down in the source tree — it is derived from
102
+ the latest ``reflex-xy-vX.Y.Z`` git tag at build time (pyproject's
103
+ uv-dynamic-versioning config) and baked into the wheel's METADATA, so
104
+ package metadata is the only place that can answer this at runtime. An
105
+ uninstalled source tree reports the same unreal ``0.0.0`` the build-time
106
+ fallback uses.
107
+ """
108
+ if name == "__version__":
109
+ from importlib.metadata import PackageNotFoundError
110
+ from importlib.metadata import version as _distribution_version
111
+
112
+ try:
113
+ value = _distribution_version("reflex-xy")
114
+ except PackageNotFoundError:
115
+ value = "0.0.0"
116
+ globals()["__version__"] = value
117
+ return value
118
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
119
+
120
+
121
+ def register(chart_or_figure: Any) -> str:
122
+ """Imperatively register a chart; returns an opaque token for state.
123
+
124
+ Dev-tier API: the figure lives only in this process and cannot be
125
+ rebuilt after a worker restart or on another node — prefer
126
+ `@reflex_xy.figure` for anything long-lived (see the module doc).
127
+ """
128
+ return registry.register(_figure_of(chart_or_figure))
129
+
130
+
131
+ def inline(chart_or_figure: Any) -> str:
132
+ """Register a fixed, kernel-backed chart at module scope; returns its token.
133
+
134
+ For charts whose data never changes but which still want server-side
135
+ drilldown/picks on the shared websocket. Call at **module scope** so the
136
+ registration side effect runs in every backend worker (page bodies only
137
+ run where the frontend compiles)::
138
+
139
+ cloud = reflex_xy.inline(xy.scatter_chart(xy.scatter(x, y)))
140
+
141
+ def index():
142
+ return reflex_xy.chart(cloud, height="460px")
143
+
144
+ The token is content-addressed — every worker independently derives the
145
+ same one, so the frontend's baked-in token resolves everywhere without
146
+ state or rebuild hooks. The entry is pinned (exempt from the TTL sweep):
147
+ there is no recipe to rebuild it from, so it lives with the process.
148
+
149
+ Shared by design: one figure object serves every viewer, so kernel-side
150
+ drill state is shared too (like N notebook views of one widget). Data
151
+ depending on who's looking belongs in `@reflex_xy.figure`; data needing
152
+ no kernel at all can be passed straight to `reflex_xy.chart()` (static
153
+ payload tier).
154
+ """
155
+ fig = _figure_of(chart_or_figure)
156
+ spec, blob = fig.build_payload()
157
+ canonical = json.dumps(spec, sort_keys=True, separators=(",", ":")).encode()
158
+ digest = hashlib.sha256(canonical + blob).hexdigest()[:20]
159
+ token = f"xyin-{digest}"
160
+ registry.publish(token, fig, broadcast=False, pinned=True)
161
+ return token
162
+
163
+
164
+ def release(token: str) -> None:
165
+ """Drop a registered figure (idempotent)."""
166
+ registry.release(token)