Downsampling
Downsampling is what keeps TVL responsive on multi-million-row tables. Line, Area, and Baseline series automatically thin out points as you pan and zoom, so the renderer never draws more bars than the chart has pixels — while still keeping every local minimum and maximum, so spikes and dips never disappear.
In normal use this is fully automatic: plot a big table with tvl.line(...) and it just works. This page is background for when you’re curious how it behaves or want to nudge the few available knobs.
What is downsampling useful for?
- Smooth interaction on big tables: A 1M-row line is unplayable if every pan ships 1M points to the browser; downsampling brings it down to ~viewport-width points and keeps interaction snappy.
- Faithful min/max preservation: The downsampler picks the highest and lowest point in each pixel column, so a tall single-row spike survives even when nine of its neighbors are dropped.
- Zero authoring cost: There’s no separate “downsampled chart” mode. Write
tvl.line(table, ...)against a 1M-row table and it works. - Whitespace fills: Whitespace-aware aggregation means leading and trailing gaps don’t compress the visible region.
How it activates
TVL has two server-side / client-side cooperating paths:
- Client-side viewport downsampling (Line / Area / Baseline). When a series has more points than the viewport has bars, the client requests a downsampled view from the server, asking for “give me the min and the max for each pixel column in the visible time range.” The aggregation runs in the Deephaven query engine via
agg_bywithfirst/last/sorted_first/sorted_lastreductions per bucket. On pan and zoom the client re-requests with the new range; on data ticks the buckets refresh. - Server-side autobinning (Candlestick / Bar / Histogram). OHLC and histogram series can’t be downsampled by min/max: they need a true aggregation. See autobin for that path.
[!NOTE] > Numeric-axis charts don’t downsample. Viewport downsampling is gated to
chart_type == "standard"and reads a hard-codedtimecolumn.tvl.custom_numeric,tvl.options_chart, andtvl.yield_curvetherefore ship raw rows to the client. Keep their input tables modest (tens of thousands of rows) or pre-aggregate server-side.
What activates it
- Row count threshold. Tables with more rows than the downsampler’s per-series budget activate the path automatically. The exact threshold is the chart’s pixel width (rounded up to a 1000-pixel bucket for cache friendliness) divided by the target bar-pixel-width.
- Viewport width. The client tells the server how many pixels wide the chart is; the server returns at most ~one bucket per pixel.
- Whitespace edges. When the visible window doesn’t cover the entire table, the server returns the body of the visible range at full resolution plus single “anchor” rows at the head and tail so
fix_left_edge/fix_right_edgehave something to clamp against.
What it preserves
For Line / Area / Baseline series, each bucket reduces to (at most) four points: first time, last time, sorted-first value, sorted-last value. That guarantees:
- The min and the max of every bucket survive. Spikes are not averaged away.
- The bucket boundaries are real data points, not interpolated midpoints: so hovering still shows actual timestamps and values.
- Vertical order is preserved within each bucket by drawing the four points in time order, so the rendered polyline respects the rise/fall of the underlying data.
For OHLC and histogram series, downsampling reduces each bucket via first(open), max(high), min(low), last(close) (candlestick / bar) or sum / count / avg / last (histogram). See autobin.
Examples
A 1M-row line chart
The tvl.data.large_prices() fixture is a 1,000,000-row intraday price series. Plotting it with tvl.line() works exactly like plotting a 100-row table. TVL handles the downsampling transparently.
Warning
Large tables can take a significant amount of time to initialize.
Pan and zoom the chart: the curve refines as you zoom in. Local minima and maxima stay visible at every zoom level.
Compare a small vs. large table
Plot the same series on a small (values()) and a large (large_prices()) table side by side. The small table is below the downsampler’s threshold and renders every point as-is; the large table is downsampled, but visually the curve is faithful.
The threshold is rarely the thing you tune. Chart width drives it more than row count, and the chart sizes to its container automatically.
Combine downsampling with multiple overlays
Each series downsamples independently. Overlaying a fine-grained line and a coarse moving average doesn’t degrade either, since each series gets its own bucket budget.
Both lines stay smooth under interaction; each gets its own server-side aggregation.
Use bar spacing to control downsampling density
bar_spacing (pixels per bar) and min_bar_spacing / max_bar_spacing (zoom limits), all passed via tvl.time_scale(...), shape the downsampler’s bucket size indirectly. Larger bar_spacing means fewer buckets per pixel, which gives the renderer more breathing room.
These knobs matter most when the rendered bars start dropping below a single pixel. Bump min_bar_spacing to keep them visible.
Downsampling is always on (Line / Area / Baseline)
There is no switch to disable downsampling for Line / Area / Baseline series, by design — turning it off would be almost always wrong:
- Browsers can’t render 1M points in a polyline at 60fps. Even on fast hardware, building and laying out the SVG / canvas path is bottlenecked on point count.
- The downsampled view is faithful. Min and max per bucket survive: the only thing you lose is rendering every individual point, which the chart couldn’t show anyway because each one is sub-pixel.
If you have a table that’s small enough that downsampling wouldn’t help and you don’t want to pay the round-trip overhead, just plot the table directly. TVL only activates the downsampling path when the chart pixel budget is smaller than the row count.
For OHLC and histogram series, the equivalent toggle exists (auto_bin=False). See autobin. The same advice applies: disable it only when the table is already small.
Tunables
The downsampler is mostly automatic, but a few variables on chart() and the series factories influence it:
width/heightonchart(): pin the pixel budget, which sets the bucket target.bar_spacing/min_bar_spacing/max_bar_spacing(ontvl.time_scale(...)): pixels per bar, which drives the bucket count.auto_bin/bin_width/bin_count(Candlestick / Bar / Histogram): see autobin.fix_left_edge/fix_right_edge(ontvl.time_scale(...)): anchor the visible range to the data edges. The downsampler emits single-row anchors at the head and tail when the visible window is strictly inside the data range so these clamps work.
For benchmarking comparisons (server-side autobin vs. client-side rendering cost), see large-data.
API Reference
The downsample-related options (downsample, downsample_threshold, downsample_target_buckets, downsample_min_pixel_step) live on tvl.chart. For the line-specific surface, see tvl.line.