@mui/x-charts-pro 9.8.0 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/BarChartPro/BarChartPro.d.mts +9 -1
  2. package/BarChartPro/BarChartPro.d.ts +9 -1
  3. package/BarChartPro/BarChartPro.js +14 -3
  4. package/BarChartPro/BarChartPro.mjs +14 -3
  5. package/BarChartPro/BarChartPro.plugins.d.mts +2 -1
  6. package/BarChartPro/BarChartPro.plugins.d.ts +2 -1
  7. package/BarChartPro/BarChartPro.plugins.js +2 -1
  8. package/BarChartPro/BarChartPro.plugins.mjs +2 -1
  9. package/CHANGELOG.md +119 -0
  10. package/ChartsContainerPro/useChartsContainerProProps.d.mts +6 -2
  11. package/ChartsContainerPro/useChartsContainerProProps.d.ts +6 -2
  12. package/ChartsContainerPro/useChartsContainerProProps.js +9 -1
  13. package/ChartsContainerPro/useChartsContainerProProps.mjs +8 -1
  14. package/ChartsDataProviderPro/ChartsDataProviderPro.d.mts +1 -1
  15. package/ChartsDataProviderPro/ChartsDataProviderPro.d.ts +1 -1
  16. package/ChartsDataProviderPro/ChartsDataProviderPro.js +14 -3
  17. package/ChartsDataProviderPro/ChartsDataProviderPro.mjs +14 -3
  18. package/ChartsToolbarPro/ChartsToolbarPro.d.mts +2 -1
  19. package/ChartsToolbarPro/ChartsToolbarPro.d.ts +2 -1
  20. package/ChartsToolbarPro/internals/ChartsMenu.d.mts +1 -1
  21. package/ChartsToolbarPro/internals/ChartsMenu.d.ts +1 -1
  22. package/ChartsZoomSlider/ChartsZoomSlider.d.mts +2 -1
  23. package/ChartsZoomSlider/ChartsZoomSlider.d.ts +2 -1
  24. package/ChartsZoomSlider/internals/ChartsAxisZoomSlider.d.mts +2 -1
  25. package/ChartsZoomSlider/internals/ChartsAxisZoomSlider.d.ts +2 -1
  26. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderActiveTrack.d.mts +2 -1
  27. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderActiveTrack.d.ts +2 -1
  28. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderPreview.d.mts +2 -1
  29. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderPreview.d.ts +2 -1
  30. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderPreviewContent.d.mts +2 -1
  31. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderPreviewContent.d.ts +2 -1
  32. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderTrack.d.mts +1 -1
  33. package/ChartsZoomSlider/internals/ChartsAxisZoomSliderTrack.d.ts +1 -1
  34. package/ChartsZoomSlider/internals/ChartsTooltipZoomSliderValue.d.mts +1 -1
  35. package/ChartsZoomSlider/internals/ChartsTooltipZoomSliderValue.d.ts +1 -1
  36. package/ChartsZoomSlider/internals/previews/AreaPreviewPlot.d.mts +1 -1
  37. package/ChartsZoomSlider/internals/previews/AreaPreviewPlot.d.ts +1 -1
  38. package/ChartsZoomSlider/internals/previews/BarPreviewPlot.d.mts +1 -1
  39. package/ChartsZoomSlider/internals/previews/BarPreviewPlot.d.ts +1 -1
  40. package/ChartsZoomSlider/internals/previews/LineAreaPreviewPlot.d.mts +2 -1
  41. package/ChartsZoomSlider/internals/previews/LineAreaPreviewPlot.d.ts +2 -1
  42. package/ChartsZoomSlider/internals/previews/LinePreviewPlot.d.mts +1 -1
  43. package/ChartsZoomSlider/internals/previews/LinePreviewPlot.d.ts +1 -1
  44. package/ChartsZoomSlider/internals/previews/LinePreviewPlot.js +16 -1
  45. package/ChartsZoomSlider/internals/previews/LinePreviewPlot.mjs +17 -2
  46. package/ChartsZoomSlider/internals/previews/ScatterPreviewPlot.d.mts +2 -1
  47. package/ChartsZoomSlider/internals/previews/ScatterPreviewPlot.d.ts +2 -1
  48. package/FunnelChart/FocusedFunnelSection.d.mts +1 -1
  49. package/FunnelChart/FocusedFunnelSection.d.ts +1 -1
  50. package/FunnelChart/FunnelPlot.d.mts +1 -1
  51. package/FunnelChart/FunnelPlot.d.ts +1 -1
  52. package/FunnelChart/seriesConfig/index.js +2 -2
  53. package/FunnelChart/seriesConfig/index.mjs +2 -2
  54. package/FunnelChart/seriesConfig/tooltipPosition.d.mts +2 -3
  55. package/FunnelChart/seriesConfig/tooltipPosition.d.ts +2 -3
  56. package/FunnelChart/seriesConfig/tooltipPosition.js +11 -15
  57. package/FunnelChart/seriesConfig/tooltipPosition.mjs +11 -15
  58. package/Heatmap/FocusedHeatmapCell.d.mts +1 -1
  59. package/Heatmap/FocusedHeatmapCell.d.ts +1 -1
  60. package/Heatmap/HeatmapItem.d.mts +1 -1
  61. package/Heatmap/HeatmapItem.d.ts +1 -1
  62. package/Heatmap/HeatmapSVGPlot.d.mts +2 -1
  63. package/Heatmap/HeatmapSVGPlot.d.ts +2 -1
  64. package/Heatmap/HeatmapTooltip/HeatmapTooltip.d.mts +1 -1
  65. package/Heatmap/HeatmapTooltip/HeatmapTooltip.d.ts +1 -1
  66. package/Heatmap/HeatmapTooltip/HeatmapTooltipContent.d.mts +1 -1
  67. package/Heatmap/HeatmapTooltip/HeatmapTooltipContent.d.ts +1 -1
  68. package/Heatmap/seriesConfig/index.js +2 -2
  69. package/Heatmap/seriesConfig/index.mjs +2 -2
  70. package/Heatmap/seriesConfig/tooltipPosition.d.mts +2 -3
  71. package/Heatmap/seriesConfig/tooltipPosition.d.ts +2 -3
  72. package/Heatmap/seriesConfig/tooltipPosition.js +12 -16
  73. package/Heatmap/seriesConfig/tooltipPosition.mjs +12 -16
  74. package/LineChartPro/LineChartPro.d.mts +11 -1
  75. package/LineChartPro/LineChartPro.d.ts +11 -1
  76. package/LineChartPro/LineChartPro.js +16 -3
  77. package/LineChartPro/LineChartPro.mjs +16 -3
  78. package/LineChartPro/LineChartPro.plugins.d.mts +2 -1
  79. package/LineChartPro/LineChartPro.plugins.d.ts +2 -1
  80. package/LineChartPro/LineChartPro.plugins.js +2 -1
  81. package/LineChartPro/LineChartPro.plugins.mjs +2 -1
  82. package/SankeyChart/FocusedSankeyLink.d.mts +1 -1
  83. package/SankeyChart/FocusedSankeyLink.d.ts +1 -1
  84. package/SankeyChart/FocusedSankeyNode.d.mts +1 -1
  85. package/SankeyChart/FocusedSankeyNode.d.ts +1 -1
  86. package/SankeyChart/SankeyDataProvider.d.mts +1 -1
  87. package/SankeyChart/SankeyDataProvider.d.ts +1 -1
  88. package/SankeyChart/SankeyLinkLabelPlot.d.mts +1 -1
  89. package/SankeyChart/SankeyLinkLabelPlot.d.ts +1 -1
  90. package/SankeyChart/SankeyLinkPlot.d.mts +1 -1
  91. package/SankeyChart/SankeyLinkPlot.d.ts +1 -1
  92. package/SankeyChart/SankeyNodeLabelPlot.d.mts +1 -1
  93. package/SankeyChart/SankeyNodeLabelPlot.d.ts +1 -1
  94. package/SankeyChart/SankeyNodePlot.d.mts +1 -1
  95. package/SankeyChart/SankeyNodePlot.d.ts +1 -1
  96. package/SankeyChart/SankeyPlot.d.mts +1 -1
  97. package/SankeyChart/SankeyPlot.d.ts +1 -1
  98. package/SankeyChart/SankeyTooltip/SankeyTooltip.d.mts +1 -1
  99. package/SankeyChart/SankeyTooltip/SankeyTooltip.d.ts +1 -1
  100. package/SankeyChart/SankeyTooltip/SankeyTooltipContent.d.mts +1 -1
  101. package/SankeyChart/SankeyTooltip/SankeyTooltipContent.d.ts +1 -1
  102. package/SankeyChart/seriesConfig/index.js +2 -2
  103. package/SankeyChart/seriesConfig/index.mjs +2 -2
  104. package/SankeyChart/seriesConfig/tooltipPosition.d.mts +2 -3
  105. package/SankeyChart/seriesConfig/tooltipPosition.d.ts +2 -3
  106. package/SankeyChart/seriesConfig/tooltipPosition.js +6 -11
  107. package/SankeyChart/seriesConfig/tooltipPosition.mjs +5 -10
  108. package/index.js +1 -1
  109. package/index.mjs +1 -1
  110. package/internals/ChartsWatermark.d.mts +2 -1
  111. package/internals/ChartsWatermark.d.ts +2 -1
  112. package/internals/index.d.mts +2 -0
  113. package/internals/index.d.ts +2 -0
  114. package/internals/index.js +30 -0
  115. package/internals/index.mjs +2 -0
  116. package/internals/material/components/BaseMenuItem.d.mts +1 -1
  117. package/internals/material/components/BaseMenuItem.d.ts +1 -1
  118. package/internals/material/components/BasePopper.d.mts +2 -1
  119. package/internals/material/components/BasePopper.d.ts +2 -1
  120. package/internals/plugins/useChartProSampling/barSampler.d.mts +8 -0
  121. package/internals/plugins/useChartProSampling/barSampler.d.ts +8 -0
  122. package/internals/plugins/useChartProSampling/barSampler.js +18 -0
  123. package/internals/plugins/useChartProSampling/barSampler.mjs +12 -0
  124. package/internals/plugins/useChartProSampling/index.d.mts +2 -0
  125. package/internals/plugins/useChartProSampling/index.d.ts +2 -0
  126. package/internals/plugins/useChartProSampling/index.js +12 -0
  127. package/internals/plugins/useChartProSampling/index.mjs +1 -0
  128. package/internals/plugins/useChartProSampling/lineSampler.d.mts +8 -0
  129. package/internals/plugins/useChartProSampling/lineSampler.d.ts +8 -0
  130. package/internals/plugins/useChartProSampling/lineSampler.js +47 -0
  131. package/internals/plugins/useChartProSampling/lineSampler.mjs +41 -0
  132. package/internals/plugins/useChartProSampling/sampling.d.mts +38 -0
  133. package/internals/plugins/useChartProSampling/sampling.d.ts +38 -0
  134. package/internals/plugins/useChartProSampling/sampling.js +175 -0
  135. package/internals/plugins/useChartProSampling/sampling.line.d.mts +22 -0
  136. package/internals/plugins/useChartProSampling/sampling.line.d.ts +22 -0
  137. package/internals/plugins/useChartProSampling/sampling.line.js +172 -0
  138. package/internals/plugins/useChartProSampling/sampling.line.mjs +164 -0
  139. package/internals/plugins/useChartProSampling/sampling.mjs +164 -0
  140. package/internals/plugins/useChartProSampling/sampling.pyramid.types.d.mts +33 -0
  141. package/internals/plugins/useChartProSampling/sampling.pyramid.types.d.ts +33 -0
  142. package/internals/plugins/useChartProSampling/sampling.pyramid.types.js +5 -0
  143. package/internals/plugins/useChartProSampling/sampling.pyramid.types.mjs +1 -0
  144. package/internals/plugins/useChartProSampling/useChartProSampling.d.mts +4 -0
  145. package/internals/plugins/useChartProSampling/useChartProSampling.d.ts +4 -0
  146. package/internals/plugins/useChartProSampling/useChartProSampling.js +59 -0
  147. package/internals/plugins/useChartProSampling/useChartProSampling.mjs +52 -0
  148. package/internals/plugins/useChartProSampling/useChartProSampling.types.d.mts +25 -0
  149. package/internals/plugins/useChartProSampling/useChartProSampling.types.d.ts +25 -0
  150. package/internals/plugins/useChartProSampling/useChartProSampling.types.js +5 -0
  151. package/internals/plugins/useChartProSampling/useChartProSampling.types.mjs +1 -0
  152. package/internals/plugins/useChartProZoom/gestureHooks/useRegisterZoomGestures.d.mts +4 -3
  153. package/internals/plugins/useChartProZoom/gestureHooks/useRegisterZoomGestures.d.ts +4 -3
  154. package/internals/plugins/useChartProZoom/gestureHooks/useRegisterZoomGestures.mjs +1 -0
  155. package/package.json +6 -6
  156. package/typeOverloads/modules.d.mts +9 -1
  157. package/typeOverloads/modules.d.ts +9 -1
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.largestTriangleThreeBuckets = largestTriangleThreeBuckets;
7
+ exports.sample = sample;
8
+ exports.sampleBuckets = sampleBuckets;
9
+ var _sampling = require("./sampling");
10
+ /** Points the `minmax` envelope renders per bucket (min, max). Used to align the `lttb` budget. */
11
+ const ENVELOPE_POINTS_PER_BUCKET = 2;
12
+
13
+ /**
14
+ * The active level of detail as buckets for the current zoom, or `null` for no sampling. Each bucket
15
+ * carries its original-index span plus the ascending indices to render. The same output feeds both
16
+ * series types: lines flatten the indices into a polyline, bars draw one merged rect per bucket.
17
+ *
18
+ * `minmax` (default; bars use it) keeps {min, max} per bucket; `m4` also brackets them with the
19
+ * bucket's first/last (pixel-accurate); both read the shared `SamplingPyramid`. `lttb` ignores
20
+ * the pyramid and reduces on demand, reading raw values from `getValues` (called only for `lttb`)
21
+ * as a single whole-range bucket. Its budget matches the envelope methods' point density so it
22
+ * isn't visually sparser at the same zoom.
23
+ */
24
+ function sampleBuckets(pyramid, currentSpan, availableSizePx, minSpan, algorithm = 'minmax', getValues) {
25
+ const level = (0, _sampling.selectSamplingLevel)(pyramid, currentSpan, availableSizePx, minSpan);
26
+ if (!level) {
27
+ return null;
28
+ }
29
+ const {
30
+ bucketSize,
31
+ start,
32
+ end
33
+ } = level;
34
+ const {
35
+ nullIndices
36
+ } = pyramid;
37
+ const maxIndex = pyramid.dataLength - 1;
38
+ const nullLength = nullIndices?.length ?? 0;
39
+
40
+ // Builds one bucket's render indices: the ascending `candidates` (extrema/ends or the lttb points)
41
+ // with this bucket's null indices merged in, deduped, so the line breaks at gaps. `nullCursor`
42
+ // advances monotonically since buckets partition `[0, maxIndex]` left to right.
43
+ let nullCursor = 0;
44
+ const mergeBucket = (candidates, endIndex) => {
45
+ const indices = [];
46
+ let c = 0;
47
+ while (c < candidates.length || nullCursor < nullLength && nullIndices[nullCursor] <= endIndex) {
48
+ const nextCandidate = c < candidates.length ? candidates[c] : Infinity;
49
+ const nextNull = nullCursor < nullLength && nullIndices[nullCursor] <= endIndex ? nullIndices[nullCursor] : Infinity;
50
+ const next = Math.min(nextCandidate, nextNull);
51
+ if (indices[indices.length - 1] !== next) {
52
+ indices.push(next);
53
+ }
54
+ if (next === nextCandidate) {
55
+ c += 1;
56
+ }
57
+ if (next === nextNull) {
58
+ nullCursor += 1;
59
+ }
60
+ }
61
+ return Int32Array.from(indices);
62
+ };
63
+ if (algorithm === 'lttb') {
64
+ // LTTB emits one point per bucket, vs `ENVELOPE_POINTS_PER_BUCKET` for the min/max methods.
65
+ // Scale the budget by that factor so LTTB isn't sparser than them at the same zoom.
66
+ const indices = largestTriangleThreeBuckets(getValues(), Math.ceil(ENVELOPE_POINTS_PER_BUCKET * pyramid.dataLength / bucketSize));
67
+ return [{
68
+ startIndex: 0,
69
+ endIndex: maxIndex,
70
+ indices: mergeBucket(indices, maxIndex)
71
+ }];
72
+ }
73
+ const {
74
+ argMin,
75
+ argMax
76
+ } = pyramid;
77
+ const withEnds = algorithm === 'm4';
78
+ const buckets = [];
79
+ for (let j = start; j < end; j += 1) {
80
+ const bucket = j - start;
81
+ const startIndex = bucket * bucketSize;
82
+ const endIndex = Math.min(startIndex + bucketSize - 1, maxIndex);
83
+ // Extrema in ascending index order; `m4` also brackets them with the bucket's first/last.
84
+ const extremaFirst = Math.min(argMin[j], argMax[j]);
85
+ const extremaLast = Math.max(argMin[j], argMax[j]);
86
+ const candidates = withEnds ? [startIndex, extremaFirst, extremaLast, endIndex] : [extremaFirst, extremaLast];
87
+ buckets.push({
88
+ startIndex,
89
+ endIndex,
90
+ indices: mergeBucket(candidates, endIndex)
91
+ });
92
+ }
93
+ return buckets;
94
+ }
95
+
96
+ /** Shared `sample` for every series type; the per-type build feeds the channels into the pyramid. */
97
+ function sample(context) {
98
+ const {
99
+ zoom
100
+ } = context;
101
+ if (!zoom) {
102
+ return null;
103
+ }
104
+ return sampleBuckets(context.built, zoom.end - zoom.start, context.availableSize, context.minSpan, context.algorithm, context.getValues);
105
+ }
106
+
107
+ /**
108
+ * Largest-Triangle-Three-Buckets downsampling. Returns `threshold` original indices (including the
109
+ * first and last) chosen to preserve the line's visual shape. Uses the data index as the x channel.
110
+ * @see https://skemman.is/handle/1946/15343 (Sveinn Steinarsson, 2013)
111
+ */
112
+ function largestTriangleThreeBuckets(values, threshold) {
113
+ const dataLength = values.length;
114
+ if (threshold >= dataLength) {
115
+ return Int32Array.from({
116
+ length: dataLength
117
+ }, (_, i) => i);
118
+ }
119
+ if (threshold <= 2) {
120
+ // Keep just the endpoints; returning every index here would bypass the rendered-point cap.
121
+ return Int32Array.from([0, dataLength - 1]);
122
+ }
123
+ const sampled = new Int32Array(threshold);
124
+ let sampledIndex = 0;
125
+ sampled[sampledIndex] = 0; // always keep the first point
126
+ sampledIndex += 1;
127
+
128
+ // Middle buckets span indices [1, dataLength - 1); the last point is fixed separately.
129
+ const bucketSize = (dataLength - 2) / (threshold - 2);
130
+ let a = 0; // index of the previously selected point
131
+
132
+ for (let i = 0; i < threshold - 2; i += 1) {
133
+ // Average point of the next bucket (the third triangle vertex).
134
+ const avgStart = Math.floor((i + 1) * bucketSize) + 1;
135
+ let avgEnd = Math.floor((i + 2) * bucketSize) + 1;
136
+ if (avgEnd > dataLength) {
137
+ avgEnd = dataLength;
138
+ }
139
+ let avgX = 0;
140
+ let avgY = 0;
141
+ const avgCount = avgEnd - avgStart;
142
+ for (let k = avgStart; k < avgEnd; k += 1) {
143
+ avgX += k;
144
+ avgY += values[k];
145
+ }
146
+ if (avgCount > 0) {
147
+ avgX /= avgCount;
148
+ avgY /= avgCount;
149
+ } else {
150
+ avgX = dataLength - 1;
151
+ avgY = values[dataLength - 1];
152
+ }
153
+ const rangeStart = Math.floor(i * bucketSize) + 1;
154
+ const rangeEnd = Math.floor((i + 1) * bucketSize) + 1;
155
+ const ax = a;
156
+ const ay = values[a];
157
+ let maxArea = -1;
158
+ let chosen = rangeStart;
159
+ for (let k = rangeStart; k < rangeEnd; k += 1) {
160
+ const area = Math.abs((ax - avgX) * (values[k] - ay) - (ax - k) * (avgY - ay));
161
+ if (area > maxArea) {
162
+ maxArea = area;
163
+ chosen = k;
164
+ }
165
+ }
166
+ sampled[sampledIndex] = chosen;
167
+ sampledIndex += 1;
168
+ a = chosen;
169
+ }
170
+ sampled[sampledIndex] = dataLength - 1; // always keep the last point
171
+ return sampled;
172
+ }
@@ -0,0 +1,164 @@
1
+ import { selectSamplingLevel } from "./sampling.mjs";
2
+ /** Points the `minmax` envelope renders per bucket (min, max). Used to align the `lttb` budget. */
3
+ const ENVELOPE_POINTS_PER_BUCKET = 2;
4
+
5
+ /**
6
+ * The active level of detail as buckets for the current zoom, or `null` for no sampling. Each bucket
7
+ * carries its original-index span plus the ascending indices to render. The same output feeds both
8
+ * series types: lines flatten the indices into a polyline, bars draw one merged rect per bucket.
9
+ *
10
+ * `minmax` (default; bars use it) keeps {min, max} per bucket; `m4` also brackets them with the
11
+ * bucket's first/last (pixel-accurate); both read the shared `SamplingPyramid`. `lttb` ignores
12
+ * the pyramid and reduces on demand, reading raw values from `getValues` (called only for `lttb`)
13
+ * as a single whole-range bucket. Its budget matches the envelope methods' point density so it
14
+ * isn't visually sparser at the same zoom.
15
+ */
16
+ export function sampleBuckets(pyramid, currentSpan, availableSizePx, minSpan, algorithm = 'minmax', getValues) {
17
+ const level = selectSamplingLevel(pyramid, currentSpan, availableSizePx, minSpan);
18
+ if (!level) {
19
+ return null;
20
+ }
21
+ const {
22
+ bucketSize,
23
+ start,
24
+ end
25
+ } = level;
26
+ const {
27
+ nullIndices
28
+ } = pyramid;
29
+ const maxIndex = pyramid.dataLength - 1;
30
+ const nullLength = nullIndices?.length ?? 0;
31
+
32
+ // Builds one bucket's render indices: the ascending `candidates` (extrema/ends or the lttb points)
33
+ // with this bucket's null indices merged in, deduped, so the line breaks at gaps. `nullCursor`
34
+ // advances monotonically since buckets partition `[0, maxIndex]` left to right.
35
+ let nullCursor = 0;
36
+ const mergeBucket = (candidates, endIndex) => {
37
+ const indices = [];
38
+ let c = 0;
39
+ while (c < candidates.length || nullCursor < nullLength && nullIndices[nullCursor] <= endIndex) {
40
+ const nextCandidate = c < candidates.length ? candidates[c] : Infinity;
41
+ const nextNull = nullCursor < nullLength && nullIndices[nullCursor] <= endIndex ? nullIndices[nullCursor] : Infinity;
42
+ const next = Math.min(nextCandidate, nextNull);
43
+ if (indices[indices.length - 1] !== next) {
44
+ indices.push(next);
45
+ }
46
+ if (next === nextCandidate) {
47
+ c += 1;
48
+ }
49
+ if (next === nextNull) {
50
+ nullCursor += 1;
51
+ }
52
+ }
53
+ return Int32Array.from(indices);
54
+ };
55
+ if (algorithm === 'lttb') {
56
+ // LTTB emits one point per bucket, vs `ENVELOPE_POINTS_PER_BUCKET` for the min/max methods.
57
+ // Scale the budget by that factor so LTTB isn't sparser than them at the same zoom.
58
+ const indices = largestTriangleThreeBuckets(getValues(), Math.ceil(ENVELOPE_POINTS_PER_BUCKET * pyramid.dataLength / bucketSize));
59
+ return [{
60
+ startIndex: 0,
61
+ endIndex: maxIndex,
62
+ indices: mergeBucket(indices, maxIndex)
63
+ }];
64
+ }
65
+ const {
66
+ argMin,
67
+ argMax
68
+ } = pyramid;
69
+ const withEnds = algorithm === 'm4';
70
+ const buckets = [];
71
+ for (let j = start; j < end; j += 1) {
72
+ const bucket = j - start;
73
+ const startIndex = bucket * bucketSize;
74
+ const endIndex = Math.min(startIndex + bucketSize - 1, maxIndex);
75
+ // Extrema in ascending index order; `m4` also brackets them with the bucket's first/last.
76
+ const extremaFirst = Math.min(argMin[j], argMax[j]);
77
+ const extremaLast = Math.max(argMin[j], argMax[j]);
78
+ const candidates = withEnds ? [startIndex, extremaFirst, extremaLast, endIndex] : [extremaFirst, extremaLast];
79
+ buckets.push({
80
+ startIndex,
81
+ endIndex,
82
+ indices: mergeBucket(candidates, endIndex)
83
+ });
84
+ }
85
+ return buckets;
86
+ }
87
+
88
+ /** Shared `sample` for every series type; the per-type build feeds the channels into the pyramid. */
89
+ export function sample(context) {
90
+ const {
91
+ zoom
92
+ } = context;
93
+ if (!zoom) {
94
+ return null;
95
+ }
96
+ return sampleBuckets(context.built, zoom.end - zoom.start, context.availableSize, context.minSpan, context.algorithm, context.getValues);
97
+ }
98
+
99
+ /**
100
+ * Largest-Triangle-Three-Buckets downsampling. Returns `threshold` original indices (including the
101
+ * first and last) chosen to preserve the line's visual shape. Uses the data index as the x channel.
102
+ * @see https://skemman.is/handle/1946/15343 (Sveinn Steinarsson, 2013)
103
+ */
104
+ export function largestTriangleThreeBuckets(values, threshold) {
105
+ const dataLength = values.length;
106
+ if (threshold >= dataLength) {
107
+ return Int32Array.from({
108
+ length: dataLength
109
+ }, (_, i) => i);
110
+ }
111
+ if (threshold <= 2) {
112
+ // Keep just the endpoints; returning every index here would bypass the rendered-point cap.
113
+ return Int32Array.from([0, dataLength - 1]);
114
+ }
115
+ const sampled = new Int32Array(threshold);
116
+ let sampledIndex = 0;
117
+ sampled[sampledIndex] = 0; // always keep the first point
118
+ sampledIndex += 1;
119
+
120
+ // Middle buckets span indices [1, dataLength - 1); the last point is fixed separately.
121
+ const bucketSize = (dataLength - 2) / (threshold - 2);
122
+ let a = 0; // index of the previously selected point
123
+
124
+ for (let i = 0; i < threshold - 2; i += 1) {
125
+ // Average point of the next bucket (the third triangle vertex).
126
+ const avgStart = Math.floor((i + 1) * bucketSize) + 1;
127
+ let avgEnd = Math.floor((i + 2) * bucketSize) + 1;
128
+ if (avgEnd > dataLength) {
129
+ avgEnd = dataLength;
130
+ }
131
+ let avgX = 0;
132
+ let avgY = 0;
133
+ const avgCount = avgEnd - avgStart;
134
+ for (let k = avgStart; k < avgEnd; k += 1) {
135
+ avgX += k;
136
+ avgY += values[k];
137
+ }
138
+ if (avgCount > 0) {
139
+ avgX /= avgCount;
140
+ avgY /= avgCount;
141
+ } else {
142
+ avgX = dataLength - 1;
143
+ avgY = values[dataLength - 1];
144
+ }
145
+ const rangeStart = Math.floor(i * bucketSize) + 1;
146
+ const rangeEnd = Math.floor((i + 1) * bucketSize) + 1;
147
+ const ax = a;
148
+ const ay = values[a];
149
+ let maxArea = -1;
150
+ let chosen = rangeStart;
151
+ for (let k = rangeStart; k < rangeEnd; k += 1) {
152
+ const area = Math.abs((ax - avgX) * (values[k] - ay) - (ax - k) * (avgY - ay));
153
+ if (area > maxArea) {
154
+ maxArea = area;
155
+ chosen = k;
156
+ }
157
+ }
158
+ sampled[sampledIndex] = chosen;
159
+ sampledIndex += 1;
160
+ a = chosen;
161
+ }
162
+ sampled[sampledIndex] = dataLength - 1; // always keep the last point
163
+ return sampled;
164
+ }
@@ -0,0 +1,164 @@
1
+ /** Smallest element size (px) allowed before sampling kicks in. */
2
+ export const MIN_ELEMENT_SIZE_PX = 6;
3
+
4
+ /**
5
+ * Min zoom span (%) at which the data fills the view with `MIN_ELEMENT_SIZE_PX`-wide elements.
6
+ * Screen-derived, so elements never get thinner and the level count adapts to screen + data.
7
+ */
8
+ export function getSamplingMinSpan(dataLength, availableSizePx) {
9
+ return 100 * availableSizePx / (MIN_ELEMENT_SIZE_PX * dataLength);
10
+ }
11
+ const EMPTY_PYRAMID_OFFSETS = new Int32Array(1);
12
+
13
+ /**
14
+ * Level 0 (raw) extends this factor above `minSpan` before the first sampled level kicks in — a
15
+ * half-octave of wiggle room so the deepest zoom stays unsampled without a knife-edge transition.
16
+ */
17
+ const RAW_LEVEL_SPAN_FACTOR = Math.SQRT2;
18
+
19
+ /**
20
+ * Hard ceiling on rendered elements, independent of zoom. Even at the deepest zoom (where elements
21
+ * would be wide enough to skip screen sampling), a view with more than this many points is still
22
+ * sampled enough levels to get under the cap — too many elements hurts performance whatever the size.
23
+ */
24
+ export const MAX_RENDERED_POINTS = 2000;
25
+
26
+ /** Levels of sampling needed to bring `visiblePoints` under `MAX_RENDERED_POINTS` (0 if under). */
27
+ function pointCapLevel(visiblePoints) {
28
+ return visiblePoints > MAX_RENDERED_POINTS ? Math.ceil(Math.log2(visiblePoints / MAX_RENDERED_POINTS)) : 0;
29
+ }
30
+
31
+ /**
32
+ * Builds the LOD pyramid for one series from its low/high value channels (for bars: the stacked
33
+ * `[base, top]` channels; for lines: the displayed `y` for both). Each bucket keeps the original
34
+ * index of its min (over `low`) and max (over `high`) — a min/max envelope, so spikes and troughs
35
+ * survive. Consumers read the values back from their own series data by index.
36
+ *
37
+ * Stored flat: all levels concatenated into `argMin`/`argMax`, with `offsets` marking level
38
+ * boundaries. Level 0 (no merge) is implicit; bucket indices are derived (`j * bucketSize`).
39
+ */
40
+ export function buildSamplingPyramid(low, high) {
41
+ const dataLength = low.length;
42
+ if (dataLength <= 1) {
43
+ return {
44
+ dataLength,
45
+ argMin: new Int32Array(0),
46
+ argMax: new Int32Array(0),
47
+ offsets: EMPTY_PYRAMID_OFFSETS
48
+ };
49
+ }
50
+ const levelCount = Math.ceil(Math.log2(dataLength));
51
+
52
+ // Level start offsets into the shared buffers; each level halves the previous count (round up).
53
+ const offsets = new Int32Array(levelCount + 1);
54
+ for (let levelIndex = 0, count = dataLength; levelIndex < levelCount; levelIndex += 1) {
55
+ count = Math.ceil(count / 2);
56
+ offsets[levelIndex + 1] = offsets[levelIndex] + count;
57
+ }
58
+ const argMin = new Int32Array(offsets[levelCount]);
59
+ const argMax = new Int32Array(offsets[levelCount]);
60
+
61
+ // Level 0 (bucketSize 2) merges adjacent raw points; the only level that scans raw data.
62
+ for (let j = 0, i = 0; i < dataLength; j += 1, i += 2) {
63
+ const next = i + 1;
64
+ let lo = i;
65
+ let hi = i;
66
+ if (next < dataLength) {
67
+ if (low[next] < low[i]) {
68
+ lo = next;
69
+ }
70
+ if (high[next] > high[i]) {
71
+ hi = next;
72
+ }
73
+ }
74
+ argMin[j] = lo;
75
+ argMax[j] = hi;
76
+ }
77
+
78
+ // Higher levels merge pairs of buckets from the level below (argMin/argMax are associative via
79
+ // value compare, so the result equals re-scanning raw data). n/2 + n/4 + ... → O(n) build.
80
+ for (let levelIndex = 1; levelIndex < levelCount; levelIndex += 1) {
81
+ const prevStart = offsets[levelIndex - 1];
82
+ const prevCount = offsets[levelIndex] - prevStart;
83
+ const start = offsets[levelIndex];
84
+ for (let i = 0, j = 0; i < prevCount; i += 2, j += 1) {
85
+ const a = prevStart + i;
86
+ const w = start + j;
87
+ if (i + 1 >= prevCount) {
88
+ argMin[w] = argMin[a];
89
+ argMax[w] = argMax[a];
90
+ } else {
91
+ const b = a + 1;
92
+ argMin[w] = low[argMin[a]] <= low[argMin[b]] ? argMin[a] : argMin[b];
93
+ argMax[w] = high[argMax[a]] >= high[argMax[b]] ? argMax[a] : argMax[b];
94
+ }
95
+ }
96
+ }
97
+ return {
98
+ dataLength,
99
+ argMin,
100
+ argMax,
101
+ offsets
102
+ };
103
+ }
104
+
105
+ /** Number of stored levels (finest `bucketSize 2` to coarsest). */
106
+ export function getSamplingLevelCount(pyramid) {
107
+ return pyramid.offsets.length - 1;
108
+ }
109
+
110
+ /**
111
+ * Level-of-detail index for the current zoom:
112
+ * - Level 0 (no sampling, raw) at the deepest zoom (`currentSpan <= minSpan * RAW_LEVEL_SPAN_FACTOR`),
113
+ * whatever the bar size — fully zoomed in shows every point, up to `MAX_RENDERED_POINTS`.
114
+ * - Above that, the level is screen-defined: bucket size keeps elements at least
115
+ * `MIN_ELEMENT_SIZE_PX` wide (+1 per span doubling past that pixel threshold).
116
+ * - The `MAX_RENDERED_POINTS` cap applies at every zoom: a view with too many points is always
117
+ * sampled, even in the raw zone where the screen rule alone would render it untouched.
118
+ */
119
+ function levelIndexFor(currentSpan, dataLength, availableSizePx, minSpan) {
120
+ if (!(currentSpan > 0)) {
121
+ return 0;
122
+ }
123
+ const capLevel = pointCapLevel(dataLength * Math.min(currentSpan, 100) / 100);
124
+ if (currentSpan <= minSpan * RAW_LEVEL_SPAN_FACTOR) {
125
+ return capLevel; // raw zone, but still bounded by the point cap
126
+ }
127
+ const screenMinSpan = getSamplingMinSpan(dataLength, availableSizePx);
128
+ if (!(screenMinSpan > 0)) {
129
+ return capLevel;
130
+ }
131
+ return Math.max(capLevel, Math.round(Math.log2(currentSpan / screenMinSpan)));
132
+ }
133
+
134
+ /**
135
+ * Picks the active level from the zoom span: level 0 (deepest zoom, `span <= minSpan`, or elements
136
+ * still wide enough) → no sampling (`null`, raw 1:1); each level up → bucket size ×2, chosen so
137
+ * sampled elements stay at least `MIN_ELEMENT_SIZE_PX` wide. Clamped to the pyramid depth.
138
+ * `bucketSize === 2 ** clampedLevel`, level buckets `offsets[clampedLevel - 1 .. clampedLevel]`.
139
+ */
140
+ export function selectSamplingLevel(pyramid, currentSpan, availableSizePx, minSpan) {
141
+ const levelCount = pyramid.offsets.length - 1;
142
+ const levelIndex = levelIndexFor(currentSpan, pyramid.dataLength, availableSizePx, minSpan);
143
+ if (levelIndex <= 0 || levelCount === 0) {
144
+ return null;
145
+ }
146
+ const clampedLevel = Math.min(levelIndex, levelCount);
147
+ return {
148
+ bucketSize: 2 ** clampedLevel,
149
+ start: pyramid.offsets[clampedLevel - 1],
150
+ end: pyramid.offsets[clampedLevel]
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Active bucket size (`1` = no sampling) for an axis with no built pyramid (the axis highlight).
156
+ * Matches `selectSamplingLevel`'s `bucketSize`, derived from `dataLength` + screen + `minSpan`.
157
+ */
158
+ export function getSamplingBucketSize(currentSpan, dataLength, availableSizePx, minSpan) {
159
+ const levelIndex = levelIndexFor(currentSpan, dataLength, availableSizePx, minSpan);
160
+ if (levelIndex <= 0 || dataLength <= 1) {
161
+ return 1;
162
+ }
163
+ return 2 ** Math.min(levelIndex, Math.ceil(Math.log2(dataLength)));
164
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The active level of detail for the current zoom: the slice `[start, end)` of the pyramid's
3
+ * `argMin`/`argMax` arrays plus its bucket size. Bucket `j` (`j` from `0`) covers indices
4
+ * `[j * bucketSize, min((j + 1) * bucketSize - 1, dataLength - 1)]`.
5
+ */
6
+ export interface ActiveSamplingLevel {
7
+ /** Elements merged per bucket (power of two, `>= 2`). */
8
+ bucketSize: number;
9
+ /** Start offset into `argMin`/`argMax`. */
10
+ start: number;
11
+ /** End offset (exclusive) into `argMin`/`argMax`. */
12
+ end: number;
13
+ }
14
+ /**
15
+ * Precomputed LOD pyramid for one series, shared by every chart type and sampling method.
16
+ * Stored as flat typed arrays (no per-level or per-bucket objects): for each bucket it keeps the
17
+ * original index of the minimum (`argMin`, over the low channel) and the maximum (`argMax`, over
18
+ * the high channel). Consumers read the values back from their own series data by index.
19
+ *
20
+ * All levels are concatenated finest (`bucketSize 2`) to coarsest; `offsets[i]..offsets[i + 1]` is
21
+ * level `i` (bucketSize `2 ** (i + 1)`), and there are `offsets.length - 1` levels.
22
+ */
23
+ export interface SamplingPyramid {
24
+ dataLength: number;
25
+ /** Index of the per-bucket minimum (low channel), concatenated finest to coarsest. */
26
+ argMin: Int32Array;
27
+ /** Index of the per-bucket maximum (high channel), same order. */
28
+ argMax: Int32Array;
29
+ /** Level start offsets into `argMin`/`argMax`; length `levelCount + 1`. */
30
+ offsets: Int32Array;
31
+ /** Ascending null-point indices, merged back into the output as line breaks. Absent without nulls. */
32
+ nullIndices?: Int32Array;
33
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The active level of detail for the current zoom: the slice `[start, end)` of the pyramid's
3
+ * `argMin`/`argMax` arrays plus its bucket size. Bucket `j` (`j` from `0`) covers indices
4
+ * `[j * bucketSize, min((j + 1) * bucketSize - 1, dataLength - 1)]`.
5
+ */
6
+ export interface ActiveSamplingLevel {
7
+ /** Elements merged per bucket (power of two, `>= 2`). */
8
+ bucketSize: number;
9
+ /** Start offset into `argMin`/`argMax`. */
10
+ start: number;
11
+ /** End offset (exclusive) into `argMin`/`argMax`. */
12
+ end: number;
13
+ }
14
+ /**
15
+ * Precomputed LOD pyramid for one series, shared by every chart type and sampling method.
16
+ * Stored as flat typed arrays (no per-level or per-bucket objects): for each bucket it keeps the
17
+ * original index of the minimum (`argMin`, over the low channel) and the maximum (`argMax`, over
18
+ * the high channel). Consumers read the values back from their own series data by index.
19
+ *
20
+ * All levels are concatenated finest (`bucketSize 2`) to coarsest; `offsets[i]..offsets[i + 1]` is
21
+ * level `i` (bucketSize `2 ** (i + 1)`), and there are `offsets.length - 1` levels.
22
+ */
23
+ export interface SamplingPyramid {
24
+ dataLength: number;
25
+ /** Index of the per-bucket minimum (low channel), concatenated finest to coarsest. */
26
+ argMin: Int32Array;
27
+ /** Index of the per-bucket maximum (high channel), same order. */
28
+ argMax: Int32Array;
29
+ /** Level start offsets into `argMin`/`argMax`; length `levelCount + 1`. */
30
+ offsets: Int32Array;
31
+ /** Ascending null-point indices, merged back into the output as line breaks. Absent without nulls. */
32
+ nullIndices?: Int32Array;
33
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
@@ -0,0 +1,4 @@
1
+ import type { ChartPlugin } from '@mui/x-charts/internals';
2
+ import type { UseChartProSamplingSignature } from "./useChartProSampling.types.mjs";
3
+ /** Toggles sampling. Pyramids and level selection live in community selectors. */
4
+ export declare const useChartProSampling: ChartPlugin<UseChartProSamplingSignature>;
@@ -0,0 +1,4 @@
1
+ import type { ChartPlugin } from '@mui/x-charts/internals';
2
+ import type { UseChartProSamplingSignature } from "./useChartProSampling.types.js";
3
+ /** Toggles sampling. Pyramids and level selection live in community selectors. */
4
+ export declare const useChartProSampling: ChartPlugin<UseChartProSamplingSignature>;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ 'use client';
3
+
4
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.useChartProSampling = void 0;
9
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
10
+ var _useEffectAfterFirstRender = require("@mui/x-internals/useEffectAfterFirstRender");
11
+ var _isDeepEqual = require("@mui/x-internals/isDeepEqual");
12
+ /** Toggles sampling. Pyramids and level selection live in community selectors. */
13
+ const useChartProSampling = ({
14
+ params,
15
+ store
16
+ }) => {
17
+ const {
18
+ sampling
19
+ } = params;
20
+ (0, _useEffectAfterFirstRender.useEffectAfterFirstRender)(() => {
21
+ // Skip the update when unchanged so an inlined `sampling` config (a new object each render, e.g.
22
+ // in composition) doesn't replace the state and force the pyramids to recompute every render.
23
+ const next = toSamplingState(sampling);
24
+ if (!(0, _isDeepEqual.isDeepEqual)(store.state.sampling, next)) {
25
+ store.set('sampling', next);
26
+ }
27
+ }, [sampling, store]);
28
+ return {};
29
+ };
30
+
31
+ /** Maps the per-series-type `sampling` config to the internal state (enabled methods per type). */
32
+ exports.useChartProSampling = useChartProSampling;
33
+ function toSamplingState(sampling = {}) {
34
+ const methods = {};
35
+ // Iterate generically so a new series type only needs its `sampling` config entry, no edit here.
36
+ Object.keys(sampling).forEach(seriesType => {
37
+ const method = sampling[seriesType];
38
+ if (method && method !== 'none') {
39
+ methods[seriesType] = method;
40
+ }
41
+ });
42
+ return {
43
+ enabled: Object.keys(methods).length > 0,
44
+ methods
45
+ };
46
+ }
47
+ useChartProSampling.params = {
48
+ sampling: true
49
+ };
50
+ useChartProSampling.getDefaultizedParams = ({
51
+ params
52
+ }) => (0, _extends2.default)({}, params, {
53
+ sampling: params.sampling ?? {}
54
+ });
55
+ useChartProSampling.getInitialState = ({
56
+ sampling
57
+ }) => ({
58
+ sampling: toSamplingState(sampling)
59
+ });