Skip to main content

reflex-perspective

A Reflex custom component for Perspective — the WebAssembly analytics engine and interactive viewer (datagrid + WebGL charts) built for large and streaming datasets.

Explorer

  • All three Perspective data architectures
    • Client-only: data from Reflex state, a URL (CSV / JSON / NDJSON / Arrow) or a schema lives in a browser WebWorker.
    • Server-only (virtual): tables live in perspective-python inside the Reflex backend; only the visible cells cross the wire.
    • Client/server replicated: the browser keeps a live copy of a server table and computes views locally.
  • Declarative props for the whole ViewerConfig (plugin, group_by, split_by, columns, filter, sort, expressions, aggregates, columns_config, theme, settings, ...) mapped to restore().
  • Multi-panel workspaces (restoreWorkspace) with master panels and global cross-filters.
  • Streaming through props (update_rows, remove_keys) or straight from event handlers (rp.update).
  • Perspective Custom Events as Reflex events: on_click, on_select, on_config_update, on_global_filter_update, on_layout_update, on_load, on_error, ...
  • Imperative API from Python: rp.save, rp.restore, rp.download, rp.copy, rp.export, rp.reset, rp.toggle_config, rp.update, rp.remove, rp.clear, rp.add_panel, ...
  • All 16 bundled themes, every datagrid and chart plugin (X/Y Bar, Line, Area, Scatter, Density, Treemap, Sunburst, Heatmap, Candlestick, OHLC, Map Scatter/Line/Density).
  • SSR-safe lazy WASM bootstrap, automatic WebSocket reconnection, works in reflex run dev and --env prod.

Built against Reflex 0.10.0 and Perspective 5.5.1 (@perspective-dev/* npm packages and perspective-python).

Installation

pip install reflex-perspective            # client-only mode
pip install "reflex-perspective[server]"  # + perspective-python for server modes

The npm packages are installed by Reflex automatically on the first reflex run. Requires Python 3.11+ (Reflex 0.10 dropped 3.10; perspective-python 5.x only publishes cp311-abi3 wheels).

Quick start (client-only)

import reflex as rx
import reflex_perspective as rp


class State(rx.State):
    rows: list[dict] = [
        {"region": "East", "product": "A", "sales": 120.5},
        {"region": "West", "product": "B", "sales": 98.1},
    ]
    plugin: str = "Y Bar"

    @rx.event
    def clicked(self, detail: dict):
        print(detail["row"], detail["config"]["filter"])


def index():
    return rp.perspective_viewer(
        id="sales",
        data=State.rows,  # list of rows, dict of columns, or CSV text
        plugin=State.plugin,
        group_by=["region"],
        split_by=["product"],
        columns=["sales"],
        theme="Pro Dark",
        on_click=State.clicked,
        height="500px",
    )


app = rx.App()
app.add_page(index)

Changing any prop re-applies only the difference with restore(); changing data calls table.replace() and keeps the user's view.

Server-hosted tables (perspective-python)

import reflex as rx
import reflex_perspective as rp
from reflex_perspective import server as ps

hub = ps.get_hub()  # a perspective.Server + local client
hub.table(big_dataframe, name="orders", index="order_id")

app = rx.App(api_transformer=ps.perspective_api())  # WebSocket at /perspective
# or: ps.mount(app)  (keeps any existing api_transformer)


def page():
    return rx.hstack(
        # virtual: pivots/sorts run in Python, only the viewport is sent
        rp.perspective_viewer(server_url="/perspective", server_table="orders"),
        # replicated: the browser keeps a synced copy and computes locally
        rp.perspective_viewer(
            server_url="/perspective", server_table="orders", server_mode="replicated"
        ),
    )


class State(rx.State):
    @rx.event
    def add_order(self):
        hub.update("orders", [{"order_id": 42, "qty": 3}])  # every viewer updates

server_url may be a path (resolved against the Reflex backend URL, which also handles the dev setup where frontend and backend run on different ports) or an absolute ws:// URL of any Perspective server. Feed tables from background tasks, lifespan tasks (ps.run_periodically(fn, interval)) or plain threads — the hub is thread-safe and the WebSocket handler marshals messages back onto the event loop. Engine requests run in the event loop's default thread pool (pass executor= to perspective_api() / mount() to use your own), so a heavy pivot on a large table never blocks other Reflex events.

Access control

By default (as in 0.1.0) the endpoint only checks the browser Origin header against Reflex's cors_allowed_origins (or allowed_origins=[...]), closing foreign connections with code 1008, and any client that reaches it can read and modify every hosted table. For any deployment with more than one user, set explicit origins and use both options below (keyword-only on perspective_api(), mount(), asgi_app() and serve()):

async def authorize_panel(websocket):  # sync or async
    user = await user_from_cookie(websocket.cookies.get("session"))
    if user is None:
        return 4401  # refuse: close code 4000-4999 (or 1008)
    if not user.can_view_analytics:
        return 4403
    return None  # accept


ps.mount(
    app,
    path="/analytics/ws",
    allowed_origins=["https://app.example.com"],
    authorize=authorize_panel,
    read_only=True,
)
  • authorize= runs after the Origin check and before any Perspective session exists. A refusal accepts the socket and closes it at once with your code, so the browser sees it (closing before the handshake would turn it into an HTTP 403 and the browser would only see 1006). A hook that raises, or returns anything other than None, 4000–4999 or 1008, closes with 1011 and is logged.
  • read_only=True forwards only frames carrying exactly one known read request of the pinned Perspective version (ps.READ_VARIANTS); writes (update, remove, replace/clear, table delete, new tables, joins), unknown variants and malformed or tampered frames close the socket with 4409 (write_close_code= to change it) and are logged by variant number and name, never by content. Writes from Python (hub.update(), lifespan tasks) keep reaching every viewer. On a perspective-python version without a verified read table, building the app raises RuntimeError unless you pass read_variants=.
  • With read_only=True, a server viewer with edit_mode="EDIT", update_rows, remove_keys or rp.update/remove/replace/clear gets its socket closed with 4409; the viewers stop reconnecting and the browser console says why. Write through the hub instead. Read-only does not stop users from exporting what they can see (view_to_* reads stay allowed).
  • max_sessions=N caps the sessions open on the hub (across all its routes). Once reached, new sockets are accepted and closed with 4429 after authorize; the viewers keep retrying with backoff and connect when a slot frees up. hub.session_count gives the current number.
  • on_reject=fn is called as fn(code, websocket) (sync or async) before every refusal (1008, authorize's code, 1011, 4409 and 4429), for metrics. Its errors are logged and never change the close.
  • ps.request_variant() and ps.is_read_request() are public, for apps that compose their own handler around hub.serve().

PerspectiveHub helpers: table(), get_table(), has_table(), table_names(), update(), remove(), clear(), size(), query(name, **view_config) and delete_table().

Streaming

rp.perspective_viewer(
    id="ticks",
    schema={"time": "datetime", "symbol": "string", "price": "float"},
    limit=5000,  # rolling window
    update_rows=State.batch,  # every new list is applied with table.update()
    plugin="Y Line",
    group_by=["time"],
    split_by=["symbol"],
    columns=["price"],
)


# or push rows without storing them in state:
@rx.event
def burst(self):
    return rp.update("ticks", rows)

Workspaces

rp.perspective_viewer(
    data=State.rows,
    workspace={
        "masters": ["regions"],  # selections here become global filters
        "layout": {
            "type": "split-layout",
            "orientation": "horizontal",
            "sizes": [0.4, 0.6],
            "children": [
                {"type": "tab-layout", "tabs": ["regions"]},
                {"type": "tab-layout", "tabs": ["chart"]},
            ],
        },
        "panels": {
            "regions": {
                "plugin": "Datagrid",
                "group_by": ["Region"],
                "columns": ["Sales"],
            },
            "chart": {
                "plugin": "Treemap",
                "group_by": ["Category"],
                "columns": ["Sales"],
            },
        },
    },
    on_global_filter_update=State.set_filters,
)

Panels without a table in the workspace prop are bound to the viewer's own table automatically. rp.restore_workspace() and rp.add_panel() pass their config through unchanged, so include table there.

Props

Prop Description
data Rows (list[dict]), columns (dict[str, list]) or CSV text.
schema {"col": "string" | "integer" | "float" | "boolean" | "date" | "datetime"}; enforces types.
index / limit Primary key (upserts) or rolling row limit.
table_name Table name (defaults to the component id); saved configs reference it.
url, url_format Load CSV / JSON / NDJSON / Arrow from a URL.
server_url, server_table, server_mode Connect to a Perspective WebSocket server ("server" or "replicated").
update_rows, remove_keys Streaming: applied every time the prop changes.
config Full ViewerConfigUpdate; the shortcut props below are merged on top.
plugin, columns, group_by, split_by, filter, filter_op, sort, expressions, aggregates, group_by_depth, group_rollup_mode, split_rollup_mode, plugin_config, columns_config, theme, title, settings View configuration.
edit_mode Datagrid mode: READ_ONLY, EDIT, SELECT_ROW, SELECT_COLUMN, SELECT_REGION, SELECT_ROW_TREE.
workspace Multi-panel WorkspaceConfigUpdate.
auto_size, auto_pause, throttle, plugin_limits Render policy.

Styling props (height, width, border_radius, ...) apply to the <perspective-viewer> element (default height 600px).

Events

Event Payload
on_load {"table", "schema", "num_rows", "source"}
on_config_update Full saved viewer config after any change
on_click {"row", "column_names", "config", "panel"} (config.filter selects the clicked datum)
on_select {"selected", "row", "column_names", "insert_filters", "remove_filters", "panel"}
on_global_filter_update List of filters (workspaces)
on_layout_update / on_active_panel_update Panel ids / active panel id
on_toggle_settings bool
on_disconnect (url, code): the server URL and the WebSocket close code (None if unknown); one-argument handlers get the URL
on_error Message

A controlled loop (config=State.cfg, on_config_update=State.set_cfg) is safe: the component does not re-apply a config the viewer itself just emitted.

Server viewers reconnect with exponential backoff (0.5 s doubling, up to 10 s) after a lost connection, except when the server refused it for good: a close code in 4400–4499 other than 4429 (e.g. 4401/4403 from authorize, 4409 from read_only). Then every viewer of that URL stops retrying until the page is reloaded and the browser console explains the code; 4429 ("too many sessions") keeps retrying.

Actions

All return an EventSpec, usable in triggers (on_click=rp.download("v")) or returned from handlers. Arguments may be plain values or state vars. Result-returning actions take a callback event handler.

save(id, callback), restore(id, config), save_workspace(id, callback), restore_workspace(id, workspace), reset(id, all=False), toggle_config(id, force=None), download(id, method="csv"), copy(id, method="csv"), export(id, callback, method="csv"), update(id, rows), remove(id, keys), replace(id, data), clear(id), table_size(id, callback), table_schema(id, callback), add_panel(id, config), resize(id).

export delivers text for csv, json, ndjson, html and json-config, and a base64 data: URL for the binary methods (arrow*, and plugin, which is a PNG image).

Export methods: rp.EXPORT_METHODS. Plugins: rp.PLUGINS. Themes: rp.THEMES. Aggregates: rp.AGGREGATES.

Demo app

uv venv -p 3.12 && uv pip install -e ".[server]"
cd perspective_demo
uv run reflex run
Page What it shows
/ Explorer Declarative props driven by state controls, click/select/config events
/streaming Background task streaming through update_rows, rp.update bursts, callbacks
/server perspective-python tables over a WebSocket: live market feed, 250k-row virtual table, replicated mode, Python-side queries, authorize switch and read-only mode
/workspace Multi-panel dashboards, master panels, global filters, saveWorkspace()
/api Imperative actions, named layouts, editable datagrid, mutations, event log
/gallery Every chart plugin with a different theme

Run it with PERSPECTIVE_DEMO_READ_ONLY=1 uv run reflex run to serve the WebSocket in read-only mode: the "Write from the browser" button on /server is then refused with 4409. PERSPECTIVE_DEMO_MAX_SESSIONS=1 caps the hub: a second tab gets 4429 until the first closes. The access card counts refusals by code through on_reject.

Streaming Workspace
Server Gallery

Notes

  • Keep perspective-python and the npm packages on the same version (rp.PERSPECTIVE_VERSION); the WebSocket protocol is versioned together.
  • Server tables live in the backend process. With several backend workers each worker has its own hub; use a single worker or an external Perspective server (server_url="ws://...") for shared tables.
  • Unless you pass authorize= and read_only=True (see Access control), any client that passes the Origin check can read and modify every hosted table, just like with Perspective's own handlers.
  • update_rows / rp.update write to the viewer's own table. In server mode that's the shared hosted table; in replicated mode it's only the browser replica (write through the hub to reach every viewer).
  • On the very first reflex run after installing, Vite may pre-bundle the new npm dependencies while the page loads; reload once if the viewer does not appear.

Development

uv sync --extra dev
uv run pytest                      # unit tests
uvx ruff check . && uvx ruff format --check .
uv build                           # sdist + wheel in dist/

CI runs on every push and PR to main / develop: Quality (ruff, a JSX syntax check, pytest on Python 3.11–3.13, a build + twine check with a wheel-content check, and a reflex compile of the demo) and Security (CodeQL, Bandit, pip-audit, gitleaks and dependency review).

Releasing

  1. Bump version in pyproject.toml and __version__ in custom_components/reflex_perspective/__init__.py (CI checks they match).
  2. Merge to main, then tag and push: git tag v0.1.1 && git push origin v0.1.1.
  3. The Release workflow checks the tag against the version, runs the checks again, builds, publishes to PyPI (Trusted Publishing) and creates the GitHub release with the artifacts attached.

License

Apache-2.0. Perspective is © the Perspective Authors, Apache-2.0.

Metadata

Release files for reflex-perspective 0.4.0

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-perspective 0.4.0
File Size Uploaded Provenance
reflex_perspective-0.4.0.tar.gz 55.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for reflex-perspective 0.4.0
File Interpreter ABI Platform
reflex_perspective-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 96.0 kB

Release files / reflex_perspective-0.4.0.tar.gz

Download URL reflex_perspective-0.4.0.tar.gz
Size 55.6 kB
Tags Source
SHA-256 checksum
How to use checksums
51be496251f90a6d5cd190ef4e95039c4d839c4b3eaf44382add38b927b8c7a1
BLAKE2b-256 checksum
How to use checksums
0f7fcbf309ff3e0f6e708cf8ef4a3d980021308558db8c7a6652e181e96a0e97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 25, 2026.

Transparency log

Release files / reflex_perspective-0.4.0-py3-none-any.whl

Download URL reflex_perspective-0.4.0-py3-none-any.whl
Size 40.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
673158162f0a135bc53d1a6249b6582e9bfab6490bf35a788d54f57d4d4698c4
BLAKE2b-256 checksum
How to use checksums
884ef4428a9ff99b0c8c0ff6a008fb6453ff00d8925cd0f2cca37bd5d5bc24bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 25, 2026.

Transparency log

Security for reflex-perspective 0.4.0

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_perspective-0.4.0.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 Oct 25, 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