Skip to main content

Reflex integration for xy: WebGL charts over the app's own websocket

Project description

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.

Status: prototype implementing spec/design/reflex-integration.md.

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()

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),
    )

Selection JSON is capped and reports total_count plus truncated; call reflex_xy.resolve_selection(event) in the handler when all canonical rows are required. Hover and view changes are latest-wins throttled (view changes stream live during a gesture and always end on a final-phase event), and view/selection state survives dependent republishes without feedback events. The complete envelopes, limits, clear/shared-handler/viewport examples, and static-versus-live availability are documented in docs/engineering/design/reflex-integration.md.

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).

Demo

The repository's examples/reflex/ is a runnable showcase of every linking method: a drillable figure var with hover / click / box-select events, a histogram driven by a slider and cross-filtered by the selection, a streaming line, a detail chart recomputed from on_view_change, and the two fixed-data tiers (a direct xy.Chart and an inline() token). Each section shows its own source, read live with inspect.getsource.

For serving the same charts without Reflex, examples/fastapi/ renders standalone HTML and a live 100M-point drilldown from a plain FastAPI app.

Versioning & releases

reflex-xy versions independently of xy: the distribution version is derived from reflex-xy-vX.Y.Z git tags (the core uses bare vX.Y.Z), and releases publish from the repository's release-reflex-xy.yml workflow after a dated entry lands in this package's CHANGELOG. Builds between tags carry a .devN+<commit> version that PyPI rejects by design.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reflex_xy-0.0.1a1.tar.gz (152.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reflex_xy-0.0.1a1-py3-none-any.whl (41.1 kB view details)

Uploaded Python 3

File details

Details for the file reflex_xy-0.0.1a1.tar.gz.

File metadata

  • Download URL: reflex_xy-0.0.1a1.tar.gz
  • Upload date:
  • Size: 152.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for reflex_xy-0.0.1a1.tar.gz
Algorithm Hash digest
SHA256 3f66afacedcdb156d18adba90db2adcaa60ec725af5af38201f16e32c5f8e615
MD5 fc951e71153fb3f31144496f3f0d1be4
BLAKE2b-256 68557effa36f962c059373362f772cd014462cea0c832bf083400048431e7015

See more details on using hashes here.

Provenance

The following attestation bundles were made for reflex_xy-0.0.1a1.tar.gz:

Publisher: release-reflex-xy.yml on reflex-dev/xy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file reflex_xy-0.0.1a1-py3-none-any.whl.

File metadata

  • Download URL: reflex_xy-0.0.1a1-py3-none-any.whl
  • Upload date:
  • Size: 41.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for reflex_xy-0.0.1a1-py3-none-any.whl
Algorithm Hash digest
SHA256 054e75ec954a2ea1064baec2f7657c2d1c6189aec7cc50111595b7363fafbb0f
MD5 021c90c209af0b81615a6e48e33b8d95
BLAKE2b-256 eeef13fec87d4859b3c4cb68c1a7d4a8a55ad7d25ce8f617fc3a24abd1f75e32

See more details on using hashes here.

Provenance

The following attestation bundles were made for reflex_xy-0.0.1a1-py3-none-any.whl:

Publisher: release-reflex-xy.yml on reflex-dev/xy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page