Skip to content

[charts-pro] Add data sampling for large bar and line charts#22830

Merged
JCQuintas merged 63 commits into
mui:masterfrom
JCQuintas:precalculated-sub-sampling
Jul 3, 2026
Merged

[charts-pro] Add data sampling for large bar and line charts#22830
JCQuintas merged 63 commits into
mui:masterfrom
JCQuintas:precalculated-sub-sampling

Conversation

@JCQuintas

@JCQuintas JCQuintas commented Jun 16, 2026

Copy link
Copy Markdown
Member

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 sampling prop, 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 ChartsDataProviderPro takes a per-series-type map, so a multi-type chart can configure each independently:

<ChartsDataProviderPro sampling={{ line: 'lttb', bar: 'minmax' }} />

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

  • A level-of-detail "pyramid" is precomputed per series and memoized on the data, so it rebuilds only when the data changes.
  • The active level is picked from the zoom span vs. the available pixels, capped at a maximum rendered-point count.
  • All sampling algorithms live in the pro package, registered as a per-series-type SamplingStrategy on seriesConfig.sampler. Community plot hooks/selectors only consume the pro-injected strategy, so the community bundle stays algorithm-free.
  • The set of methods each series type accepts is declared in one place via the BarSeriesExtension / LineSeriesExtension series-config extension points (empty in community, augmented by pro) — mirroring the existing AxisConfigExtension pattern. Enabling sampling on a new series type means adding its samplingMethod extension and registering a sampler; the per-type config map and the state-building are generic. (Type-specific chart components still wire their own sampling prop, and the axis-highlight widening currently special-cases bar/line.)

Docs & tests

  • Performance demos: LineSampling, BarSampling.
  • Unit tests for the pyramid math, bucket selection, and each algorithm.
  • Render tests plus visual regression fixtures covering every method at multiple zoom levels for both chart types.

Closes #22671
Closes #22713

@JCQuintas JCQuintas added plan: Pro Impact at least one Pro user. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: charts Changes related to the charts. labels Jun 16, 2026
@JCQuintas JCQuintas self-assigned this Jun 16, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 16, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 🔺+2.94KB(+0.76%) 🔺+1.3KB(+1.11%)
@mui/x-charts-pro 🔺+7KB(+1.35%) 🔺+3.03KB(+1.95%)
@mui/x-charts-premium 🔺+7.04KB(+1.12%) 🔺+3.05KB(+1.61%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 0B(0.00%) 0B(0.00%)
@mui/x-scheduler-premium 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes

Performance

Total duration: 1,849.18 ms -98.10 ms(-5.0%) | Renders: 63 (+0)

Test Duration Renders
BarChart with big data amount 90.36 ms 🔺+22.91 ms(+34.0%) 2 (+0)
ScatterChartPremium with big data amount (webgl renderer) 71.90 ms ▼-23.40 ms(-24.6%) 5 (+0)
ScatterChart with big data amount 23.56 ms ▼-6.20 ms(-20.8%) 2 (+0)
ScatterChartPro with big data amount and zoomed in (batch renderer) 7.66 ms ▼-4.49 ms(-36.9%) 2 (+0)
ScatterChartPro with big data amount (batch renderer) 7.25 ms ▼-2.54 ms(-26.0%) 2 (+0)

…and 1 more (+20 within noise) — details

Metric alarms

Test Metric Change
BarChart with big data amount bench:paint 🔺 +35.12 ms
BarChart stacked with multiple series bench:paint 🔺 +40.31 ms

Check out the code infra dashboard for more information about this PR.

- 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.
@JCQuintas
JCQuintas requested a review from alexfauquette June 30, 2026 07:54
Controlled shared zoomData so zooming one chart zooms all, comparing the methods
over the same range.
Comment on lines +155 to +162
Lines support all three algorithms; pick the one that best fits your data.

```jsx
<LineChartPro series={series} sampling="m4" />
```

{{"demo": "LineSampling.js"}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have section comparing the 3 methods

Suggested change
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" />

Comment on lines +130 to +134
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.
:::

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI suggestion:

  1. To support reversed axis you should addconst bucketStartPosition = Math.min(yScale(data[bucketStart])!, yScale(data[bucketEnd])!);
  2. 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

Comment on lines +67 to +75
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) }];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alexfauquette left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@JCQuintas

Copy link
Copy Markdown
Member Author

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

Will wait to merge this after release so we have more time to handle issues :)

@JCQuintas
JCQuintas merged commit b193f7f into mui:master Jul 3, 2026
21 checks passed
@JCQuintas
JCQuintas deleted the precalculated-sub-sampling branch July 3, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plan: Pro Impact at least one Pro user. scope: charts Changes related to the charts. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[charts-pro] Add series-wide parameters for sampling

2 participants