Skip to main content

reflex-xy

xy figures as first-class Reflex components: WebGL rendering, million-point interactivity, and streaming updates — with chart data riding the app's existing websocket, not a sidecar API.

How it works

  • Control plane (Reflex-native). The only chart state is a token string, minted by a @reflex_xy.figure computed var. Semantic events — on_point_hover(event), on_point_click(event), on_select_end(event), and on_view_change(event) — arrive as ordinary Reflex event handlers with small JSON payloads.
  • Data plane (xy-native). A second socket.io namespace (/_xy) multiplexed onto the app's own engine.io websocket ships the spec as JSON and every data column as a binary frame (no JSON numbers, no base64, no extra endpoints to reverse-proxy). Pan/zoom/hover round-trips go straight to the figure kernel and never touch Reflex state.
  • No figure server. Figures live in a per-process registry as rebuildable caches: the token encodes (client, state, var), so any backend worker can re-run the builder against Reflex state (already distributed via redis in prod) when a reconnect lands on it.

Usage

# rxconfig.py
import reflex as rx
import reflex_xy

config = rx.Config(app_name="dash", plugins=[reflex_xy.XYPlugin()])
# dash/dash.py
import numpy as np
import reflex as rx
import xy
import reflex_xy


class Dash(rx.State):
    points: int = 200_000
    hovered: dict = {}

    @reflex_xy.figure
    def chart(self) -> xy.Chart:
        rng = np.random.default_rng(7)
        xs = rng.normal(size=self.points)
        ys = xs * 0.6 + rng.normal(scale=0.6, size=self.points)
        return xy.scatter_chart(xy.scatter(xs, ys), width="100%", height=460)

    @rx.event
    def on_hover(self, row: dict):
        self.hovered = row


def index() -> rx.Component:
    return rx.vstack(
        reflex_xy.chart(Dash.chart, on_point_hover=Dash.on_hover, height="460px"),
        rx.text(Dash.hovered.to_string()),
        width="100%",
    )


app = rx.App()

Tailwind classes on live charts

Add Reflex's Tailwind plugin when chart chrome uses utility classes. A direct fixed xy.Chart is scanned automatically. A token or Var is resolved only at runtime, so expose its possible complete utility names through the adapter:

# rxconfig.py
config = rx.Config(
    app_name="dash",
    plugins=[
        # Keep dark: utilities synchronized with Reflex's .dark color mode.
        rx.plugins.TailwindV4Plugin(config={"darkMode": "selector"}),
        reflex_xy.XYPlugin(),
    ],
)

# dash.py
LIVE_CHART_CLASSES = [
    "rounded-2xl border border-slate-200 dark:border-slate-800",
    "bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100",
]

reflex_xy.chart(
    Dash.chart,
    tailwind_classes=LIVE_CHART_CLASSES,
)

The inventory is present only in generated source for Tailwind's build-time scan. It never reaches the DOM; the runtime chart receives the same class names from its XY payload. Use a string or ordered iterable (not a mapping or set), and include every complete class that any state-driven version of the figure can emit. Quotes, escaped arbitrary-selector underscores, and Unicode content are preserved verbatim:

LIVE_CHART_CLASSES = [
    "before:content-['✓']",
    r"[&_[data-xy-slot=legend\_label]]:font-semibold",
]

When a recomputed live figure changes root or slot classes, the browser rebuilds its DOM chrome so the new classes take effect without changing the figure token. Every named-axis range and durable box/range/lasso geometry survives that rebuild without replaying semantic callbacks.

Change points in an event handler and the chart re-publishes itself to every subscriber — the token never changes, so nothing re-renders except pixels.

Builders can be async def (they become reflex AsyncComputedVars, same rule as rx.var) — await a database, HTTP endpoint, or dataframe store:

    @reflex_xy.figure
    async def remote(self) -> xy.Chart:
        rows = await fetch_rows(self.query)
        return xy.line_chart(xy.line(rows.t, rows.value))

Streaming: reflex_xy.append(token, x=[...], y=[...]) from any handler or background task pushes an incremental update over the same socket.

Events and cross-filtering

Live token-backed charts emit versioned, bounded dictionaries with a stable token and canonical row IDs. Selection rows can update ordinary State, which causes dependent @reflex_xy.figure builders to republish without changing their tokens or remounting the chart:

class Dash(rx.State):
    selected_groups: list[str] = []

    @rx.event
    def filter_groups(self, event: dict):
        selection = event["selection"]
        self.selected_groups = [] if selection["cleared"] else sorted({
            row["color_category"] for row in selection["rows"]
        })

def index():
    return rx.grid(
        reflex_xy.chart(Dash.groups, on_select_end=Dash.filter_groups),
        reflex_xy.chart(Dash.filtered_detail),
    )

Fixed-data charts

For a chart that doesn't depend on state, skip the state var entirely — pass the Chart straight in:

def index() -> rx.Component:
    return reflex_xy.chart(
        xy.line_chart(xy.line(t, np.sin(t)), width="100%", height=220),
        height="220px",
    )

That compiles the figure to a content-addressed binary asset at page build and renders it with zero backend involvement — client-side hover, pan/zoom, and density re-bin, same as Figure.to_html() exports; works under reflex export. When a fixed chart still needs kernel round-trips (deep drilldown into millions of points), register it once at module scope instead:

cloud = reflex_xy.inline(xy.scatter_chart(xy.scatter(x, y)))  # module scope

def index() -> rx.Component:
    return reflex_xy.chart(cloud, height="460px")

inline() tokens are content-addressed, so every backend worker derives the same one — no state, no coordination. The escalation path is: direct Chart (static) → inline() (fixed data, live kernel) → @reflex_xy.figure (per-session, state-driven).

Metadata

Release files for reflex-xy 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reflex-xy 0.0.2
File Size Uploaded Provenance Actions
reflex_xy-0.0.2.tar.gz 156.1 kB With provenance Details for reflex_xy-0.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for reflex-xy 0.0.2
File Interpreter ABI Platform Actions
reflex_xy-0.0.2-py3-none-any.whl Python 3 none any Details for reflex_xy-0.0.2-py3-none-any.whl

Total release size: 200.1 kB

Release files / reflex_xy-0.0.2.tar.gz

Download URL reflex_xy-0.0.2.tar.gz
Size 156.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fd82fb8140128052b70a1aeabb59d7b2b5a11b3de66551e99224892a42d818e5
BLAKE2b-256 checksum
How to use checksums
4a020ff0758650f4e69fc8fe58c0b123bfe08e7167a0df9845e61be456d9d5d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release files / reflex_xy-0.0.2-py3-none-any.whl

Download URL reflex_xy-0.0.2-py3-none-any.whl
Size 44.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55af2692066b2b8137cbe8699e815521fe414dbe4388dd98eddba29d4adfc4e5
BLAKE2b-256 checksum
How to use checksums
c43e95ae05b62b1fd299b139ead9f466bcfc9ccb8065f166d70ed670cca3f33f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Security for reflex-xy 0.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for reflex_xy-0.0.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page