@geotab/zenith 1.26.3 → 1.26.4-beta.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 (99) hide show
  1. package/README.md +6 -0
  2. package/dist/chart/accessibleChart/accessibleChart.d.ts +4 -0
  3. package/dist/chart/accessibleChart/accessibleChart.js +9 -0
  4. package/dist/chart/accessibleChart/accessibleChartNarrative.d.ts +7 -0
  5. package/dist/chart/accessibleChart/accessibleChartNarrative.js +605 -0
  6. package/dist/chart/accessibleChart/accessibleChartTable.d.ts +3 -0
  7. package/dist/chart/accessibleChart/accessibleChartTable.js +107 -0
  8. package/dist/chart/accessibleChart/interfaces.d.ts +48 -0
  9. package/dist/chart/accessibleChart/interfaces.js +2 -0
  10. package/dist/chart/barChart/getDefaultDatasetStyle.d.ts +5 -5
  11. package/dist/chart/barChart.js +2 -1
  12. package/dist/chart/chart.d.ts +8 -5
  13. package/dist/chart/chart.js +8 -7
  14. package/dist/chart/lineChart/getDefaultDatasetStyle.d.ts +9 -9
  15. package/dist/chart/lineChart/interfaces.d.ts +1 -1
  16. package/dist/chart/lineChart.js +2 -1
  17. package/dist/chart/pieChart/getDefaultDatasetStyle.d.ts +7 -7
  18. package/dist/chart/pieChart.js +2 -1
  19. package/dist/chart/utils/calculateMinTruncationPrecision.d.ts +1 -0
  20. package/dist/chart/utils/calculateMinTruncationPrecision.js +25 -0
  21. package/dist/chart/utils/convertDates.d.ts +1 -1
  22. package/dist/chart/utils/getFormattedLabel.d.ts +3 -0
  23. package/dist/chart/utils/getFormattedLabel.js +20 -0
  24. package/dist/chart/utils/removeTrendDataFromDatasets.d.ts +2 -0
  25. package/dist/chart/utils/removeTrendDataFromDatasets.js +11 -0
  26. package/dist/chart/utils/resampleArrayEvenly.d.ts +1 -0
  27. package/dist/chart/utils/resampleArrayEvenly.js +23 -0
  28. package/dist/dataFeed/dataFeed.d.ts +2 -1
  29. package/dist/dataFeed/dataFeed.js +3 -3
  30. package/dist/dataFeed/dataFeedColumnsItems.d.ts +2 -1
  31. package/dist/dataFeed/dataFeedColumnsItems.js +23 -4
  32. package/dist/dataGrid/columns/actionsColumn/actionsColumn.d.ts +1 -0
  33. package/dist/dataGrid/columns/actionsColumn/actionsColumn.js +3 -0
  34. package/dist/index.css +25 -6
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +29 -15
  37. package/dist/lineChart/utils.d.ts +9 -9
  38. package/dist/lineChartMini/lineChartMini.js +2 -1
  39. package/dist/list/hooks/useVirtualScroll.js +3 -0
  40. package/dist/menu/components/menuErrorItem.d.ts +7 -0
  41. package/dist/menu/components/menuErrorItem.js +12 -0
  42. package/dist/nav/navHeader/navHeader.d.ts +3 -3
  43. package/dist/nav/navHeader/navHeader.js +8 -3
  44. package/dist/nav/navItem/navItem.d.ts +3 -2
  45. package/dist/nav/navItem/navItem.js +3 -3
  46. package/dist/nav/navSection/navSection.js +1 -4
  47. package/dist/nav/utils/navUtils.d.ts +2 -0
  48. package/dist/nav/utils/navUtils.js +15 -1
  49. package/dist/stepperRaw/utils/calculateWithPrecision.js +1 -1
  50. package/dist/table/actions/actionsMenu.d.ts +2 -1
  51. package/dist/table/actions/actionsMenu.js +42 -28
  52. package/dist/table/actions/useActions.d.ts +6 -0
  53. package/dist/table/actions/useActions.js +3 -1
  54. package/dist/table/table.js +2 -2
  55. package/dist/textIconButton/textIconButton.d.ts +1 -0
  56. package/dist/utils/localization/translations/cs-json.d.ts +3 -0
  57. package/dist/utils/localization/translations/cs-json.js +3 -0
  58. package/dist/utils/localization/translations/de-json.d.ts +3 -0
  59. package/dist/utils/localization/translations/de-json.js +3 -0
  60. package/dist/utils/localization/translations/en-json.d.ts +18 -0
  61. package/dist/utils/localization/translations/en-json.js +19 -1
  62. package/dist/utils/localization/translations/es-json.d.ts +3 -0
  63. package/dist/utils/localization/translations/es-json.js +3 -0
  64. package/dist/utils/localization/translations/fr-FR-json.d.ts +3 -0
  65. package/dist/utils/localization/translations/fr-FR-json.js +3 -0
  66. package/dist/utils/localization/translations/fr-json.d.ts +3 -0
  67. package/dist/utils/localization/translations/fr-json.js +3 -0
  68. package/dist/utils/localization/translations/id-json.d.ts +3 -0
  69. package/dist/utils/localization/translations/id-json.js +3 -0
  70. package/dist/utils/localization/translations/it-json.d.ts +3 -0
  71. package/dist/utils/localization/translations/it-json.js +3 -0
  72. package/dist/utils/localization/translations/ja-json.d.ts +3 -0
  73. package/dist/utils/localization/translations/ja-json.js +3 -0
  74. package/dist/utils/localization/translations/ko-KR-json.d.ts +3 -0
  75. package/dist/utils/localization/translations/ko-KR-json.js +3 -0
  76. package/dist/utils/localization/translations/ms-json.d.ts +3 -0
  77. package/dist/utils/localization/translations/ms-json.js +3 -0
  78. package/dist/utils/localization/translations/nl-json.d.ts +3 -0
  79. package/dist/utils/localization/translations/nl-json.js +3 -0
  80. package/dist/utils/localization/translations/pl-json.d.ts +3 -0
  81. package/dist/utils/localization/translations/pl-json.js +3 -0
  82. package/dist/utils/localization/translations/pt-BR-json.d.ts +3 -0
  83. package/dist/utils/localization/translations/pt-BR-json.js +3 -0
  84. package/dist/utils/localization/translations/sv-json.d.ts +3 -0
  85. package/dist/utils/localization/translations/sv-json.js +3 -0
  86. package/dist/utils/localization/translations/th-json.d.ts +3 -0
  87. package/dist/utils/localization/translations/th-json.js +3 -0
  88. package/dist/utils/localization/translations/tr-json.d.ts +3 -0
  89. package/dist/utils/localization/translations/tr-json.js +3 -0
  90. package/dist/utils/localization/translations/zh-Hans-json.d.ts +3 -0
  91. package/dist/utils/localization/translations/zh-Hans-json.js +3 -0
  92. package/dist/utils/localization/translations/zh-TW-json.d.ts +1 -0
  93. package/dist/utils/localization/translations/zh-TW-json.js +1 -0
  94. package/dist/utils/positioningUtils/calculatePosition.js +2 -1
  95. package/dist/utils/truncateDecimals.d.ts +1 -0
  96. package/dist/utils/truncateDecimals.js +12 -0
  97. package/package.json +4 -4
  98. /package/dist/{stepperRaw/utils → utils}/countDecimals.d.ts +0 -0
  99. /package/dist/{stepperRaw/utils → utils}/countDecimals.js +0 -0
package/README.md CHANGED
@@ -40,6 +40,12 @@ Zenith library provides components defined in Zenith Design System. It includes
40
40
 
41
41
  ## Change log
42
42
 
43
+ ### 1.26.4
44
+
45
+ * Improve accessibility of `Chart`
46
+ * Delayed loading of `Table` actions on mobile
47
+ * Improve positioning of `Popup` in edge cases
48
+
43
49
  ### 1.26.3
44
50
 
45
51
  * Update the title of the dropdown trigger button
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ import { IAccessibleChart } from "./interfaces";
3
+ import "./accessibleChart.less";
4
+ export declare const AccessibleChart: React.FC<IAccessibleChart>;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessibleChart = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const userFormatProvider_1 = require("../../utils/userFormat/userFormatProvider");
6
+ const accessibleChartNarrative_1 = require("./accessibleChartNarrative");
7
+ const accessibleChartTable_1 = require("./accessibleChartTable");
8
+ const AccessibleChart = (props) => (0, jsx_runtime_1.jsxs)(userFormatProvider_1.UserFormatProvider, { dateFormat: "d MMMM yyyy", children: [props.type !== "pie" && (0, jsx_runtime_1.jsx)(accessibleChartNarrative_1.AccessibleChartNarrative, Object.assign({}, props)), (0, jsx_runtime_1.jsx)(accessibleChartTable_1.AccessibleChartTable, Object.assign({}, props))] });
9
+ exports.AccessibleChart = AccessibleChart;
@@ -0,0 +1,7 @@
1
+ /// <reference types="react" />
2
+ import { AccessibleChartDataset, IAccessibleChart, IAccessiblePoint } from "./interfaces";
3
+ import { IBarChartOptions } from "../barChart/interfaces";
4
+ import { ILineChartOptions, ILineChartPoint } from "../lineChart/interfaces";
5
+ export declare function collectAllPoints(dataset: AccessibleChartDataset, dateFormatter: (d: Date | string, format?: string) => string, miniChartLabels?: string[], options?: ILineChartOptions | IBarChartOptions): IAccessiblePoint[];
6
+ export declare function simplifyData(data: ILineChartPoint[]): ILineChartPoint[];
7
+ export declare const AccessibleChartNarrative: React.FC<IAccessibleChart>;
@@ -0,0 +1,605 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessibleChartNarrative = exports.simplifyData = exports.collectAllPoints = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const useLanguage_1 = require("../../utils/localization/useLanguage");
6
+ const react_1 = require("react");
7
+ const userFormatContext_1 = require("../../utils/userFormat/userFormatContext");
8
+ const formatDate_1 = require("../../utils/formatDate");
9
+ const isDateString_1 = require("../utils/isDateString");
10
+ const getFormattedLabel_1 = require("../utils/getFormattedLabel");
11
+ const truncateDecimals_1 = require("../../utils/truncateDecimals");
12
+ const calculateMinTruncationPrecision_1 = require("../utils/calculateMinTruncationPrecision");
13
+ const removeTrendDataFromDatasets_1 = require("../utils/removeTrendDataFromDatasets");
14
+ function collectAllPoints(dataset, dateFormatter, miniChartLabels, options) {
15
+ if (dataset.data.length === 0) {
16
+ return [];
17
+ }
18
+ // Minichart data processing
19
+ if (miniChartLabels) {
20
+ return dataset.data.map((value, index) => ({
21
+ x: index,
22
+ y: typeof value === "number" ? value : 0,
23
+ originX: index === 0 ? miniChartLabels[0] : (index === dataset.data.length - 1 ? miniChartLabels[miniChartLabels.length - 1] : index)
24
+ }));
25
+ }
26
+ // Standard chart data processing
27
+ const allPoints = [];
28
+ for (const [index, point] of dataset.data.entries()) {
29
+ if (typeof point !== "object" || point === null || !("y" in point)) {
30
+ continue;
31
+ }
32
+ let xNumeric;
33
+ let originX;
34
+ const rawXValue = point.x;
35
+ if (rawXValue instanceof Date || (typeof rawXValue === "string" && (0, isDateString_1.isDateString)(rawXValue))) {
36
+ xNumeric = new Date(rawXValue).getTime();
37
+ originX = (0, getFormattedLabel_1.getFormattedLabel)(rawXValue, dateFormatter, options);
38
+ }
39
+ else if (typeof rawXValue === "number") {
40
+ xNumeric = rawXValue;
41
+ originX = rawXValue;
42
+ }
43
+ else {
44
+ xNumeric = index;
45
+ originX = rawXValue;
46
+ }
47
+ allPoints.push({ x: xNumeric, y: point.y || 0, originX });
48
+ }
49
+ return allPoints;
50
+ }
51
+ exports.collectAllPoints = collectAllPoints;
52
+ function simplifyData(data) {
53
+ // Need at least 3 points to find a peak or valley
54
+ if (data.length < 3) {
55
+ return data;
56
+ }
57
+ // Calculate Potential Peak/Valley Height and Dominance for Every Point
58
+ const scoredData = data.map((currentPoint, i) => {
59
+ if (currentPoint.y === null) {
60
+ return {
61
+ point: currentPoint,
62
+ peakHeight: 0,
63
+ valleyHeight: 0,
64
+ peakDominance: 0,
65
+ valleyDominance: 0
66
+ };
67
+ }
68
+ let peakHeight = 0;
69
+ let valleyHeight = 0;
70
+ let peakDominance = 0;
71
+ let valleyDominance = 0;
72
+ let localMinForPeak = currentPoint.y;
73
+ // Scan left
74
+ for (let j = i - 1; j >= 0; j--) {
75
+ const point = data[j];
76
+ if (point.y !== null && point.y < currentPoint.y) {
77
+ localMinForPeak = Math.min(localMinForPeak, point.y);
78
+ peakDominance++;
79
+ }
80
+ else {
81
+ break;
82
+ }
83
+ }
84
+ // Scan right
85
+ for (let j = i + 1; j < data.length; j++) {
86
+ const point = data[j];
87
+ if (point.y !== null && point.y < currentPoint.y) {
88
+ localMinForPeak = Math.min(localMinForPeak, point.y);
89
+ peakDominance++;
90
+ }
91
+ else {
92
+ break;
93
+ }
94
+ }
95
+ if (peakDominance > 0) {
96
+ peakHeight = currentPoint.y - localMinForPeak;
97
+ }
98
+ // Calculate Valley Height & Dominance
99
+ let localMaxForValley = currentPoint.y;
100
+ // Scan left
101
+ for (let j = i - 1; j >= 0; j--) {
102
+ const point = data[j];
103
+ if (point.y !== null && point.y > currentPoint.y) {
104
+ localMaxForValley = Math.max(localMaxForValley, point.y);
105
+ valleyDominance++;
106
+ }
107
+ else {
108
+ break;
109
+ }
110
+ }
111
+ // Scan right
112
+ for (let j = i + 1; j < data.length; j++) {
113
+ const point = data[j];
114
+ if (point.y !== null && point.y > currentPoint.y) {
115
+ localMaxForValley = Math.max(localMaxForValley, point.y);
116
+ valleyDominance++;
117
+ }
118
+ else {
119
+ break;
120
+ }
121
+ }
122
+ if (valleyDominance > 0) {
123
+ valleyHeight = localMaxForValley - currentPoint.y;
124
+ }
125
+ return { point: currentPoint, peakHeight, valleyHeight, peakDominance, valleyDominance };
126
+ });
127
+ // Filter points by trend change and relative height
128
+ const significantExtremums = [];
129
+ const strengthThreshold = 0.50;
130
+ const yValues = data.filter(p => p.y !== null).map(p => p.y);
131
+ const minY = yValues.reduce((min, current) => Math.min(min, current));
132
+ const maxY = yValues.reduce((max, current) => Math.max(max, current));
133
+ const datasetRange = maxY - minY;
134
+ // Proceed only if there is a range to avoid division by zero
135
+ if (datasetRange > 0) {
136
+ for (let i = 1; i < scoredData.length - 1; i++) {
137
+ const prev = scoredData[i - 1];
138
+ const current = scoredData[i];
139
+ const next = scoredData[i + 1];
140
+ // Check for a peak in the 'peakHeight' values
141
+ const isPeak = current.peakHeight > prev.peakHeight && current.peakHeight > next.peakHeight;
142
+ // Check for a peak in the 'valleyHeight' values
143
+ const isValley = current.valleyHeight > prev.valleyHeight && current.valleyHeight > next.valleyHeight;
144
+ if (isPeak) {
145
+ const relHeight = current.peakHeight / datasetRange;
146
+ if (relHeight >= strengthThreshold && current.peakDominance > 2) {
147
+ significantExtremums.push(current.point);
148
+ }
149
+ }
150
+ else if (isValley) {
151
+ const relHeight = current.valleyHeight / datasetRange;
152
+ if (relHeight >= strengthThreshold && current.valleyDominance > 2) {
153
+ significantExtremums.push(current.point);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ const uniquePointsSet = new Set();
159
+ const finalResult = [];
160
+ const addPointIfUnique = (point) => {
161
+ // using a composite key for uniqueness
162
+ const key = `${point.x.toString()}-${point.y || ""}`;
163
+ if (!uniquePointsSet.has(key)) {
164
+ uniquePointsSet.add(key);
165
+ finalResult.push(point);
166
+ }
167
+ };
168
+ // Add key points to the array
169
+ addPointIfUnique(data[0]);
170
+ addPointIfUnique(data[data.length - 1]);
171
+ const globalMinPoint = data.find(p => p.y === minY);
172
+ if (globalMinPoint) {
173
+ addPointIfUnique(globalMinPoint);
174
+ }
175
+ const globalMaxPoint = data.find(p => p.y === maxY);
176
+ if (globalMaxPoint) {
177
+ addPointIfUnique(globalMaxPoint);
178
+ }
179
+ // Add other significant extremums
180
+ significantExtremums.forEach(addPointIfUnique);
181
+ // Sort the final result array chronologically
182
+ finalResult.sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
183
+ // Filter out non-significant points based on area under the curve
184
+ const getSquare = (p1, p2) => {
185
+ const x1 = new Date(p1.x).getTime();
186
+ const x2 = new Date(p2.x).getTime();
187
+ const y1 = p1.y;
188
+ const y2 = p2.y;
189
+ return (x2 - x1) * (y1 + (y2 - y1) / 2);
190
+ };
191
+ const immutablePoints = [data[0], data[data.length - 1], globalMinPoint, globalMaxPoint].filter(Boolean);
192
+ for (let i = 1; i < finalResult.length - 1; i++) {
193
+ const current = finalResult[i];
194
+ // Skip removal if the current point is one of the designated immutable points
195
+ if (immutablePoints.some(p => p && p.x === current.x && p.y === current.y)) {
196
+ continue;
197
+ }
198
+ const prev = finalResult[i - 1];
199
+ const next = finalResult[i + 1];
200
+ const squareWithPoint = getSquare(prev, current) + getSquare(current, next);
201
+ const squareWithoutPoint = getSquare(prev, next);
202
+ const squares = [squareWithPoint, squareWithoutPoint].sort((a, b) => a - b);
203
+ const squareRatio = squares[0] / squares[1];
204
+ if (squareRatio > 0.9) {
205
+ // remove insignificant point
206
+ finalResult.splice(i, 1);
207
+ i--;
208
+ }
209
+ }
210
+ return finalResult;
211
+ }
212
+ exports.simplifyData = simplifyData;
213
+ function getTrendDirectionBySlope(slope) {
214
+ const slopeThreshold = 1e-10;
215
+ if (slope > slopeThreshold) {
216
+ return "increases";
217
+ }
218
+ if (slope < -slopeThreshold) {
219
+ return "decreases";
220
+ }
221
+ return "flat";
222
+ }
223
+ function calculateLinearRegression(points) {
224
+ if (points.length < 2) {
225
+ return { slope: NaN, yIntercept: NaN };
226
+ }
227
+ const sumX = points.reduce((sum, p) => sum + p.x, 0);
228
+ const sumY = points.reduce((sum, p) => sum + p.y, 0);
229
+ const meanX = sumX / points.length;
230
+ const meanY = sumY / points.length;
231
+ let numerator = 0;
232
+ let denominator = 0;
233
+ for (const p of points) {
234
+ numerator += (p.x - meanX) * (p.y - meanY);
235
+ denominator += (p.x - meanX) * (p.x - meanX);
236
+ }
237
+ const slope = denominator === 0 ? 0 : numerator / denominator;
238
+ const yIntercept = meanY - slope * meanX;
239
+ return { slope, yIntercept };
240
+ }
241
+ function detectTwoStateChart(dataset, translate, dateFormatter, miniChartLabels, options) {
242
+ const allPoints = collectAllPoints(dataset, dateFormatter, miniChartLabels, options);
243
+ if (allPoints.length <= 2) {
244
+ return false;
245
+ }
246
+ const datasetName = dataset.label || translate("Data");
247
+ const uniqueYValues = new Set();
248
+ const tolerance = 1e-6;
249
+ for (const point of allPoints) {
250
+ uniqueYValues.add(point.y);
251
+ if (uniqueYValues.size > 2) {
252
+ return false;
253
+ }
254
+ }
255
+ const isTwoState = uniqueYValues.size === 2;
256
+ if (!isTwoState) {
257
+ return false;
258
+ }
259
+ // Count state transitions
260
+ let transitions = 0;
261
+ for (let i = 1; i < allPoints.length; i++) {
262
+ const prevValue = allPoints[i - 1].y;
263
+ const currentValue = allPoints[i].y;
264
+ if (Math.abs(prevValue - currentValue) > tolerance) {
265
+ transitions++;
266
+ }
267
+ }
268
+ const [val1, val2] = uniqueYValues;
269
+ const decimalLength = (0, calculateMinTruncationPrecision_1.calculateMinTruncationPrecision)([val1, val2]);
270
+ const summary = `${datasetName} ${translate("switched {TRANSITIONS} times between two states: {STATE1} and {STATE2}.")
271
+ .replace("{STATE1}", (0, truncateDecimals_1.truncateDecimals)(val1, decimalLength).toString())
272
+ .replace("{STATE2}", (0, truncateDecimals_1.truncateDecimals)(val2, decimalLength).toString())
273
+ .replace("{TRANSITIONS}", transitions.toString())}`;
274
+ return summary;
275
+ }
276
+ // Calculates the Median Absolute Deviation (MAD) for a given array of numbers.
277
+ function calculateMAD(data, scaleFactor = 1.4826) {
278
+ if (data.length === 0) {
279
+ return 0;
280
+ }
281
+ // Sort the data to find the median
282
+ const sortedData = [...data].sort((a, b) => a - b);
283
+ const mid = Math.floor(sortedData.length / 2);
284
+ let median;
285
+ if (sortedData.length % 2 === 0) {
286
+ median = (sortedData[mid - 1] + sortedData[mid]) / 2;
287
+ }
288
+ else {
289
+ median = sortedData[mid];
290
+ }
291
+ // Calculate absolute deviations from the median
292
+ const deviations = sortedData.map(d => Math.abs(d - median));
293
+ let mad;
294
+ const madMid = Math.floor(deviations.length / 2);
295
+ if (deviations.length % 2 === 0) {
296
+ mad = (deviations[madMid - 1] + deviations[madMid]) / 2;
297
+ }
298
+ else {
299
+ mad = deviations[madMid];
300
+ }
301
+ return mad * scaleFactor;
302
+ }
303
+ function getInliersForSmallSet(allPoints) {
304
+ // Only proceed if there are enough points to perform exclusion
305
+ if (allPoints.length < 3) {
306
+ return allPoints;
307
+ }
308
+ const initialLr = calculateLinearRegression(allPoints);
309
+ const initialResiduals = allPoints.map((point) => ({
310
+ point,
311
+ residual: Math.abs(point.y - (initialLr.slope * point.x + initialLr.yIntercept))
312
+ }));
313
+ // Sort to put the worst-fitting point (largest residual) first
314
+ initialResiduals.sort((a, b) => b.residual - a.residual);
315
+ // Exclude the top residual (the potential outlier) and return the rest as inliers.
316
+ return initialResiduals.slice(1).map(r => r.point);
317
+ }
318
+ function getInliersForLargeSet(allPoints, initialLr) {
319
+ const initialResiduals = allPoints.map(point => {
320
+ const predictedY = initialLr.slope * point.x + initialLr.yIntercept;
321
+ return point.y - predictedY;
322
+ });
323
+ const madOfInitialResiduals = calculateMAD(initialResiduals);
324
+ const strictInlierFilter = 1.5;
325
+ const pointsForRobustLr = [];
326
+ if (madOfInitialResiduals > 1e-9) {
327
+ for (let i = 0; i < allPoints.length; i++) {
328
+ if (Math.abs(initialResiduals[i]) <= strictInlierFilter * madOfInitialResiduals) {
329
+ pointsForRobustLr.push(allPoints[i]);
330
+ }
331
+ }
332
+ }
333
+ else {
334
+ // If no spread, use all points.
335
+ pointsForRobustLr.push(...allPoints);
336
+ }
337
+ return pointsForRobustLr;
338
+ }
339
+ function detectSignificantDeviations(allPoints, translate, decimalLength, deviationThreshold = 2) {
340
+ if (allPoints.length < 3) {
341
+ return null;
342
+ }
343
+ let pointsForRobustLr = [];
344
+ let initialLr = null;
345
+ // If the dataset is tiny (where MAD-based filtering is most fragile)
346
+ if (allPoints.length <= 5) {
347
+ pointsForRobustLr = getInliersForSmallSet(allPoints);
348
+ }
349
+ else {
350
+ initialLr = calculateLinearRegression(allPoints);
351
+ pointsForRobustLr = getInliersForLargeSet(allPoints, initialLr);
352
+ }
353
+ let finalSlope;
354
+ let finalYIntercept;
355
+ // Use the determined pointsForRobustLr for the final regression
356
+ if (pointsForRobustLr.length < 2) {
357
+ console.warn("Not enough points for robust regression, falling back to all points.");
358
+ if (!initialLr) {
359
+ initialLr = calculateLinearRegression(allPoints);
360
+ }
361
+ finalSlope = initialLr.slope;
362
+ finalYIntercept = initialLr.yIntercept;
363
+ }
364
+ else {
365
+ const robustLr = calculateLinearRegression(pointsForRobustLr);
366
+ finalSlope = robustLr.slope;
367
+ finalYIntercept = robustLr.yIntercept;
368
+ }
369
+ // Calculate Final Residuals for ALL Original Points against the Robust Line
370
+ const pointsWithFinalResiduals = allPoints.map(point => {
371
+ const predictedYRobust = finalSlope * point.x + finalYIntercept;
372
+ const residual = point.y - predictedYRobust;
373
+ return Object.assign(Object.assign({}, point), { residual: residual, isSignificantlyHigh: false, isSignificantlyLow: false });
374
+ });
375
+ const finalResidualsForDetection = pointsWithFinalResiduals.map(p => p.residual);
376
+ const robustResidualSpread = calculateMAD(finalResidualsForDetection);
377
+ const SMALL_TOLERANCE = 1e-9;
378
+ let effectiveDeviationMeasure = robustResidualSpread;
379
+ const isSmallSet = allPoints.length <= 5;
380
+ // Calculate the Y-range of the final data set for absolute thresholding
381
+ const yValues = allPoints.map(p => p.y);
382
+ const yMin = yValues.reduce((min, current) => Math.min(min, current), Infinity);
383
+ const yMax = yValues.reduce((max, current) => Math.max(max, current), -Infinity);
384
+ const yRange = yMax - yMin;
385
+ // Set a very low absolute residual floor (e.g., 5% of the total range)
386
+ const ABS_RESIDUAL_FLOOR = yRange * 0.05; // 5% of range
387
+ if (effectiveDeviationMeasure < SMALL_TOLERANCE) {
388
+ const maxResidual = finalResidualsForDetection.reduce((max, current) => Math.max(max, Math.abs(current)), 0);
389
+ effectiveDeviationMeasure = maxResidual > SMALL_TOLERANCE ? maxResidual / (deviationThreshold + SMALL_TOLERANCE) : 0;
390
+ }
391
+ else if (isSmallSet) {
392
+ // Aggressively reduce the spread measure for small sets with non-zero MAD
393
+ effectiveDeviationMeasure = effectiveDeviationMeasure / 1.5;
394
+ }
395
+ const significantDeviations = [];
396
+ // Detect Significant Deviations using the robust spread
397
+ if (effectiveDeviationMeasure > SMALL_TOLERANCE) {
398
+ for (const pointWithRes of pointsWithFinalResiduals) {
399
+ const isSignificantByMAD = Math.abs(pointWithRes.residual) > deviationThreshold * effectiveDeviationMeasure;
400
+ const isSignificantByFloor = Math.abs(pointWithRes.residual) > ABS_RESIDUAL_FLOOR;
401
+ if (isSignificantByMAD || isSignificantByFloor) {
402
+ significantDeviations.push(Object.assign(Object.assign({}, pointWithRes), { isSignificantlyHigh: pointWithRes.residual > 0, isSignificantlyLow: pointWithRes.residual < 0 }));
403
+ }
404
+ }
405
+ }
406
+ // Check for Absolute Maximum Inclusion
407
+ const maxIsIncluded = significantDeviations.some(sd => sd.y === yMax);
408
+ if (!maxIsIncluded) {
409
+ const maxPoint = allPoints.find(p => p.y === yMax);
410
+ if (maxPoint) {
411
+ // Flag the absolute max as a significant deviation if it was missed
412
+ significantDeviations.push(Object.assign(Object.assign({}, maxPoint), { residual: maxPoint.y - (finalSlope * maxPoint.x + finalYIntercept), isSignificantlyHigh: true, isSignificantlyLow: false }));
413
+ }
414
+ }
415
+ // Check for Absolute Minimum Inclusion
416
+ const minIsIncluded = significantDeviations.some(sd => sd.y === yMin);
417
+ if (!minIsIncluded) {
418
+ const minPoint = allPoints.find(p => p.y === yMin);
419
+ if (minPoint) {
420
+ // Flag the absolute min as a significant deviation if it was missed
421
+ significantDeviations.push(Object.assign(Object.assign({}, minPoint), { residual: minPoint.y - (finalSlope * minPoint.x + finalYIntercept), isSignificantlyHigh: false, isSignificantlyLow: true // It's the lowest point
422
+ }));
423
+ }
424
+ }
425
+ if (significantDeviations.length === 0) {
426
+ return null;
427
+ }
428
+ let summary = "";
429
+ const highestPoints = significantDeviations.filter(sd => sd.isSignificantlyHigh);
430
+ const lowestPoints = significantDeviations.filter(sd => sd.isSignificantlyLow);
431
+ const MAX_POINTS_TO_REPORT = 5;
432
+ if (highestPoints.length > 0) {
433
+ const uniqueHighestY = [...new Set(highestPoints.map(hp => hp.y))];
434
+ uniqueHighestY.sort((a, b) => b - a);
435
+ const limitedHighestY = uniqueHighestY.slice(0, MAX_POINTS_TO_REPORT);
436
+ const formattedHighestY = limitedHighestY.map(y => (0, truncateDecimals_1.truncateDecimals)(y, decimalLength)).join(", ");
437
+ summary += `${translate("Highest point(s):")} ${formattedHighestY}. `;
438
+ }
439
+ if (lowestPoints.length > 0) {
440
+ const uniqueLowestY = [...new Set(lowestPoints.map(lp => lp.y))];
441
+ uniqueLowestY.sort((a, b) => a - b);
442
+ const limitedLowestY = uniqueLowestY.slice(0, MAX_POINTS_TO_REPORT);
443
+ const formattedLowestY = limitedLowestY.map(y => (0, truncateDecimals_1.truncateDecimals)(y, decimalLength)).join(", ");
444
+ summary += `${translate("Lowest point(s):")} ${formattedLowestY}. `;
445
+ }
446
+ return summary.trim();
447
+ }
448
+ function calculateNumberOfSegments(points) {
449
+ const minPointsForSegmentation = 50;
450
+ const maxSegments = 8;
451
+ const base = 5;
452
+ if (points.length <= minPointsForSegmentation) {
453
+ return 1;
454
+ }
455
+ const calculatedSegments = Math.floor(base * Math.log(points.length));
456
+ return Math.min(calculatedSegments, maxSegments);
457
+ }
458
+ function splitIntoSegments(points) {
459
+ const numSegments = calculateNumberOfSegments(points);
460
+ if (numSegments === 1) {
461
+ return [points];
462
+ }
463
+ const segments = [];
464
+ const segmentSize = Math.floor(points.length / numSegments);
465
+ const remainder = points.length % numSegments;
466
+ let startIndex = 0;
467
+ for (let i = 0; i < numSegments; i++) {
468
+ const currentSegmentSize = segmentSize + (i < remainder ? 1 : 0);
469
+ const endIndex = startIndex + currentSegmentSize;
470
+ segments.push(points.slice(startIndex, endIndex));
471
+ startIndex = endIndex;
472
+ }
473
+ return segments;
474
+ }
475
+ function detectComplexTrend(allPoints, datasetName, decimalLength, translate) {
476
+ const segments = splitIntoSegments(allPoints);
477
+ const segmentTrends = segments.map(segment => calculateLinearRegression(segment));
478
+ const trendDirections = segmentTrends.map(trend => getTrendDirectionBySlope(trend.slope));
479
+ const isComplex = new Set(trendDirections).size > 1;
480
+ if (isComplex) {
481
+ const summaryParts = trendDirections.map((direction, index) => {
482
+ const startPoint = segments[index][0];
483
+ const endPoint = segments[index][segments[index].length - 1];
484
+ return translate(`Segment {INDEX} {DIRECTION} from {FROM} at {X0} to {TO} at {X}.`)
485
+ .replace("{INDEX}", (index + 1).toString())
486
+ .replace("{DIRECTION}", direction)
487
+ .replace("{FROM}", (0, truncateDecimals_1.truncateDecimals)(startPoint.y, decimalLength).toString())
488
+ .replace("{TO}", (0, truncateDecimals_1.truncateDecimals)(endPoint.y, decimalLength).toString())
489
+ .replace("{X0}", startPoint.originX.toString())
490
+ .replace("{X}", endPoint.originX.toString());
491
+ });
492
+ const complexSummary = `${datasetName} ${translate("shows a complex trend with multiple segments:")} ${summaryParts.join(" ")}`;
493
+ return complexSummary;
494
+ }
495
+ return false;
496
+ }
497
+ function calculateAverageValue(points) {
498
+ const yValues = points.map(point => point.y);
499
+ const validNumbers = yValues.filter(n => typeof n === "number");
500
+ if (validNumbers.length === 0) {
501
+ return null;
502
+ }
503
+ const sum = validNumbers.reduce((total, current) => total + current, 0);
504
+ return sum / validNumbers.length;
505
+ }
506
+ function detectDatasetTrend(dataset, translate, dateFormatter, miniChartLabels, options) {
507
+ const datasetName = dataset.label || translate("Data");
508
+ const twoState = detectTwoStateChart(dataset, translate, dateFormatter, miniChartLabels, options);
509
+ if (twoState) {
510
+ return { trendDirection: "flat", summary: twoState };
511
+ }
512
+ const datasetObj = Object.assign({}, dataset);
513
+ if (!miniChartLabels) {
514
+ // simplify the data before trend detection
515
+ datasetObj.data = simplifyData(datasetObj.data);
516
+ }
517
+ const allPoints = collectAllPoints(datasetObj, dateFormatter, miniChartLabels, options);
518
+ if (allPoints.length === 0) {
519
+ return { trendDirection: "undefined", summary: translate("No data available") };
520
+ }
521
+ const allUniqueYValues = [...new Set(allPoints.map(p => p.y))];
522
+ const decimalLength = (0, calculateMinTruncationPrecision_1.calculateMinTruncationPrecision)(allUniqueYValues);
523
+ if (allPoints.length === 1) {
524
+ return { trendDirection: "flat", summary: translate("Only one data point: value is {VALUE}.").replace("{VALUE}", (0, truncateDecimals_1.truncateDecimals)(allPoints[0].y, decimalLength).toString()) };
525
+ }
526
+ const complexTrend = detectComplexTrend(allPoints, datasetName, decimalLength, translate);
527
+ if (complexTrend) {
528
+ return { trendDirection: "complex", summary: complexTrend };
529
+ }
530
+ const { slope } = calculateLinearRegression(allPoints);
531
+ let trendDirection = getTrendDirectionBySlope(slope);
532
+ const firstPoint = allPoints[0];
533
+ const lastPoint = allPoints[allPoints.length - 1];
534
+ const yValues = allPoints.map(p => p.y);
535
+ const yMin = yValues.reduce((min, current) => Math.min(min, current));
536
+ const yMax = yValues.reduce((max, current) => Math.max(max, current));
537
+ const yRange = yMax - yMin;
538
+ const netChange = lastPoint.y - firstPoint.y;
539
+ // A tolerance to decide if the net change is significant enough
540
+ // to overrule a weak linear regression. (e.g., a 5% change of the total range).
541
+ const NARRATIVE_TREND_THRESHOLD = 0.05;
542
+ // If the linear regression detected a non-flat trend, check if it's strong enough.
543
+ if (trendDirection !== "flat" && yRange > 1e-9) {
544
+ const netChangeRatio = Math.abs(netChange / yRange);
545
+ if (netChangeRatio < NARRATIVE_TREND_THRESHOLD) {
546
+ trendDirection = "flat";
547
+ }
548
+ else {
549
+ const netDirection = netChange > 1e-9 ? "increases" : "decreases";
550
+ if (trendDirection !== netDirection) {
551
+ trendDirection = netDirection;
552
+ }
553
+ }
554
+ }
555
+ let summary;
556
+ if (trendDirection === "flat") {
557
+ const averageValue = calculateAverageValue(allPoints);
558
+ const averageString = averageValue ? ` ${translate("Average value is:")} ${(0, truncateDecimals_1.truncateDecimals)(averageValue, decimalLength)}` : "";
559
+ const FLAT_TREND_TOLERANCE = 0.25;
560
+ if (yMax - yMin < FLAT_TREND_TOLERANCE) {
561
+ summary = `${datasetName} ${translate("shows a relatively flat or stable trend.")} ${averageString}`;
562
+ }
563
+ else {
564
+ const formattedYMin = (0, truncateDecimals_1.truncateDecimals)(yMin, decimalLength);
565
+ const formattedYMax = (0, truncateDecimals_1.truncateDecimals)(yMax, decimalLength);
566
+ summary = `${datasetName} ${translate("value changes in the range from {MIN} to {MAX}.")
567
+ .replace("{MIN}", formattedYMin.toString())
568
+ .replace("{MAX}", formattedYMax.toString())}
569
+ ${averageString}`;
570
+ }
571
+ }
572
+ else {
573
+ summary = `${datasetName} ${trendDirection} ${translate("from {FROM} at {X0} to {TO} at {X}.")
574
+ .replace("{FROM}", (0, truncateDecimals_1.truncateDecimals)(firstPoint.y, decimalLength).toString())
575
+ .replace("{TO}", (0, truncateDecimals_1.truncateDecimals)(lastPoint.y, decimalLength).toString())
576
+ .replace("{X0}", firstPoint.originX.toString())
577
+ .replace("{X}", lastPoint.originX.toString())}`;
578
+ const significantDeviations = detectSignificantDeviations(allPoints, translate, decimalLength);
579
+ if (significantDeviations && typeof significantDeviations === "string") {
580
+ summary += ` ${significantDeviations}`;
581
+ }
582
+ }
583
+ return { trendDirection, summary };
584
+ }
585
+ const AccessibleChartNarrative = ({ data, options }) => {
586
+ const { translate } = (0, useLanguage_1.useLanguage)();
587
+ const miniChartLabels = (0, react_1.useMemo)(() => data.labels, [data]);
588
+ const { dateFormat, toLocalDateTime } = (0, react_1.useContext)(userFormatContext_1.userFormatContext);
589
+ const dateFormatter = (0, react_1.useCallback)((d, format = dateFormat) => (0, formatDate_1.formatDate)(toLocalDateTime(d), format, translate), [toLocalDateTime, dateFormat, translate]);
590
+ const filteredDatasets = (0, removeTrendDataFromDatasets_1.removeTrendDataFromDatasets)(data.datasets);
591
+ const datasetsWithAnalysis = (0, react_1.useMemo)(() => filteredDatasets.map(dataset => {
592
+ const datasetObj = Object.assign({}, dataset);
593
+ try {
594
+ const trend = detectDatasetTrend(datasetObj, translate, dateFormatter, miniChartLabels, options);
595
+ return trend.summary;
596
+ }
597
+ catch (error) {
598
+ const errMsg = `${translate("Error generating chart narrative for dataset")} ${dataset.label || ""}`;
599
+ console.error(errMsg, error);
600
+ return errMsg;
601
+ }
602
+ }), [filteredDatasets, translate, dateFormatter, miniChartLabels, options]);
603
+ return (0, jsx_runtime_1.jsx)("div", { className: "screen-reader-only", children: datasetsWithAnalysis.map((analysis, index) => (0, jsx_runtime_1.jsx)("div", { children: analysis }, index)) });
604
+ };
605
+ exports.AccessibleChartNarrative = AccessibleChartNarrative;
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ import { IAccessibleChart } from "./interfaces";
3
+ export declare const AccessibleChartTable: React.FC<IAccessibleChart>;