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.9.12 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. The client-only component works on Python 3.10+; the server extra needs Python 3.11+ (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.

The endpoint checks the browser Origin header against Reflex's cors_allowed_origins (or allowed_origins=[...] passed to perspective_api() / mount()), closing foreign connections with code 1008. Set explicit origins in production: the Reflex default ("*") lets any site open the socket, and hosted tables are writable over it.

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 / on_error URL / 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.

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
/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
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.
  • Apart from the Origin check, the WebSocket endpoint has no authentication, just like Perspective's own handlers. Any client that can reach it can read and modify every hosted table. Put it behind your auth (e.g. a Starlette middleware in api_transformer) before exposing it to untrusted networks.
  • 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.10–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.1.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.1.0
File Size Uploaded Provenance
reflex_perspective-0.1.0.tar.gz 38.2 kB With provenance Details

Built distribution (wheel)

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

Total release size: 72.5 kB

Release files / reflex_perspective-0.1.0.tar.gz

Download URL reflex_perspective-0.1.0.tar.gz
Size 38.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f3369237ccf3abc42a8ac8b0fb40f04981be003fe028ee372dbc367f32f61ba9
BLAKE2b-256 checksum
How to use checksums
c85c9ac26aa4790aaf9e0a77a5e9c667f5a5fb82ce1a3483f776dbb1cd3115a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 24, 2026.

Transparency log

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

Download URL reflex_perspective-0.1.0-py3-none-any.whl
Size 34.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b49d19b5fa8d04df07f93fbe0e439f443ab607370056719b4a058015318ac9ac
BLAKE2b-256 checksum
How to use checksums
d077d02194d2a65be4bdaca32a5be84bbc083b693c1966e13934df8c6416c6f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 24, 2026.

Transparency log

Security for reflex-perspective 0.1.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.1.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 Sep 24, 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