[charts-pro] Add data sampling for large bar and line charts#22830
Conversation
Deploy previewBundle size
PerformanceTotal duration: 1,849.18 ms -98.10 ms(-5.0%) | Renders: 63 (+0)
…and 1 more (+20 within noise) — details Metric alarms
Check out the code infra dashboard for more information about this PR. |
…lated-sub-sampling
- barSampler: envelope over min/max of both stacked coords so diverging/negative bars keep their full extent when merged (was truncated to the base channel). - largestTriangleThreeBuckets: return the two endpoints when threshold <= 2 instead of the whole dataset, which bypassed the rendered-point cap. - useBarPlotData: memoize processBarDataForPlot so hover/tooltip re-renders don't recompute all bar geometry.
Iterate the sampling config instead of hardcoding bar/line, so a new series type only needs its config entry and a registered sampler.
Controlled shared zoomData so zooming one chart zooms all, comparing the methods over the same range.
| Lines support all three algorithms; pick the one that best fits your data. | ||
|
|
||
| ```jsx | ||
| <LineChartPro series={series} sampling="m4" /> | ||
| ``` | ||
|
|
||
| {{"demo": "LineSampling.js"}} | ||
|
|
There was a problem hiding this comment.
Since we already have section comparing the 3 methods
| Lines support all three algorithms; pick the one that best fits your data. | |
| ```jsx | |
| <LineChartPro series={series} sampling="m4" /> | |
| ``` | |
| {{"demo": "LineSampling.js"}} | |
| Lines support all three algorithms; pick the one that best fits your data. | |
| See [Comparing section](#comparing-methods). | |
| ```jsx | |
| <LineChartPro series={series} sampling="m4" /> |
| Sampling also keeps a hard ceiling on the number of rendered elements (2,000). Once the view fits under it, the chart renders every visible element untouched; while a zoomed-in view still holds more than that, it stays sampled even when each element would be wide enough to draw on its own—so a very dense dataset never floods the renderer at any zoom level. | ||
|
|
||
| :::info | ||
| The zoom `minSpan` is a percentage, so the most zoomed-in view always renders about `dataLength × minSpan / 100` elements. On a very large dataset, keep the axis `zoom.minSpan` small so the deepest zoom doesn't render too many elements at once. | ||
| ::: |
There was a problem hiding this comment.
If you really want to keep this behavior of switching from a subsample to no subsample when reaching the minSpan, at least modify the bar demo such that minSpan is small enougth to have all the level visible
Here is the zoom experience with and without adapted min span
Capture.video.du.2026-07-01.12-14-24.mp4
Capture.video.du.2026-07-01.12-13-44.mp4
| const data = yAxis.data; | ||
| const bucketSize = bucketSizeByAxis.get(axisId) ?? 1; | ||
| if (bucketSize > 1 && data) { | ||
| const index = data.indexOf(value); |
There was a problem hiding this comment.
For a follow up: We could modify the ordinal scale to get access to its index such that this would be done in O(1) instead of O(N)
| if (index >= 0) { | ||
| const bucketStart = Math.floor(index / bucketSize) * bucketSize; | ||
| const bucketEnd = Math.min(bucketStart + bucketSize - 1, data.length - 1); | ||
| bandStart = yScale(data[bucketStart])! - (step - yScale.bandwidth()) / 2; |
There was a problem hiding this comment.
AI suggestion:
- To support reversed axis you should add
const bucketStartPosition = Math.min(yScale(data[bucketStart])!, yScale(data[bucketEnd])!); - This
if(bucketSize>1) ...is duplicated between X and Y highlight. an helper that take (index, scale, bucketSize) and return (bandSTart, bandSize) would avoid duplication
| if (algorithm === 'lttb') { | ||
| // LTTB emits one point per bucket, vs `ENVELOPE_POINTS_PER_BUCKET` for the min/max methods. | ||
| // Scale the budget by that factor so LTTB isn't sparser than them at the same zoom. | ||
| const indices = largestTriangleThreeBuckets( | ||
| getValues!(), | ||
| Math.ceil((ENVELOPE_POINTS_PER_BUCKET * pyramid.dataLength) / bucketSize), | ||
| ); | ||
| return [{ startIndex: 0, endIndex: maxIndex, indices: mergeBucket(indices, maxIndex) }]; | ||
| } |
There was a problem hiding this comment.
Seems like the 'lttb' is recomputing at each zoom. Instead of picking the indexes from a saved array, it call getValues() which correspond to getValues: () => Float64Array.from(visibleStackedData, (point) => point[1])
Can be left for a follow up issue
alexfauquette
left a comment
There was a problem hiding this comment.
Technically it look nice. I left suggestion about minor issue that could be done in dedicated PRs
I'm just bothered by the default behavior that to me looks weird, but issues are already create to let user pick a different behavior
- Skip the sampling-state update when unchanged so an inlined config doesn't recompute the pyramids every render. - Docs: link line sampling to the comparison section. - Bar demo: small minSpan so zoom steps through every sampling level to raw.
Will wait to merge this after release so we have more time to handle issues :) |
Co-authored-by: Alexandre Fauquette <[email protected]> Signed-off-by: Jose C Quintas Jr <[email protected]>
Summary
Adds opt-in data sampling to the Pro bar and line charts, so large datasets stay responsive when zoomed out. When the data has more points than the axis can meaningfully render, each series is reduced to a zoom-appropriate level of detail before drawing; zooming in progressively reveals more, down to the raw data at full zoom.
API
Type-specific charts take a single
samplingprop, typed per series type:LineChartPro:sampling="none" | "minmax" | "m4" | "lttb"(default"none")BarChartPro/BarChartPremium:sampling="none" | "minmax"(bars always use a min/max envelope, so the line-only algorithms are excluded)The generic
ChartsDataProviderProtakes a per-series-type map, so a multi-type chart can configure each independently:Methods:
minmax— keep the min and max per bucket (2 points).m4— pixel-accurate: first, min, max, last per bucket (line only).lttb— Largest-Triangle-Three-Buckets, preserves the visual shape (line only).Sampling only runs on zoomable axes and requires
@mui/x-charts-pro.How it works
SamplingStrategyonseriesConfig.sampler. Community plot hooks/selectors only consume the pro-injected strategy, so the community bundle stays algorithm-free.BarSeriesExtension/LineSeriesExtensionseries-config extension points (empty in community, augmented by pro) — mirroring the existingAxisConfigExtensionpattern. Enabling sampling on a new series type means adding itssamplingMethodextension and registering a sampler; the per-type config map and the state-building are generic. (Type-specific chart components still wire their ownsamplingprop, and the axis-highlight widening currently special-cases bar/line.)Docs & tests
LineSampling,BarSampling.Closes #22671
Closes #22713