@tensor_1/react 2.37.0 → 2.39.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.
- package/README.md +50 -0
- package/dist/{chart-theme-Cl0IrkKQ.d.cts → ChartCore-BKK2HEfi.d.cts} +77 -50
- package/dist/chart-analytic-guide.d.ts +6 -0
- package/dist/chart-analytic-guide.js +10 -0
- package/dist/chart-analytic-model.d.ts +141 -0
- package/dist/chart-analytic-model.js +252 -0
- package/dist/chart-analytic.cjs +1372 -0
- package/dist/chart-analytic.d.cts +335 -0
- package/dist/chart-analytic.d.ts +19 -0
- package/dist/chart-analytic.js +95 -0
- package/dist/chart-panels-model.d.ts +92 -0
- package/dist/chart-panels-model.js +284 -0
- package/dist/chart-preset.cjs +41 -4
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -1
- package/dist/chart-stats-o4zDhpVm.d.cts +46 -0
- package/dist/chart-stats.d.ts +46 -0
- package/dist/chart-stats.js +89 -0
- package/dist/chart-theme.d.ts +3 -0
- package/dist/chart-theme.js +2 -0
- package/dist/chart.cjs +41 -4
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -1
- package/dist/components/Chart.d.ts +1 -1
- package/dist/components/ChartCore.d.ts +23 -3
- package/dist/components/ChartCore.js +40 -5
- package/dist/components/ChartPanels.d.ts +52 -0
- package/dist/components/ChartPanels.js +170 -0
- package/dist/components/InsightCard.d.ts +86 -0
- package/dist/components/InsightCard.js +118 -0
- package/dist/components/Stat.d.ts +7 -0
- package/dist/components/Stat.js +6 -3
- package/dist/correlation-scatter-model.d.ts +68 -0
- package/dist/correlation-scatter-model.js +179 -0
- package/dist/icons/Icon.d.ts +23 -23
- package/dist/index.cjs +1185 -961
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -0
- package/package.json +6 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { uixChartAreaGradient } from "./chart-theme.js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ANALYTIC_LABELS,
|
|
4
|
+
combineMarks,
|
|
5
|
+
comparisonSeries,
|
|
6
|
+
crossingPoints,
|
|
7
|
+
eventMarks,
|
|
8
|
+
forecastSeries,
|
|
9
|
+
partialPeriod,
|
|
10
|
+
thresholdMarks
|
|
11
|
+
} from "./chart-analytic-model.js";
|
|
12
|
+
const defaultNumber = new Intl.NumberFormat("en", { maximumFractionDigits: 1 });
|
|
13
|
+
const DEFAULT_CHART_PANELS_LABELS = {
|
|
14
|
+
...DEFAULT_ANALYTIC_LABELS,
|
|
15
|
+
x: "Period",
|
|
16
|
+
table: (title) => `${title} \u2014 data table`,
|
|
17
|
+
formatValue: (value, unit) => {
|
|
18
|
+
const text = defaultNumber.format(value);
|
|
19
|
+
if (!unit) return text;
|
|
20
|
+
return unit === "%" ? `${text}%` : `${text} ${unit}`;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const CHART_PANELS_LAYOUT = {
|
|
24
|
+
top: 8,
|
|
25
|
+
lane: 14,
|
|
26
|
+
laneGap: 10,
|
|
27
|
+
title: 28,
|
|
28
|
+
panelHeight: 112,
|
|
29
|
+
panelGap: 14,
|
|
30
|
+
axisBand: 30,
|
|
31
|
+
left: 52,
|
|
32
|
+
right: 16,
|
|
33
|
+
gutter: 100
|
|
34
|
+
};
|
|
35
|
+
function niceCeil(value) {
|
|
36
|
+
if (!(value > 0)) return value === 0 ? 1 : value;
|
|
37
|
+
const exponent = Math.floor(Math.log10(value));
|
|
38
|
+
const base = 10 ** exponent;
|
|
39
|
+
for (const step of [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]) {
|
|
40
|
+
if (step * base >= value - 1e-12) return step * base;
|
|
41
|
+
}
|
|
42
|
+
return 10 * base;
|
|
43
|
+
}
|
|
44
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
45
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
46
|
+
const sameX = (a, b) => String(a) === String(b);
|
|
47
|
+
function chartPanelsModel(spec, tokens, labelOverrides) {
|
|
48
|
+
const labels = { ...DEFAULT_CHART_PANELS_LABELS, ...labelOverrides };
|
|
49
|
+
const L = CHART_PANELS_LAYOUT;
|
|
50
|
+
const events = spec.events ?? [];
|
|
51
|
+
const hasLane = events.length > 0;
|
|
52
|
+
const hasGutter = spec.panels.some((panel) => panel.thresholds?.length);
|
|
53
|
+
const right = hasGutter ? L.gutter : L.right;
|
|
54
|
+
const anyBar = spec.panels.some((panel) => panel.kind === "bar");
|
|
55
|
+
const categories = [...spec.x.values];
|
|
56
|
+
for (const panel of spec.panels) {
|
|
57
|
+
for (const point of panel.forecast?.points ?? []) {
|
|
58
|
+
if (!categories.some((x) => sameX(x, point.x))) categories.push(point.x);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const lastActual = spec.x.values[spec.x.values.length - 1];
|
|
62
|
+
const previousActual = spec.x.values[spec.x.values.length - 2];
|
|
63
|
+
const grids = [];
|
|
64
|
+
const xAxes = [];
|
|
65
|
+
const yAxes = [];
|
|
66
|
+
const series = [];
|
|
67
|
+
const seriesPanel = {};
|
|
68
|
+
const legend = [];
|
|
69
|
+
let cursor = L.top;
|
|
70
|
+
const xAxisBase = (gridIndex, showLabels) => {
|
|
71
|
+
const common = {
|
|
72
|
+
gridIndex,
|
|
73
|
+
axisLabel: {
|
|
74
|
+
show: showLabels,
|
|
75
|
+
hideOverlap: true,
|
|
76
|
+
showMaxLabel: true,
|
|
77
|
+
// the first and last labels align inward, so neither runs into the y-axis labels or the gutter
|
|
78
|
+
alignMinLabel: "left",
|
|
79
|
+
alignMaxLabel: "right",
|
|
80
|
+
formatter: (value) => labels.formatX(value)
|
|
81
|
+
},
|
|
82
|
+
axisLine: { show: true },
|
|
83
|
+
splitLine: { show: false }
|
|
84
|
+
};
|
|
85
|
+
return spec.x.type === "time" ? { type: "time", ...common } : { type: "category", data: categories.map(String), boundaryGap: anyBar, ...common };
|
|
86
|
+
};
|
|
87
|
+
let laneSeriesId = "";
|
|
88
|
+
let eventFragment = null;
|
|
89
|
+
if (hasLane) {
|
|
90
|
+
grids.push({ top: cursor, height: L.lane, left: L.left, right, containLabel: false });
|
|
91
|
+
xAxes.push({ ...xAxisBase(0, false), axisLine: { show: false } });
|
|
92
|
+
yAxes.push({ type: "value", gridIndex: 0, min: -1, max: 1, show: false });
|
|
93
|
+
const marks = eventMarks(events, { tokens, xAxisIndex: 0, yAxisIndex: 0, seriesId: "uix-event-lane" });
|
|
94
|
+
laneSeriesId = "uix-event-lane";
|
|
95
|
+
series.push(marks.lane.series);
|
|
96
|
+
eventFragment = { markLine: marks.markLine, markArea: marks.markArea };
|
|
97
|
+
cursor += L.lane + L.laneGap;
|
|
98
|
+
}
|
|
99
|
+
const tables = [];
|
|
100
|
+
spec.panels.forEach((panel, panelIndex) => {
|
|
101
|
+
const gridIndex = grids.length;
|
|
102
|
+
const isLast = panelIndex === spec.panels.length - 1;
|
|
103
|
+
const height = panel.height ?? L.panelHeight;
|
|
104
|
+
cursor += L.title;
|
|
105
|
+
grids.push({ top: cursor, height, left: L.left, right, containLabel: false });
|
|
106
|
+
cursor += height + (isLast ? L.axisBand : L.panelGap);
|
|
107
|
+
const values = [];
|
|
108
|
+
for (const s of panel.series) values.push(...s.values.filter(finite));
|
|
109
|
+
values.push(...panel.comparison?.values.filter(finite) ?? []);
|
|
110
|
+
for (const p of panel.forecast?.points ?? []) values.push(p.lower, p.upper, p.y);
|
|
111
|
+
for (const t of panel.thresholds ?? []) values.push(t.value, ...finite(t.to) ? [t.to] : []);
|
|
112
|
+
const usable = values.filter(Number.isFinite);
|
|
113
|
+
const dataMax = usable.length ? Math.max(...usable) : 1;
|
|
114
|
+
const dataMin = usable.length ? Math.min(...usable) : 0;
|
|
115
|
+
const max = niceCeil(dataMax * 1.08);
|
|
116
|
+
const min = dataMin < 0 ? -niceCeil(-dataMin * 1.08) : 0;
|
|
117
|
+
xAxes.push(xAxisBase(gridIndex, isLast));
|
|
118
|
+
yAxes.push({
|
|
119
|
+
type: "value",
|
|
120
|
+
gridIndex,
|
|
121
|
+
min,
|
|
122
|
+
max,
|
|
123
|
+
// three ticks (min, half way, max): a short panel never prints a crowded extra max label
|
|
124
|
+
interval: (max - min) / 2,
|
|
125
|
+
name: panel.unit ? `${panel.title} (${panel.unit})` : panel.title,
|
|
126
|
+
nameLocation: "end",
|
|
127
|
+
nameGap: 14,
|
|
128
|
+
nameTextStyle: { align: "left", padding: [0, 0, 0, -L.left + 4], color: tokens.text, fontFamily: tokens.fontSans, fontSize: 12, fontWeight: 600, width: 320, overflow: "truncate" },
|
|
129
|
+
axisLabel: { formatter: (value) => labels.formatValue(value, panel.unit === "%" ? "%" : void 0) }
|
|
130
|
+
});
|
|
131
|
+
const axes = { xAxisIndex: gridIndex, yAxisIndex: gridIndex };
|
|
132
|
+
const pairs = (vals) => spec.x.values.map((x, i) => [x, vals[i] ?? null]);
|
|
133
|
+
panel.series.forEach((s, seriesIndex) => {
|
|
134
|
+
const colorIndex = s.colorIndex ?? seriesIndex;
|
|
135
|
+
const color = tokens.palette[colorIndex % tokens.palette.length];
|
|
136
|
+
const id = `${panel.id}:${s.id}`;
|
|
137
|
+
seriesPanel[id] = panel.id;
|
|
138
|
+
const isPrimary = seriesIndex === 0;
|
|
139
|
+
let marks = {};
|
|
140
|
+
if (isPrimary) {
|
|
141
|
+
const crossings = (panel.thresholds ?? []).filter((t) => t.kind !== "target" && (t.badSide || t.kind === "limit" || t.kind === "budget" || t.kind === "floor")).slice(0, 1).map((t) => crossingPoints(s.values.slice(0, spec.x.values.length), t, { x: spec.x.values, tokens, labels }));
|
|
142
|
+
marks = combineMarks(
|
|
143
|
+
panel.thresholds?.length ? thresholdMarks(panel.thresholds, { tokens, extent: [min, max] }) : null,
|
|
144
|
+
eventFragment,
|
|
145
|
+
spec.partialLast && lastActual != null ? partialWash(partialPeriod(lastActual, { tokens, from: previousActual, labels }), panelIndex === 0) : null,
|
|
146
|
+
...crossings
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (panel.kind === "bar") {
|
|
150
|
+
series.push({
|
|
151
|
+
id,
|
|
152
|
+
name: s.label,
|
|
153
|
+
type: "bar",
|
|
154
|
+
...axes,
|
|
155
|
+
data: pairs(s.values),
|
|
156
|
+
barMaxWidth: 24,
|
|
157
|
+
itemStyle: { color, borderRadius: [4, 4, 0, 0], borderColor: tokens.surface, borderWidth: 1 },
|
|
158
|
+
animation: false,
|
|
159
|
+
z: 2,
|
|
160
|
+
...marks
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
series.push({
|
|
164
|
+
id,
|
|
165
|
+
name: s.label,
|
|
166
|
+
type: "line",
|
|
167
|
+
...axes,
|
|
168
|
+
data: pairs(s.values),
|
|
169
|
+
symbol: "circle",
|
|
170
|
+
symbolSize: 8,
|
|
171
|
+
showSymbol: false,
|
|
172
|
+
connectNulls: false,
|
|
173
|
+
lineStyle: { color, width: 2, type: "solid" },
|
|
174
|
+
itemStyle: { color, borderColor: tokens.surface, borderWidth: 2 },
|
|
175
|
+
...panel.kind === "area" ? { areaStyle: { color: uixChartAreaGradient(color) } } : {},
|
|
176
|
+
emphasis: { focus: "none" },
|
|
177
|
+
animation: false,
|
|
178
|
+
z: 2,
|
|
179
|
+
...marks
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (panel.series.length > 1 || panel.comparison || panel.forecast) legend.push({ label: s.label, color, swatch: panel.kind === "bar" ? "block" : "line" });
|
|
183
|
+
});
|
|
184
|
+
if (panel.comparison) {
|
|
185
|
+
const id = `${panel.id}:comparison`;
|
|
186
|
+
seriesPanel[id] = panel.id;
|
|
187
|
+
series.push(comparisonSeries(panel.comparison, { x: spec.x.values, tokens, ...axes, id }));
|
|
188
|
+
if (!legend.some((item) => item.label === panel.comparison.label)) legend.push({ label: panel.comparison.label, color: tokens.comparison, swatch: "line" });
|
|
189
|
+
}
|
|
190
|
+
if (panel.forecast && panel.series[0]) {
|
|
191
|
+
const primary = panel.series[0];
|
|
192
|
+
const fromIndex = spec.x.values.findIndex((x) => sameX(x, panel.forecast.from));
|
|
193
|
+
const fromValue = fromIndex >= 0 ? primary.values[fromIndex] ?? null : null;
|
|
194
|
+
const colorIndex = primary.colorIndex ?? 0;
|
|
195
|
+
const fc = forecastSeries(panel.forecast, { name: primary.label, colorIndex, tokens, labels, fromValue, ...axes, idPrefix: `${panel.id}:forecast` });
|
|
196
|
+
for (const s of fc) seriesPanel[String(s.id)] = panel.id;
|
|
197
|
+
series.push(...fc);
|
|
198
|
+
if (!legend.some((item) => item.swatch === "dash")) {
|
|
199
|
+
legend.push({ label: labels.projection, color: tokens.palette[colorIndex % tokens.palette.length], swatch: "dash" });
|
|
200
|
+
legend.push({ label: labels.range(panel.forecast.level), color: tokens.forecastBand, swatch: "block" });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
tables.push(panelTable(spec, panel, categories, events, labels));
|
|
204
|
+
});
|
|
205
|
+
const allX = xAxes.map((_, i) => i);
|
|
206
|
+
const option = {
|
|
207
|
+
animation: false,
|
|
208
|
+
grid: grids,
|
|
209
|
+
xAxis: xAxes,
|
|
210
|
+
yAxis: yAxes,
|
|
211
|
+
axisPointer: { link: [{ xAxisIndex: "all" }], label: { show: false } },
|
|
212
|
+
tooltip: {
|
|
213
|
+
trigger: "axis",
|
|
214
|
+
confine: true,
|
|
215
|
+
axisPointer: { type: "line" },
|
|
216
|
+
formatter: (params) => tooltipHtml(params, spec, categories, events, labels)
|
|
217
|
+
},
|
|
218
|
+
series,
|
|
219
|
+
...spec.zoom ? { dataZoom: [{ type: "inside", xAxisIndex: allX, filterMode: "none" }] } : {}
|
|
220
|
+
};
|
|
221
|
+
return { option, height: cursor, legend, tables, categories, seriesPanel, laneSeriesId };
|
|
222
|
+
}
|
|
223
|
+
function partialWash(marks, labelled) {
|
|
224
|
+
if (labelled || !marks.markArea?.data) return marks;
|
|
225
|
+
const data = marks.markArea.data.map(([from, to]) => [{ ...from, label: { show: false } }, to]);
|
|
226
|
+
return { markArea: { ...marks.markArea, data } };
|
|
227
|
+
}
|
|
228
|
+
function panelTable(spec, panel, categories, events, labels) {
|
|
229
|
+
const headers = [labels.x, ...panel.series.map((s) => s.label)];
|
|
230
|
+
if (panel.comparison) headers.push(panel.comparison.label);
|
|
231
|
+
if (panel.forecast) headers.push(labels.projection, labels.lower, labels.upper);
|
|
232
|
+
for (const t of panel.thresholds ?? []) headers.push(`${labels.threshold}: ${t.label}`);
|
|
233
|
+
if (events.length) headers.push(labels.event);
|
|
234
|
+
const unit = panel.unit;
|
|
235
|
+
const fmt = (v) => finite(v) ? labels.formatValue(v, unit) : "";
|
|
236
|
+
const rows = categories.map((x, i) => {
|
|
237
|
+
const row = { [labels.x]: labels.formatX(x) };
|
|
238
|
+
for (const s of panel.series) row[s.label] = i < spec.x.values.length ? fmt(s.values[i]) : "";
|
|
239
|
+
if (panel.comparison) row[panel.comparison.label] = i < spec.x.values.length ? fmt(panel.comparison.values[i]) : "";
|
|
240
|
+
if (panel.forecast) {
|
|
241
|
+
const p = panel.forecast.points.find((point) => sameX(point.x, x));
|
|
242
|
+
row[labels.projection] = fmt(p?.y);
|
|
243
|
+
row[labels.lower] = fmt(p?.lower);
|
|
244
|
+
row[labels.upper] = fmt(p?.upper);
|
|
245
|
+
}
|
|
246
|
+
for (const t of panel.thresholds ?? []) row[`${labels.threshold}: ${t.label}`] = finite(t.to) ? `${fmt(t.value)}\u2013${fmt(t.to)}` : fmt(t.value);
|
|
247
|
+
if (events.length) row[labels.event] = events.filter((e) => sameX(e.at, x)).map((e) => e.label).join("; ");
|
|
248
|
+
return row;
|
|
249
|
+
});
|
|
250
|
+
return { caption: labels.table(panel.title), headers, rows };
|
|
251
|
+
}
|
|
252
|
+
function tooltipHtml(params, spec, categories, events, labels) {
|
|
253
|
+
const list = Array.isArray(params) ? params : [params];
|
|
254
|
+
const first = list[0];
|
|
255
|
+
if (!first) return "";
|
|
256
|
+
const axisValue = first.axisValue ?? (Array.isArray(first.value) ? first.value[0] : void 0);
|
|
257
|
+
const index = categories.findIndex((x2) => sameX(x2, axisValue));
|
|
258
|
+
if (index < 0) return "";
|
|
259
|
+
const x = categories[index];
|
|
260
|
+
const lines = [`<div style="font-weight:600;margin-bottom:4px">${escapeHtml(labels.formatX(x))}</div>`];
|
|
261
|
+
const row = (name, value) => `<div style="display:flex;justify-content:space-between;gap:16px"><span>${escapeHtml(name)}</span><span style="font-variant-numeric:tabular-nums">${escapeHtml(value)}</span></div>`;
|
|
262
|
+
for (const panel of spec.panels) {
|
|
263
|
+
for (const s of panel.series) {
|
|
264
|
+
const v = index < spec.x.values.length ? s.values[index] : null;
|
|
265
|
+
if (finite(v)) lines.push(row(s.label, labels.formatValue(v, panel.unit)));
|
|
266
|
+
}
|
|
267
|
+
const c = panel.comparison && index < spec.x.values.length ? panel.comparison.values[index] : null;
|
|
268
|
+
if (panel.comparison && finite(c)) lines.push(row(panel.comparison.label, labels.formatValue(c, panel.unit)));
|
|
269
|
+
const p = panel.forecast?.points.find((point) => sameX(point.x, x));
|
|
270
|
+
if (p && panel.forecast) {
|
|
271
|
+
lines.push(row(`${labels.projection} (${panel.forecast.method})`, labels.formatValue(p.y, panel.unit)));
|
|
272
|
+
lines.push(row(labels.range(panel.forecast.level), `${labels.formatValue(p.lower, panel.unit)} \u2013 ${labels.formatValue(p.upper, panel.unit)}`));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const e of events.filter((event) => sameX(event.at, x))) lines.push(row(e.kind, e.label));
|
|
276
|
+
if (spec.partialLast && index === spec.x.values.length - 1) lines.push(`<div style="opacity:.75">${escapeHtml(labels.partial)}</div>`);
|
|
277
|
+
return lines.join("");
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
CHART_PANELS_LAYOUT,
|
|
281
|
+
DEFAULT_CHART_PANELS_LABELS,
|
|
282
|
+
chartPanelsModel,
|
|
283
|
+
niceCeil
|
|
284
|
+
};
|
package/dist/chart-preset.cjs
CHANGED
|
@@ -79,6 +79,8 @@ function uixChartTokens(source) {
|
|
|
79
79
|
event: t("chart-event"),
|
|
80
80
|
partial: t("chart-partial"),
|
|
81
81
|
comparison: t("chart-comparison"),
|
|
82
|
+
warning: t("warning"),
|
|
83
|
+
danger: t("danger"),
|
|
82
84
|
surface: t("surface"),
|
|
83
85
|
text: t("text"),
|
|
84
86
|
textMuted: t("text-muted"),
|
|
@@ -161,7 +163,8 @@ function uixChartPalette() {
|
|
|
161
163
|
return [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readToken(`--uix-chart-${i}`));
|
|
162
164
|
}
|
|
163
165
|
function themed(option, theme, element) {
|
|
164
|
-
|
|
166
|
+
const resolved = typeof option === "function" ? option(uixChartTokens(element)) : option;
|
|
167
|
+
return theme === "none" ? resolved : mergeChartTheme(uixChartTheme(element), resolved);
|
|
165
168
|
}
|
|
166
169
|
function motionSafe(option) {
|
|
167
170
|
if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
@@ -192,17 +195,22 @@ function ChartCore({
|
|
|
192
195
|
loading = false,
|
|
193
196
|
empty,
|
|
194
197
|
labels: labelOverrides,
|
|
195
|
-
theme = "uix"
|
|
198
|
+
theme = "uix",
|
|
199
|
+
group,
|
|
200
|
+
onEvents,
|
|
201
|
+
tables
|
|
196
202
|
}) {
|
|
197
203
|
const labels = { ...DEFAULT_CHART_LABELS, ...labelOverrides };
|
|
198
204
|
const containerRef = (0, import_react.useRef)(null);
|
|
199
205
|
const chartRef = (0, import_react.useRef)(null);
|
|
200
206
|
const appliedOptionRef = (0, import_react.useRef)(null);
|
|
207
|
+
const onEventsRef = (0, import_react.useRef)(onEvents);
|
|
208
|
+
onEventsRef.current = onEvents;
|
|
201
209
|
const latestOptionRef = (0, import_react.useRef)(option);
|
|
202
210
|
const onReadyRef = (0, import_react.useRef)(onReady);
|
|
203
211
|
const tableId = (0, import_react.useId)();
|
|
204
212
|
const hasEmpty = empty != null;
|
|
205
|
-
const hasTextAlternative = Boolean(ariaLabel || title || tableData?.length);
|
|
213
|
+
const hasTextAlternative = Boolean(ariaLabel || title || tableData?.length || tables?.length);
|
|
206
214
|
const hasTextAlternativeRef = (0, import_react.useRef)(hasTextAlternative);
|
|
207
215
|
const themeRef = (0, import_react.useRef)(theme);
|
|
208
216
|
latestOptionRef.current = option;
|
|
@@ -254,6 +262,29 @@ function ChartCore({
|
|
|
254
262
|
appliedOptionRef.current = null;
|
|
255
263
|
};
|
|
256
264
|
}, [engine, hasEmpty]);
|
|
265
|
+
const eventNames = Object.keys(onEvents ?? {}).sort().join("|");
|
|
266
|
+
(0, import_react.useEffect)(() => {
|
|
267
|
+
const chart = chartRef.current;
|
|
268
|
+
if (!chart || !eventNames) return;
|
|
269
|
+
const bound = eventNames.split("|").map((name) => {
|
|
270
|
+
const handler = (params) => onEventsRef.current?.[name]?.(params);
|
|
271
|
+
chart.on(name, handler);
|
|
272
|
+
return [name, handler];
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
if (chart.isDisposed()) return;
|
|
276
|
+
for (const [name, handler] of bound) chart.off(name, handler);
|
|
277
|
+
};
|
|
278
|
+
}, [engine, hasEmpty, eventNames]);
|
|
279
|
+
(0, import_react.useEffect)(() => {
|
|
280
|
+
const chart = chartRef.current;
|
|
281
|
+
if (!chart || !group) return;
|
|
282
|
+
chart.group = group;
|
|
283
|
+
engine.connect?.(group);
|
|
284
|
+
return () => {
|
|
285
|
+
if (!chart.isDisposed()) chart.group = "";
|
|
286
|
+
};
|
|
287
|
+
}, [engine, hasEmpty, group]);
|
|
257
288
|
(0, import_react.useEffect)(() => {
|
|
258
289
|
const chart = chartRef.current;
|
|
259
290
|
const element = containerRef.current;
|
|
@@ -264,6 +295,8 @@ function ChartCore({
|
|
|
264
295
|
const heightValue = typeof height === "number" ? `${height}px` : height;
|
|
265
296
|
const effectiveAriaLabel = ariaLabel ?? title ?? labels.chart;
|
|
266
297
|
const hasTable = Boolean(tableData?.length && tableHeaders?.length);
|
|
298
|
+
const extraTables = (tables ?? []).filter((table) => table.headers.length && table.rows.length);
|
|
299
|
+
const describedBy = [hasTable ? tableId : null, ...extraTables.map((_, index) => `${tableId}-${index}`)].filter(Boolean).join(" ") || void 0;
|
|
267
300
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx("uix-chart", className), style, children: [
|
|
268
301
|
title != null || subtitle != null || header != null || headerAction != null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "uix-chart__header", children: [
|
|
269
302
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "uix-chart__heading", children: [
|
|
@@ -282,7 +315,7 @@ function ChartCore({
|
|
|
282
315
|
style: { height: heightValue },
|
|
283
316
|
role: "img",
|
|
284
317
|
"aria-label": effectiveAriaLabel,
|
|
285
|
-
"aria-describedby":
|
|
318
|
+
"aria-describedby": describedBy
|
|
286
319
|
}
|
|
287
320
|
)
|
|
288
321
|
] }),
|
|
@@ -290,6 +323,10 @@ function ChartCore({
|
|
|
290
323
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: label }, label)) }) }),
|
|
291
324
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: tableData.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: row[label] }, label)) }, index)) })
|
|
292
325
|
] }) : null,
|
|
326
|
+
extraTables.map((table, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { id: `${tableId}-${index}`, className: "uix-visually-hidden", "aria-label": table.caption, children: [
|
|
327
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: table.headers.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: label }, label)) }) }),
|
|
328
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: table.rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: table.headers.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: row[label] }, label)) }, rowIndex)) })
|
|
329
|
+
] }, index)),
|
|
293
330
|
footer != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "uix-chart__footer", children: footer })
|
|
294
331
|
] });
|
|
295
332
|
}
|
package/dist/chart-preset.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
2
|
+
import { a as ChartProps } from './ChartCore-BKK2HEfi.cjs';
|
|
3
|
+
export { c as ChartEventHandler, C as ChartLabels, d as ChartLegend, e as ChartLegendItem, f as ChartLegendItemProps, g as ChartLegendProps, h as ChartMetric, i as ChartMetricProps, j as ChartOption, k as ChartTable, l as ChartTableRow, m as ChartTokenSource, D as DEFAULT_CHART_LABELS, U as UixChartTokens, n as mergeChartTheme, u as uixChartAreaGradient, o as uixChartPalette, p as uixChartTheme, q as uixChartTokens } from './ChartCore-BKK2HEfi.cjs';
|
|
4
4
|
import 'echarts';
|
|
5
5
|
|
|
6
6
|
/** Lean SVG adapter: line, bar, pie, dataset, grid, legend, title, and tooltip. */
|
package/dist/chart-preset.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ChartProps } from './components/ChartCore.js';
|
|
3
|
-
export { ChartLabels, ChartLegend, ChartLegendItem, ChartLegendItemProps, ChartLegendProps, ChartMetric, ChartMetricProps, ChartTableRow, DEFAULT_CHART_LABELS, uixChartPalette } from './components/ChartCore.js';
|
|
3
|
+
export { ChartEventHandler, ChartLabels, ChartLegend, ChartLegendItem, ChartLegendItemProps, ChartLegendProps, ChartMetric, ChartMetricProps, ChartOption, ChartTable, ChartTableRow, DEFAULT_CHART_LABELS, uixChartPalette } from './components/ChartCore.js';
|
|
4
4
|
export { ChartTokenSource, UixChartTokens, mergeChartTheme, uixChartAreaGradient, uixChartTheme, uixChartTokens } from './chart-theme.js';
|
|
5
5
|
import 'echarts';
|
|
6
6
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** One observation for {@link linearFit}. */
|
|
2
|
+
interface ChartStatPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** An ordinary least-squares line through the points, with what a prediction band needs. */
|
|
7
|
+
interface LinearFit {
|
|
8
|
+
slope: number;
|
|
9
|
+
intercept: number;
|
|
10
|
+
/** Pearson correlation; `null` when y is constant (undefined, not zero). */
|
|
11
|
+
r: number | null;
|
|
12
|
+
/** Points used (non-finite x or y are skipped). */
|
|
13
|
+
n: number;
|
|
14
|
+
/** Mean of x. */
|
|
15
|
+
xMean: number;
|
|
16
|
+
/** Sum of squared deviations of x from its mean. */
|
|
17
|
+
sxx: number;
|
|
18
|
+
/** Residual standard error, √(SSE / (n − 2)); 0 when n = 2. */
|
|
19
|
+
residualStdError: number;
|
|
20
|
+
}
|
|
21
|
+
/** A band around the fitted line at each requested x. */
|
|
22
|
+
interface PredictionBandPoint {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
lower: number;
|
|
26
|
+
upper: number;
|
|
27
|
+
}
|
|
28
|
+
/** Least-squares line through `points`; `null` below two usable points or when every x is equal. */
|
|
29
|
+
declare function linearFit(points: readonly ChartStatPoint[]): LinearFit | null;
|
|
30
|
+
/**
|
|
31
|
+
* Quantiles by linear interpolation between order statistics (the "type 7" rule of R and NumPy).
|
|
32
|
+
* `ps` are fractions in [0, 1]; non-finite values are skipped. An empty input gives `null`s.
|
|
33
|
+
*/
|
|
34
|
+
declare function quantiles(values: readonly number[], ps: readonly number[]): (number | null)[];
|
|
35
|
+
/** Inverse of the standard normal CDF (Acklam's rational approximation, |error| < 1.2e-9). */
|
|
36
|
+
declare function normalQuantile(p: number): number;
|
|
37
|
+
/** Student-t quantile by the Cornish–Fisher expansion around the normal (good to ~1e-3 from 3 df). */
|
|
38
|
+
declare function studentTQuantile(p: number, df: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* A prediction band for new observations at `xs`: ŷ ± t·s·√(1 + 1/n + (x − x̄)² / Sxx), with `level`
|
|
41
|
+
* the central coverage (0.8 = 80 %). With n = 2 there is no residual error to widen by, so the band
|
|
42
|
+
* collapses onto the line.
|
|
43
|
+
*/
|
|
44
|
+
declare function predictionBand(fit: LinearFit, xs: readonly number[], level?: number): PredictionBandPoint[];
|
|
45
|
+
|
|
46
|
+
export { type ChartStatPoint as C, type LinearFit as L, type PredictionBandPoint as P, linearFit as l, normalQuantile as n, predictionBand as p, quantiles as q, studentTQuantile as s };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** One observation for {@link linearFit}. */
|
|
2
|
+
interface ChartStatPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** An ordinary least-squares line through the points, with what a prediction band needs. */
|
|
7
|
+
interface LinearFit {
|
|
8
|
+
slope: number;
|
|
9
|
+
intercept: number;
|
|
10
|
+
/** Pearson correlation; `null` when y is constant (undefined, not zero). */
|
|
11
|
+
r: number | null;
|
|
12
|
+
/** Points used (non-finite x or y are skipped). */
|
|
13
|
+
n: number;
|
|
14
|
+
/** Mean of x. */
|
|
15
|
+
xMean: number;
|
|
16
|
+
/** Sum of squared deviations of x from its mean. */
|
|
17
|
+
sxx: number;
|
|
18
|
+
/** Residual standard error, √(SSE / (n − 2)); 0 when n = 2. */
|
|
19
|
+
residualStdError: number;
|
|
20
|
+
}
|
|
21
|
+
/** A band around the fitted line at each requested x. */
|
|
22
|
+
interface PredictionBandPoint {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
lower: number;
|
|
26
|
+
upper: number;
|
|
27
|
+
}
|
|
28
|
+
/** Least-squares line through `points`; `null` below two usable points or when every x is equal. */
|
|
29
|
+
declare function linearFit(points: readonly ChartStatPoint[]): LinearFit | null;
|
|
30
|
+
/**
|
|
31
|
+
* Quantiles by linear interpolation between order statistics (the "type 7" rule of R and NumPy).
|
|
32
|
+
* `ps` are fractions in [0, 1]; non-finite values are skipped. An empty input gives `null`s.
|
|
33
|
+
*/
|
|
34
|
+
declare function quantiles(values: readonly number[], ps: readonly number[]): (number | null)[];
|
|
35
|
+
/** Inverse of the standard normal CDF (Acklam's rational approximation, |error| < 1.2e-9). */
|
|
36
|
+
declare function normalQuantile(p: number): number;
|
|
37
|
+
/** Student-t quantile by the Cornish–Fisher expansion around the normal (good to ~1e-3 from 3 df). */
|
|
38
|
+
declare function studentTQuantile(p: number, df: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* A prediction band for new observations at `xs`: ŷ ± t·s·√(1 + 1/n + (x − x̄)² / Sxx), with `level`
|
|
41
|
+
* the central coverage (0.8 = 80 %). With n = 2 there is no residual error to widen by, so the band
|
|
42
|
+
* collapses onto the line.
|
|
43
|
+
*/
|
|
44
|
+
declare function predictionBand(fit: LinearFit, xs: readonly number[], level?: number): PredictionBandPoint[];
|
|
45
|
+
|
|
46
|
+
export { type ChartStatPoint, type LinearFit, type PredictionBandPoint, linearFit, normalQuantile, predictionBand, quantiles, studentTQuantile };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2
|
+
function linearFit(points) {
|
|
3
|
+
const usable = points.filter((p) => finite(p.x) && finite(p.y));
|
|
4
|
+
const n = usable.length;
|
|
5
|
+
if (n < 2) return null;
|
|
6
|
+
let sumX = 0;
|
|
7
|
+
let sumY = 0;
|
|
8
|
+
for (const p of usable) {
|
|
9
|
+
sumX += p.x;
|
|
10
|
+
sumY += p.y;
|
|
11
|
+
}
|
|
12
|
+
const xMean = sumX / n;
|
|
13
|
+
const yMean = sumY / n;
|
|
14
|
+
let sxx = 0;
|
|
15
|
+
let syy = 0;
|
|
16
|
+
let sxy = 0;
|
|
17
|
+
for (const p of usable) {
|
|
18
|
+
const dx = p.x - xMean;
|
|
19
|
+
const dy = p.y - yMean;
|
|
20
|
+
sxx += dx * dx;
|
|
21
|
+
syy += dy * dy;
|
|
22
|
+
sxy += dx * dy;
|
|
23
|
+
}
|
|
24
|
+
if (sxx === 0) return null;
|
|
25
|
+
const slope = sxy / sxx;
|
|
26
|
+
const intercept = yMean - slope * xMean;
|
|
27
|
+
let sse = 0;
|
|
28
|
+
for (const p of usable) {
|
|
29
|
+
const residual = p.y - (intercept + slope * p.x);
|
|
30
|
+
sse += residual * residual;
|
|
31
|
+
}
|
|
32
|
+
const r = syy === 0 ? null : Math.max(-1, Math.min(1, sxy / Math.sqrt(sxx * syy)));
|
|
33
|
+
const residualStdError = n > 2 ? Math.sqrt(sse / (n - 2)) : 0;
|
|
34
|
+
return { slope, intercept, r, n, xMean, sxx, residualStdError };
|
|
35
|
+
}
|
|
36
|
+
function quantiles(values, ps) {
|
|
37
|
+
const sorted = values.filter(finite).slice().sort((a, b) => a - b);
|
|
38
|
+
return ps.map((p) => {
|
|
39
|
+
if (!sorted.length || !finite(p)) return null;
|
|
40
|
+
const clamped = Math.max(0, Math.min(1, p));
|
|
41
|
+
const h = (sorted.length - 1) * clamped;
|
|
42
|
+
const lo = Math.floor(h);
|
|
43
|
+
const hi = Math.ceil(h);
|
|
44
|
+
return sorted[lo] + (h - lo) * (sorted[hi] - sorted[lo]);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function normalQuantile(p) {
|
|
48
|
+
if (!(p > 0 && p < 1)) return p === 0 ? -Infinity : p === 1 ? Infinity : Number.NaN;
|
|
49
|
+
const a = [-39.69683028665376, 220.9460984245205, -275.9285104469687, 138.357751867269, -30.66479806614716, 2.506628277459239];
|
|
50
|
+
const b = [-54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572];
|
|
51
|
+
const c = [-0.007784894002430293, -0.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
|
|
52
|
+
const d = [0.007784695709041462, 0.3224671290700398, 2.445134137142996, 3.754408661907416];
|
|
53
|
+
const low = 0.02425;
|
|
54
|
+
if (p < low) {
|
|
55
|
+
const q2 = Math.sqrt(-2 * Math.log(p));
|
|
56
|
+
return (((((c[0] * q2 + c[1]) * q2 + c[2]) * q2 + c[3]) * q2 + c[4]) * q2 + c[5]) / ((((d[0] * q2 + d[1]) * q2 + d[2]) * q2 + d[3]) * q2 + 1);
|
|
57
|
+
}
|
|
58
|
+
if (p > 1 - low) {
|
|
59
|
+
const q2 = Math.sqrt(-2 * Math.log(1 - p));
|
|
60
|
+
return -(((((c[0] * q2 + c[1]) * q2 + c[2]) * q2 + c[3]) * q2 + c[4]) * q2 + c[5]) / ((((d[0] * q2 + d[1]) * q2 + d[2]) * q2 + d[3]) * q2 + 1);
|
|
61
|
+
}
|
|
62
|
+
const q = p - 0.5;
|
|
63
|
+
const r = q * q;
|
|
64
|
+
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
|
|
65
|
+
}
|
|
66
|
+
function studentTQuantile(p, df) {
|
|
67
|
+
const z = normalQuantile(p);
|
|
68
|
+
if (!Number.isFinite(z) || !(df > 0)) return z;
|
|
69
|
+
const z3 = z ** 3;
|
|
70
|
+
const z5 = z ** 5;
|
|
71
|
+
const z7 = z ** 7;
|
|
72
|
+
return z + (z3 + z) / (4 * df) + (5 * z5 + 16 * z3 + 3 * z) / (96 * df ** 2) + (3 * z7 + 19 * z5 + 17 * z3 - 15 * z) / (384 * df ** 3);
|
|
73
|
+
}
|
|
74
|
+
function predictionBand(fit, xs, level = 0.8) {
|
|
75
|
+
const coverage = Math.max(0.01, Math.min(0.999, level));
|
|
76
|
+
const t = fit.n > 2 ? studentTQuantile(1 - (1 - coverage) / 2, fit.n - 2) : 0;
|
|
77
|
+
return xs.filter(finite).map((x) => {
|
|
78
|
+
const y = fit.intercept + fit.slope * x;
|
|
79
|
+
const spread = t * fit.residualStdError * Math.sqrt(1 + 1 / fit.n + (x - fit.xMean) ** 2 / fit.sxx);
|
|
80
|
+
return { x, y, lower: y - spread, upper: y + spread };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
linearFit,
|
|
85
|
+
normalQuantile,
|
|
86
|
+
predictionBand,
|
|
87
|
+
quantiles,
|
|
88
|
+
studentTQuantile
|
|
89
|
+
};
|
package/dist/chart-theme.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ interface UixChartTokens {
|
|
|
15
15
|
event: string;
|
|
16
16
|
partial: string;
|
|
17
17
|
comparison: string;
|
|
18
|
+
/** Status hues for an emphasised mark (a point that breaches); never a series colour. */
|
|
19
|
+
warning: string;
|
|
20
|
+
danger: string;
|
|
18
21
|
surface: string;
|
|
19
22
|
text: string;
|
|
20
23
|
textMuted: string;
|
package/dist/chart-theme.js
CHANGED