FlexViz: the next step beyond plotly-resampler

Three walls we hit with plotly-resampler, and the query engine we built next: a stateless server, lazy Polars, and every view as a URL.

plotly-resampler keeps a Plotly line chart responsive over millions of points. It does that by aggregating the visible range instead of drawing every sample. We wrote it, we maintain it, and it taught us something we did not set out to learn. The hard part of large-data visualization is not the drawing. It is deciding what to compute, and when.

FlexViz is what that lesson turned into. It is an open-source Python engine for linked dashboards over large tabular data, including Parquet sources that do not fit in memory. This post is about the three walls we hit, what carried over, and what had to be rebuilt. It ends with an honest answer about which of the two you should use.

What plotly-resampler proved

The idea is a pixel budget. A chart gets roughly two thousand horizontal pixels. A series of fifty million points cannot show more detail than that, no matter how much of it you send. So the browser should not receive fifty million points. It should receive an aggregate of the visible range, recomputed whenever the range changes.

plotly-resampler makes that idea practical inside Plotly. It wraps a figure, keeps the high-frequency series on the Python side, and substitutes a small representative sample for the window the user is looking at. The sampling runs in tsdownsample, our Rust package, with MinMaxLTTB as the default algorithm. Zoom in and the figure re-aggregates over the narrower window, so detail appears as you go. plotly-resampler has around 18 million PyPI downloads and tsdownsample around 16 million, so the idea has been tested well past our own use of it.

The narrow scope is why it works. It is also where it stops. Three walls show up in real analysis work.

One figure at a time. plotly-resampler aggregates a single figure, and a Dash callback is registered per figure. Each callback knows its own traces and nothing else. No part of that design carries a selection from one chart into another chart's query. Brushing a time range on a line and watching a histogram re-bin over exactly that range is not a feature you can bolt on at the edge. It needs a layer that owns every figure at once.

The series must be in memory. hf_x and hf_y take arrays, so the data has to be in the process before the figure exists. A 24 GB Parquet file is not an array. You can sample it, down-cast it, or pre-aggregate it first, but then you are exploring a copy, and you picked the resolution before you knew what you were looking for.

The view cannot leave the process. The viewport lives inside a Dash callback. That works, and it is invisible until you want to share it. There is no document that says what the current view is, so there is nothing to put in a URL, nothing to hand a colleague, and nothing for a script or a coding agent to read back. We noticed this most at the end of an analysis. Someone finds the anomaly, and then sends a screenshot of it, because the view itself is not a thing that can be sent.

From plotly-resampler to FlexViz

argminmax SIMD argmin / argmax tsdownsample downsampling algorithms Polars polars-ops, polars-utils flexviz-polars FlexViz's Polars plugin plotly-resampler view-aware figures HoloViews downsample1d hvPlot downsample= FlexViz data exploration, agent-native —— compiled or imported into - - - ideas carried over, no code solid border: our projects dashed border: other people’s
Solid arrows are code dependencies. The dashed arrow is not. It marks what FlexViz carried over from plotly-resampler, which it does not import a line of.

The two projects share no code. They meet further down, in argminmax, the SIMD kernel that sits inside tsdownsample, inside Polars, and inside FlexViz's own Polars plugin. What travelled from one project to the other was the idea, not an implementation.

What carried over

  • Aggregate for the visible range, not for the whole series.
  • MinMaxLTTB, and min/max as the cheap default.
  • The pixel budget: a few thousand points per chart.
  • The same maintainers.

What changed

  • A serializable dashboard spec, not a figure object.
  • A stateless server that receives spec plus event.
  • Lazy Polars, not an in-memory array.
  • Rust kernels as Polars plugins, not Rust over NumPy arrays.
  • Ten trace types, not one.
  • Every view is a URL.

The right-hand column is why FlexViz is a separate project and not a second major version of plotly-resampler. A figure wrapper cannot grow a query layer without turning into something else. So we wrote the something else.

A dashboard is a query system

100M+ rowsyour machine
your browserPlotly or any renderer

Your data stays local. The rows never travel to the browser, only small aggregates do.

Stateless server. Every request carries the full dashboard spec, so any replica can answer it. Shareable URLs fall out of that for free.

Raw rows stay on the machine that holds them. Every interaction is answered with a small aggregate. Measurements are on the benchmark page.

FlexViz stores no session. A zoom, a click, or a brush produces one request that carries the complete dashboard spec plus the event that triggered it. The server looks up the registered source, builds a fresh engine for that request, and asks each affected trace for the aggregation it needs. Selections coming from other figures are compiled into Polars expressions and applied to the LazyFrame before any aggregation runs. The traces on one source are batched and collected together, and every trace turns its slice of the result into a delta: a few thousand points for a line, a few hundred bins for a histogram. What goes back over the wire is that list of deltas, a few kilobytes, which the browser patches into the charts that are already on screen.

Four things follow from that shape:

  • The rows stay put. They are read where they live and aggregated there. The browser never sees them.
  • Response size follows the screen. It is set by the pixel budget, not by the row count.
  • Any replica can answer any request. There are no sessions and no sticky routing.
  • The view is a document. Viewport, selections, cross-filter mode, and grid layout all encode into a URL.

Not every interaction needs the server. Linked hover is resolved in the browser, because the points it highlights are already there. Dragging a brush is served client-side from a small pre-aggregated cube, so every linked chart updates during the drag with no round-trip at all. The request above is what runs when the drag ends, or when a zoom changes what has to be computed.

A range brushed on a FlexViz line chart, after which the linked histogram re-aggregates over the selected range
Cross-filter in the live demo. A brush on the line chart re-aggregates the linked histogram. Update mode replaces the traces with the filtered set, overlay mode draws the selection on top of the unfiltered background.

Side by side

Left: one line chart over five million points, with plotly-resampler. Right: the same kind of signal in FlexViz, read lazily from Parquet, split per signal, and linked to a histogram that cross-filters it.

plotly-resampler
import numpy as np
import plotly.graph_objects as go
from plotly_resampler import FigureResampler

t = np.arange(5_000_000)
y = 20 + 4 * np.sin(t / 200_000)

fig = FigureResampler(go.Figure())
fig.add_trace(
    go.Scattergl(name="temp"), hf_x=t, hf_y=y
)
fig.show_dash()
flexviz
import polars as pl
from flexviz import Dashboard

lf = pl.scan_parquet("signals.parquet")

dash = Dashboard(lf)
dash.add_figure(title="signals").add_line(
    x="timestamp", y="value", group_by="signal",
    n_points=2000, downsample="lttb",
)
dash.add_figure(title="values").add_histogram(
    x="value", bins=60
)
dash.show()

The FlexViz version buys three things the left column cannot express. Brushing the histogram filters the line, and brushing the line re-bins the histogram. The state of that view encodes into a link a colleague can open. The input is a LazyFrame over a file, so the dashboard opens without loading the file. It costs a server process instead of a figure object, and an API that is young enough to still move under you.

For a plotly-resampler user most of the migration is mechanical. default_n_shown_samples becomes n_points, with the same default. MinMaxLTTB() becomes downsample="lttb", and min/max stays the default downsampler. One trace per signal becomes one group_by. The Dash callback registration has no counterpart, because answering interactions is the server's whole job. The gap worth knowing about is shared-x subplots, which have no direct equivalent yet. The closest thing is one figure with several y columns.

What it costs and what it buys

We only publish numbers we can reproduce, and they are all on the benchmark page. With an in-memory frame, peak backend memory stays near 25 MB from 1 million to 200 million rows, because FlexViz reads the caller's frame zero-copy and streams the aggregation rather than materializing a copy. From Parquet, a 1 billion row, 24 GB source drives a line and histogram dashboard, with zoom and cross-filter, in under 400 MB of resident memory. The test suite asserts that flatness per trace, so a regression fails a build rather than a demo. Box plots are the stated exception, because Polars computes quantiles in memory. These are measurements of our own engine on one machine. They say what the engine does at scale, not that it beats any particular tool at equal work.

Where FlexViz stands

FlexViz is on GitHub under Apache-2.0. The whole engine is public and there is no gated edition. It has been on PyPI as flexviz since 25 August 2026, so pip install flexviz is enough to start, and the documentation is at docs.flexviz.tech.

It is also pre-1.0, and we label it that way on purpose. The Python API, the defaults, and the dashboard spec can change between minor versions while we learn from real datasets. Open source makes the implementation readable. It does not make an unsettled API a stable contract. Early users should expect movement, and they get a say in which parts settle first.

Which project should you use?

Use plotly-resampler when you already work in Plotly or Dash and need a responsive line chart over a large in-memory series. It is a smaller thing to adopt, it fits an existing figure, and for that job the smaller scope is the right one. Nothing about it is deprecated.

Use FlexViz when the question spans several linked views, when you need cross-filtering across chart types, when the view has to be shareable as a link, or when the data is a file rather than an array. It does more, and it is the younger project of the two.

This is not a story about a replacement. It is one about a focused tool that proved an idea, and a broader system built on top of what that idea implies once more than one chart is involved. If you are weighing FlexViz against a difficult dataset or an existing data platform, you can try the live demo, read the source, or get in touch.

Related: FlexViz and plotly-resampler on the open-source page.