From a6ec74a5484a5f3f883172db029b131e3b911728 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 3 Jul 2026 14:31:55 +0200 Subject: [PATCH 1/2] [charts-pro] Allow sampling on non-zoomable charts Sampling was gated on an axis zoom entry, so a static chart with a large dataset got no benefit. When sampling is enabled but the axis has no zoom, sample at full range (span 100%), still bounded by MAX_RENDERED_POINTS. Closes #23031 --- docs/data/charts/performance/performance.md | 5 +- .../BarChartPro/BarChartPro.sampling.test.tsx | 22 +++++++- .../LineChartPro.sampling.test.tsx | 55 +++++++++++++++++++ .../x-charts/src/BarChart/useBarPlotData.ts | 7 ++- .../x-charts/src/LineChart/useLinePlotData.ts | 3 +- .../sampling.selectors.ts | 16 +++++- 6 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx diff --git a/docs/data/charts/performance/performance.md b/docs/data/charts/performance/performance.md index 6d7c6dc79cd25..60f0c2961a854 100644 --- a/docs/data/charts/performance/performance.md +++ b/docs/data/charts/performance/performance.md @@ -113,8 +113,9 @@ See [Bar charts—WebGL renderer](/x/react-charts/bars/#webgl-renderer), [Scatte ## Sampling [](/x/introduction/licensing/#pro-plan 'Pro plan') -When a zoomable chart has more data than the drawing area can show without making elements +When a chart has more data than the drawing area can show without making elements too small, the data can be reduced to an appropriate level of detail before rendering. +This applies to both zoomable and static charts. `BarChartPro` and `LineChartPro` accept a `sampling` prop that selects the method: @@ -123,7 +124,7 @@ too small, the data can be reduced to an appropriate level of detail before rend - `'m4'`: pixel-accurate—keep the first, min, max, and last point of each bucket (line series). - `'lttb'`: [Largest-Triangle-Three-Buckets](https://skemman.is/handle/1946/15343), keeps one representative point per bucket to preserve the shape (line series). -Zoom in to progressively reveal more detail; sampling turns off once the view can resolve every element. +On a zoomable chart, zoom in to progressively reveal more detail; sampling turns off once the view can resolve every element. A static chart (no `zoom`) is sampled at its full range, so a large dataset still renders at an appropriate level of detail. 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. diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.sampling.test.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.sampling.test.tsx index 309b17209ab9f..a67fe25fb8bc7 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.sampling.test.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.sampling.test.tsx @@ -11,7 +11,27 @@ describe(' - Sampling', () => { const range = (length: number) => Array.from({ length }, (_, i) => i); - // Sampling only applies to zoomable axes, so every case enables zoom. + // Regression: https://github.com/mui/mui-x/issues/23031 — sampling used to require zoom. + it('samples a static (non-zoomable) axis at full range', () => { + const dataLength = 256; + const { container } = render( + , + ); + + const rendered = countBars(container); + expect(rendered).to.be.greaterThan(0); + expect(rendered).to.be.lessThan(dataLength / 2); + }); + it('renders one bar per data point when bars are wide enough', () => { const { container } = render( { + const paths = Array.from(container.querySelectorAll('path[data-series]')); + const line = paths.find((p) => p.classList.contains(lineClasses.line)); + return ((line?.getAttribute('d') ?? '').match(/[MLC]/g) ?? []).length; +}; + +describe(' - Sampling', () => { + const { render } = createRenderer(); + const range = (n: number) => Array.from({ length: n }, (_, i) => i); + + // Regression: https://github.com/mui/mui-x/issues/23031 — sampling used to require zoom. + it('samples a static (non-zoomable) axis at full range', () => { + const dataLength = 1024; + const { container } = render( + , + ); + + const rendered = linePointCount(container); + expect(rendered).to.be.greaterThan(0); + expect(rendered).to.be.lessThan(dataLength / 2); + }); + + it('renders every point when sampling is disabled (default)', () => { + const dataLength = 1024; + const { container } = render( + , + ); + + expect(linePointCount(container)).to.equal(dataLength); + }); +}); diff --git a/packages/x-charts/src/BarChart/useBarPlotData.ts b/packages/x-charts/src/BarChart/useBarPlotData.ts index 171f180270768..16faaa57656a3 100644 --- a/packages/x-charts/src/BarChart/useBarPlotData.ts +++ b/packages/x-charts/src/BarChart/useBarPlotData.ts @@ -14,7 +14,10 @@ import { import type { ChartDrawingArea } from '../hooks/useDrawingArea'; import { useChartId } from '../hooks/useChartId'; import { useStore } from '../internals/store/useStore'; -import { selectorChartSamplingPyramids } from '../internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors'; +import { + selectorChartSamplingPyramids, + getFullRangeZoom, +} from '../internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors'; import type { SampledSeriesLookup, SamplingStrategy, @@ -215,7 +218,7 @@ export function processBarDataForPlot( const pyramid = samplingPyramids[seriesId]; const baseAxisId = verticalLayout ? xAxisId : yAxisId; - const zoom = zoomMap?.get(baseAxisId); + const zoom = zoomMap?.get(baseAxisId) ?? (pyramid ? getFullRangeZoom(baseAxisId) : undefined); const availableSize = verticalLayout ? drawingArea.width : drawingArea.height; const minSpan = zoomOptions?.[baseAxisId]?.minSpan ?? 0; // The sampler (pro) owns the level-of-detail math; community renders one merged rect per diff --git a/packages/x-charts/src/LineChart/useLinePlotData.ts b/packages/x-charts/src/LineChart/useLinePlotData.ts index 8039244e47c71..01887eba1075a 100644 --- a/packages/x-charts/src/LineChart/useLinePlotData.ts +++ b/packages/x-charts/src/LineChart/useLinePlotData.ts @@ -8,6 +8,7 @@ import type { ComputedAxisConfig } from '../internals/plugins/featurePlugins/use import { selectorChartSamplingState, selectorChartSamplingPyramids, + getFullRangeZoom, } from '../internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors'; import type { SampledBucket } from '../internals/plugins/featurePlugins/useChartCartesianAxis/sampling.types'; import type { ZoomMap } from '../internals/plugins/featurePlugins/useChartCartesianAxis/zoom.types'; @@ -140,7 +141,7 @@ export function useLinePlotData( // Only the plain (non-step) path is sampled; step expansion falls back to the full render. const built = sampledSeries[seriesId]; - const zoom = zoomMap?.get(xAxisId); + const zoom = zoomMap?.get(xAxisId) ?? (built ? getFullRangeZoom(xAxisId) : undefined); let sampledBuckets: SampledBucket[] | null = null; if (samplingEnabled && built && zoom && !shouldExpand && xData) { // Pro owns the sampling math; community flattens the buckets into a polyline. diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors.ts index 18237eb3d8054..eadcdeca7d979 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/sampling.selectors.ts @@ -7,6 +7,7 @@ import type { AxisId } from '../../../../models/axis'; import type { ChartRootSelector } from '../../utils/selectors'; import type { UseChartCartesianAxisSignature } from './useChartCartesianAxis.types'; import type { SampledSeriesLookup } from './sampling.types'; +import type { ZoomData } from './zoom.types'; import { selectorChartXAxis, selectorChartYAxis, @@ -17,6 +18,15 @@ import { const EMPTY_PYRAMIDS: SampledSeriesLookup = {}; const EMPTY_BUCKET_SIZES: Map = new Map(); +/** + * Full-range zoom (span 100%) for a non-zoomable axis. Sampling gates on a zoom entry; a static axis + * has none, so we sample it at full range — the `MAX_RENDERED_POINTS` cap still applies. Mirrors the + * full-range zoom the zoom-slider preview uses to keep its density stable. + */ +export function getFullRangeZoom(axisId: AxisId): ZoomData { + return { axisId, start: 0, end: 100 }; +} + export const selectorChartSamplingState: ChartRootSelector< UseChartCartesianAxisSignature, 'sampling' @@ -103,9 +113,9 @@ export const selectorChartHighlightBucketSize = createSelectorMemoized( ).forEach(([axes, availableSize]) => { axes.axisIds.forEach((axisId) => { const data = axes.axis[axisId].data; - const zoom = zoomMap?.get(axisId); - const minSpan = zoomOptions[axisId]?.minSpan; - if (data && zoom && minSpan != null) { + const zoom = zoomMap?.get(axisId) ?? getFullRangeZoom(axisId); + const minSpan = zoomOptions[axisId]?.minSpan ?? 0; + if (data) { bucketSizes.set( axisId, bucketSizeAt(zoom.end - zoom.start, { From 72dbe8fa5b4fffe53f37d8430a72f5ced170f65c Mon Sep 17 00:00:00 2001 From: Jose C Quintas Jr Date: Wed, 8 Jul 2026 10:42:43 +0200 Subject: [PATCH 2/2] Update packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Signed-off-by: Jose C Quintas Jr --- .../x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx index 8d89736693bca..48b6b74c2402a 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.sampling.test.tsx @@ -14,7 +14,6 @@ describe(' - Sampling', () => { const { render } = createRenderer(); const range = (n: number) => Array.from({ length: n }, (_, i) => i); - // Regression: https://github.com/mui/mui-x/issues/23031 — sampling used to require zoom. it('samples a static (non-zoomable) axis at full range', () => { const dataLength = 1024; const { container } = render(