@fasl-work/caos-app-shell 0.18.0 → 0.19.1
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/dist/chart.d.ts +43 -10
- package/dist/chart.js +597 -85
- package/dist/chunk-FXPSZD6X.js +686 -0
- package/dist/index.d.ts +36 -5
- package/dist/index.js +499 -66
- package/dist/{text-DyQ1A8oo.d.ts → tokens-BvU9bFPw.d.ts} +25 -22
- package/gate/inpage.mjs +169 -0
- package/gate/run.mjs +12 -0
- package/package.json +1 -1
- package/reserved-classes.json +22 -0
- package/styles.css +41 -0
- package/dist/chunk-DBCMAL2G.js +0 -289
package/dist/chart.js
CHANGED
|
@@ -1,10 +1,210 @@
|
|
|
1
|
-
import { pick, useShellLang, useThemeStore, useStageSize, resolveToken, NBSP, formatNumber,
|
|
2
|
-
export { referenceLineName } from './chunk-
|
|
1
|
+
import { pick, placeXLabel, placeYLabel, useShellLang, useThemeStore, useStageSize, defaultChartName, axisTitleFits, resolveToken, dateNames, DRAG_THRESHOLD, sameData, NBSP, formatNumber, thinRows, chartSummary, referenceLineName, ZOOM_HINT, CHART_ROLE, ChartSummary, ChartDataTable, ResetZoomButton, formatTicks, countOverlaps, isZoomed, formatTime, keyAction, KEY_PAN, KEY_ZOOM, firstVisible, stepIndex, wheelFactor, pinchState, repeatedLabels, panRange, zoomRange, revealValue, brushRange, leastLabelSpan, MIN_SPAN_SHARE } from './chunk-FXPSZD6X.js';
|
|
2
|
+
export { referenceLineName, repeatedLabels } from './chunk-FXPSZD6X.js';
|
|
3
3
|
import './chunk-33WTUQ7Q.js';
|
|
4
|
-
import { useRef, useCallback, useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { useRef, useCallback, useState, useId, useEffect, useMemo } from 'react';
|
|
5
5
|
import uPlot from 'uplot';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
|
+
// src/chart/uplotZoom.ts
|
|
9
|
+
var rangeOf = (u, key) => [u.scales[key].min ?? 0, u.scales[key].max ?? 1];
|
|
10
|
+
function createZoom(u, ctx) {
|
|
11
|
+
const differs = (a, b, full) => {
|
|
12
|
+
const tol = Math.abs(full[1] - full[0]) * 1e-9;
|
|
13
|
+
return Math.abs(a[0] - b[0]) > tol || Math.abs(a[1] - b[1]) > tol;
|
|
14
|
+
};
|
|
15
|
+
const apply = (nx, ny) => {
|
|
16
|
+
const full = ctx.full();
|
|
17
|
+
const cx = rangeOf(u, "x");
|
|
18
|
+
const cy = rangeOf(u, "y");
|
|
19
|
+
const moved = Boolean(nx && differs(nx, cx, full ? full.x : cx) || ny && differs(ny, cy, full ? full.y : cy));
|
|
20
|
+
if (!moved) return false;
|
|
21
|
+
ctx.explicit.current = true;
|
|
22
|
+
try {
|
|
23
|
+
u.batch(() => {
|
|
24
|
+
if (nx) u.setScale("x", { min: nx[0], max: nx[1] });
|
|
25
|
+
if (ny) u.setScale("y", { min: ny[0], max: ny[1] });
|
|
26
|
+
});
|
|
27
|
+
} finally {
|
|
28
|
+
ctx.explicit.current = false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
const isNow = () => {
|
|
33
|
+
const full = ctx.full();
|
|
34
|
+
if (!full) return false;
|
|
35
|
+
return isZoomed(rangeOf(u, "x"), full.x) || ctx.both && isZoomed(rangeOf(u, "y"), full.y);
|
|
36
|
+
};
|
|
37
|
+
const leastOf = (axis, full) => {
|
|
38
|
+
if (ctx.log[axis]) return void 0;
|
|
39
|
+
return Math.max(Math.abs(full[1] - full[0]) * MIN_SPAN_SHARE, ctx.least?.(axis) ?? 0);
|
|
40
|
+
};
|
|
41
|
+
const zoomBy = (factor, ax, ay) => {
|
|
42
|
+
const full = ctx.full();
|
|
43
|
+
if (!full || !ctx.enabled()) return false;
|
|
44
|
+
const nx = zoomRange(rangeOf(u, "x"), full.x, ax, factor, { log: ctx.log.x, minSpan: leastOf("x", full.x) });
|
|
45
|
+
const ny = ctx.both && ay !== void 0 ? zoomRange(rangeOf(u, "y"), full.y, ay, factor, { log: ctx.log.y, minSpan: leastOf("y", full.y) }) : null;
|
|
46
|
+
return apply(nx, ny);
|
|
47
|
+
};
|
|
48
|
+
const panBy = (shareX, shareY = 0) => {
|
|
49
|
+
const full = ctx.full();
|
|
50
|
+
if (!full || !ctx.enabled()) return false;
|
|
51
|
+
const nx = shareX ? panRange(rangeOf(u, "x"), full.x, shareX, ctx.log.x) : null;
|
|
52
|
+
const ny = ctx.both && shareY ? panRange(rangeOf(u, "y"), full.y, shareY, ctx.log.y) : null;
|
|
53
|
+
return apply(nx, ny);
|
|
54
|
+
};
|
|
55
|
+
const brush = (sx, sy) => {
|
|
56
|
+
const full = ctx.full();
|
|
57
|
+
if (!full || !ctx.enabled()) return false;
|
|
58
|
+
const nx = sx ? brushRange(sx, full.x, leastOf("x", full.x) ?? 0) : null;
|
|
59
|
+
const ny = ctx.both && sy ? brushRange(sy, full.y, leastOf("y", full.y) ?? 0) : null;
|
|
60
|
+
return apply(nx, ny);
|
|
61
|
+
};
|
|
62
|
+
const reveal = (x) => {
|
|
63
|
+
const full = ctx.full();
|
|
64
|
+
if (!full || !isNow()) return;
|
|
65
|
+
const cur = rangeOf(u, "x");
|
|
66
|
+
const next = revealValue(cur, full.x, x, ctx.log.x);
|
|
67
|
+
if (next[0] !== cur[0] || next[1] !== cur[1]) apply(next, null);
|
|
68
|
+
};
|
|
69
|
+
const reset = () => {
|
|
70
|
+
const full = ctx.full();
|
|
71
|
+
if (!full) return;
|
|
72
|
+
apply(full.x, ctx.both ? full.y : null);
|
|
73
|
+
};
|
|
74
|
+
const place = (idx) => {
|
|
75
|
+
const d = u.data;
|
|
76
|
+
const xv = d[0][idx];
|
|
77
|
+
if (xv === null || xv === void 0) return;
|
|
78
|
+
const left = u.valToPos(xv, "x");
|
|
79
|
+
let top = u.bbox.height / (window.devicePixelRatio || 1) / 2;
|
|
80
|
+
for (let k = 1; k < d.length; k++) {
|
|
81
|
+
const v = d[k][idx];
|
|
82
|
+
if (v !== null && v !== void 0 && Number.isFinite(v)) {
|
|
83
|
+
top = u.valToPos(v, "y");
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
u.setCursor({ left, top }, true);
|
|
88
|
+
};
|
|
89
|
+
const clear = () => u.setCursor({ left: -10, top: -10 }, true);
|
|
90
|
+
const over = u.over;
|
|
91
|
+
const onHandle = (e) => e.target instanceof Element && e.target.closest("[data-y-handle]") !== null;
|
|
92
|
+
const ac = new AbortController();
|
|
93
|
+
const opt = { signal: ac.signal };
|
|
94
|
+
const sizeOf = () => {
|
|
95
|
+
const r = over.getBoundingClientRect();
|
|
96
|
+
return { w: Math.max(1, r.width), h: Math.max(1, r.height), left: r.left, top: r.top };
|
|
97
|
+
};
|
|
98
|
+
over.addEventListener(
|
|
99
|
+
"wheel",
|
|
100
|
+
(e) => {
|
|
101
|
+
if (!ctx.enabled()) return;
|
|
102
|
+
if (!ctx.plot.contains(document.activeElement)) return;
|
|
103
|
+
const s = sizeOf();
|
|
104
|
+
const changed = Math.abs(e.deltaX) > Math.abs(e.deltaY) && isNow() ? panBy(e.deltaX / s.w) : zoomBy(wheelFactor(e.deltaY, e.deltaMode), u.posToVal(e.clientX - s.left, "x"), ctx.both ? u.posToVal(e.clientY - s.top, "y") : void 0);
|
|
105
|
+
if (changed) e.preventDefault();
|
|
106
|
+
},
|
|
107
|
+
{ ...opt, passive: false }
|
|
108
|
+
);
|
|
109
|
+
u.root.addEventListener(
|
|
110
|
+
"mousedown",
|
|
111
|
+
(e) => {
|
|
112
|
+
if (e.shiftKey && e.button === 0 && ctx.enabled() && !onHandle(e)) {
|
|
113
|
+
e.stopPropagation();
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{ ...opt, capture: true }
|
|
118
|
+
);
|
|
119
|
+
const pts = /* @__PURE__ */ new Map();
|
|
120
|
+
let pan = null;
|
|
121
|
+
let pinch = null;
|
|
122
|
+
const startPan = (id, x, y) => {
|
|
123
|
+
pan = { id, x, y };
|
|
124
|
+
try {
|
|
125
|
+
over.setPointerCapture(id);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
over.addEventListener(
|
|
130
|
+
"pointerdown",
|
|
131
|
+
(e) => {
|
|
132
|
+
if (onHandle(e)) return;
|
|
133
|
+
ctx.plot.focus({ preventScroll: true });
|
|
134
|
+
if (!ctx.enabled()) return;
|
|
135
|
+
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
136
|
+
if (e.pointerType === "mouse") {
|
|
137
|
+
if (e.button === 0 && e.shiftKey) startPan(e.pointerId, e.clientX, e.clientY);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (pts.size === 2) {
|
|
141
|
+
const [a, b] = [...pts.values()];
|
|
142
|
+
pinch = { dist: pinchState(a, b).dist };
|
|
143
|
+
pan = null;
|
|
144
|
+
} else if (pts.size === 1 && isNow()) startPan(e.pointerId, e.clientX, e.clientY);
|
|
145
|
+
},
|
|
146
|
+
opt
|
|
147
|
+
);
|
|
148
|
+
over.addEventListener(
|
|
149
|
+
"pointermove",
|
|
150
|
+
(e) => {
|
|
151
|
+
if (!pts.has(e.pointerId)) return;
|
|
152
|
+
const before = pts.get(e.pointerId);
|
|
153
|
+
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
154
|
+
const s = sizeOf();
|
|
155
|
+
if (pinch && pts.size === 2) {
|
|
156
|
+
const [a, b] = [...pts.values()];
|
|
157
|
+
const now = pinchState(a, b);
|
|
158
|
+
if (pinch.dist > 0 && now.dist > 0) {
|
|
159
|
+
zoomBy(pinch.dist / now.dist, u.posToVal(now.mx - s.left, "x"), ctx.both ? u.posToVal(now.my - s.top, "y") : void 0);
|
|
160
|
+
pinch.dist = now.dist;
|
|
161
|
+
}
|
|
162
|
+
panBy(-(e.clientX - before.x) / 2 / s.w, ctx.both ? (e.clientY - before.y) / 2 / s.h : 0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (pan && pan.id === e.pointerId) {
|
|
166
|
+
panBy(-(e.clientX - pan.x) / s.w, ctx.both ? (e.clientY - pan.y) / s.h : 0);
|
|
167
|
+
pan.x = e.clientX;
|
|
168
|
+
pan.y = e.clientY;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
opt
|
|
172
|
+
);
|
|
173
|
+
const end = (e) => {
|
|
174
|
+
pts.delete(e.pointerId);
|
|
175
|
+
if (pan && pan.id === e.pointerId) {
|
|
176
|
+
pan = null;
|
|
177
|
+
if (over.hasPointerCapture?.(e.pointerId)) over.releasePointerCapture(e.pointerId);
|
|
178
|
+
}
|
|
179
|
+
if (pts.size < 2) pinch = null;
|
|
180
|
+
if (pts.size === 1 && !pan && isNow()) {
|
|
181
|
+
const [[id, p]] = [...pts.entries()];
|
|
182
|
+
startPan(id, p.x, p.y);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
over.addEventListener("pointerup", end, opt);
|
|
186
|
+
over.addEventListener("pointercancel", end, opt);
|
|
187
|
+
over.addEventListener(
|
|
188
|
+
"dblclick",
|
|
189
|
+
(e) => {
|
|
190
|
+
if (onHandle(e) || !ctx.enabled()) return;
|
|
191
|
+
reset();
|
|
192
|
+
},
|
|
193
|
+
opt
|
|
194
|
+
);
|
|
195
|
+
return {
|
|
196
|
+
zoomBy,
|
|
197
|
+
panBy,
|
|
198
|
+
brush,
|
|
199
|
+
reveal,
|
|
200
|
+
reset,
|
|
201
|
+
view: () => ({ x: rangeOf(u, "x"), y: rangeOf(u, "y") }),
|
|
202
|
+
place,
|
|
203
|
+
clear,
|
|
204
|
+
zoomed: isNow,
|
|
205
|
+
detach: () => ac.abort()
|
|
206
|
+
};
|
|
207
|
+
}
|
|
8
208
|
var YMARK_TONE_TOKENS = {
|
|
9
209
|
warn: "--color-warn",
|
|
10
210
|
bad: "--color-bad",
|
|
@@ -197,19 +397,16 @@ var DECADE_AXIS = {
|
|
|
197
397
|
};
|
|
198
398
|
var PARITY_TICK_SPACE = 40;
|
|
199
399
|
var HANDLE_HEIGHT = 14;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
|
|
203
|
-
return n;
|
|
204
|
-
}
|
|
205
|
-
function drawMarks(u, marks, style) {
|
|
400
|
+
var CUT_SEP = String.fromCharCode(1);
|
|
401
|
+
function drawMarks(u, marks, style, boxes = []) {
|
|
206
402
|
const ctx = u.ctx;
|
|
207
403
|
const pr = uPlot.pxRatio || 1;
|
|
208
404
|
const left = u.bbox.left;
|
|
209
405
|
const right = u.bbox.left + u.bbox.width;
|
|
210
406
|
const gap = 4 * pr;
|
|
211
407
|
const rowH = 14 * pr;
|
|
212
|
-
const
|
|
408
|
+
const textH = 11 * pr;
|
|
409
|
+
const cut = [];
|
|
213
410
|
ctx.save();
|
|
214
411
|
ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
|
|
215
412
|
ctx.textBaseline = "alphabetic";
|
|
@@ -225,24 +422,25 @@ function drawMarks(u, marks, style) {
|
|
|
225
422
|
ctx.lineTo(px, u.bbox.top + u.bbox.height);
|
|
226
423
|
ctx.stroke();
|
|
227
424
|
ctx.setLineDash([]);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
425
|
+
if (!m.label) continue;
|
|
426
|
+
const at = placeXLabel(m.label, { px, left, right, bottom: u.bbox.top + u.bbox.height, gap, rowH, textH, first: u.bbox.top + 12 * pr, boxes, measure: (t) => ctx.measureText(t).width });
|
|
427
|
+
if (!at) {
|
|
428
|
+
cut.push(m.label);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (at.cut) cut.push(m.label);
|
|
432
|
+
boxes.push({ kind: "x", text: at.text, full: m.label, x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
|
|
236
433
|
ctx.textAlign = "left";
|
|
237
434
|
ctx.lineWidth = 3 * pr;
|
|
238
435
|
ctx.strokeStyle = style.halo;
|
|
239
|
-
ctx.strokeText(
|
|
436
|
+
ctx.strokeText(at.text, at.x0, at.y);
|
|
240
437
|
ctx.fillStyle = style.color;
|
|
241
|
-
ctx.fillText(
|
|
438
|
+
ctx.fillText(at.text, at.x0, at.y);
|
|
242
439
|
}
|
|
243
440
|
ctx.restore();
|
|
441
|
+
return cut;
|
|
244
442
|
}
|
|
245
|
-
function drawYMarks(u, marks, style) {
|
|
443
|
+
function drawYMarks(u, marks, style, boxes) {
|
|
246
444
|
const ctx = u.ctx;
|
|
247
445
|
const pr = uPlot.pxRatio || 1;
|
|
248
446
|
const left = u.bbox.left;
|
|
@@ -250,8 +448,10 @@ function drawYMarks(u, marks, style) {
|
|
|
250
448
|
const top = u.bbox.top;
|
|
251
449
|
const bottom = u.bbox.top + u.bbox.height;
|
|
252
450
|
const gap = 4 * pr;
|
|
451
|
+
const rowH = 14 * pr;
|
|
253
452
|
const textH = 11 * pr;
|
|
254
453
|
const placed = [];
|
|
454
|
+
const cut = [];
|
|
255
455
|
ctx.save();
|
|
256
456
|
ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
|
|
257
457
|
ctx.textBaseline = "alphabetic";
|
|
@@ -269,22 +469,23 @@ function drawYMarks(u, marks, style) {
|
|
|
269
469
|
ctx.stroke();
|
|
270
470
|
ctx.setLineDash([]);
|
|
271
471
|
if (!m.label) continue;
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
x1 = Math.min(...placed.filter((p) => base - textH < p.y1 && base > p.y0).map((p) => p.x0)) - 2 * gap;
|
|
472
|
+
const at = placeYLabel(m.label, { py, left, right, top, bottom, gap, rowH, textH, boxes: [...placed, ...boxes ?? []], measure: (t) => ctx.measureText(t).width });
|
|
473
|
+
if (!at) {
|
|
474
|
+
cut.push(m.label);
|
|
475
|
+
continue;
|
|
277
476
|
}
|
|
278
|
-
|
|
279
|
-
placed.push({ x0, x1: x0 + w, y0:
|
|
477
|
+
if (at.cut) cut.push(m.label);
|
|
478
|
+
placed.push({ x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
|
|
479
|
+
boxes?.push({ kind: "y", text: at.text, full: m.label, x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
|
|
280
480
|
ctx.textAlign = "left";
|
|
281
481
|
ctx.lineWidth = 3 * pr;
|
|
282
482
|
ctx.strokeStyle = style.halo;
|
|
283
|
-
ctx.strokeText(
|
|
483
|
+
ctx.strokeText(at.text, at.x0, at.y);
|
|
284
484
|
ctx.fillStyle = color;
|
|
285
|
-
ctx.fillText(
|
|
485
|
+
ctx.fillText(at.text, at.x0, at.y);
|
|
286
486
|
}
|
|
287
487
|
ctx.restore();
|
|
488
|
+
return cut;
|
|
288
489
|
}
|
|
289
490
|
function nearestPoint(xs, ys, left, top) {
|
|
290
491
|
let best = null;
|
|
@@ -303,7 +504,7 @@ function nearestPoint(xs, ys, left, top) {
|
|
|
303
504
|
}
|
|
304
505
|
return best;
|
|
305
506
|
}
|
|
306
|
-
function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo }) {
|
|
507
|
+
function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo, title, zoomY, zoomOff }) {
|
|
307
508
|
const lang = useShellLang();
|
|
308
509
|
const theme = useThemeStore((s) => s.theme);
|
|
309
510
|
const fill = height === "fill";
|
|
@@ -319,8 +520,21 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
319
520
|
const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
|
|
320
521
|
const h = parity ? width : fill ? box.height : height;
|
|
321
522
|
const uRef = useRef(null);
|
|
523
|
+
const ctlRef = useRef(null);
|
|
524
|
+
const explicitRef = useRef(false);
|
|
322
525
|
const hostRef = useRef(null);
|
|
323
526
|
const [cursor, setCursor] = useState(null);
|
|
527
|
+
const ids = useId();
|
|
528
|
+
const tableId = `${ids}-table`;
|
|
529
|
+
const summaryId = `${ids}-summary`;
|
|
530
|
+
const [zoomed, setZoomed] = useState(false);
|
|
531
|
+
const fullRef = useRef(null);
|
|
532
|
+
const zoomRef = useRef({ x: null, y: null });
|
|
533
|
+
const zoomedRef = useRef(false);
|
|
534
|
+
const kbRef = useRef(null);
|
|
535
|
+
const dataRef = useRef(null);
|
|
536
|
+
const [forced, setForced] = useState("");
|
|
537
|
+
const [cutKey, setCutKey] = useState("");
|
|
324
538
|
const [soloPick, setSoloPick] = useState(null);
|
|
325
539
|
const soloFound = solo && soloPick !== null ? series.findIndex((s) => soloKey(s) === soloPick) : -1;
|
|
326
540
|
const soloIdx = soloFound >= 0 ? soloFound : null;
|
|
@@ -369,6 +583,20 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
369
583
|
const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
|
|
370
584
|
const xUnit = x.unit ? pick(x.unit, lang) : "";
|
|
371
585
|
const yUnit = y.unit ? pick(y.unit, lang) : "";
|
|
586
|
+
const drawn = width > 0 && h > 0;
|
|
587
|
+
const name = title ? pick(title, lang) : defaultChartName(lang, yLabel, xLabel);
|
|
588
|
+
const zoomIsOff = zoomOff !== void 0 && zoomOff !== null;
|
|
589
|
+
const zoomWhy = zoomIsOff ? pick(zoomOff, lang) : "";
|
|
590
|
+
const zoomable = !zoomIsOff && xs.length >= 2 && xs[xs.length - 1] > xs[0];
|
|
591
|
+
const both = Boolean(zoomY || parity);
|
|
592
|
+
const zoomableRef = useRef(zoomable);
|
|
593
|
+
zoomableRef.current = zoomable;
|
|
594
|
+
const bothRef = useRef(both);
|
|
595
|
+
bothRef.current = both;
|
|
596
|
+
const xTitle = xUnit ? `${xLabel} (${xUnit})` : xLabel;
|
|
597
|
+
const yTitle = yUnit ? `${yLabel} (${yUnit})` : yLabel;
|
|
598
|
+
const yAbove = !axisTitleFits(yTitle, h - 44) || forced === yTitle;
|
|
599
|
+
const xBelow = !axisTitleFits(xTitle, width - 16);
|
|
372
600
|
const structure = JSON.stringify({
|
|
373
601
|
s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s), Boolean(s.dots)]),
|
|
374
602
|
x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt, ticks ? ticks.map((t) => [t.value, pick(t.label, lang)]) : null],
|
|
@@ -381,9 +609,19 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
381
609
|
theme,
|
|
382
610
|
lang,
|
|
383
611
|
h,
|
|
384
|
-
w: width
|
|
612
|
+
w: width,
|
|
613
|
+
ta: [yAbove, xBelow, zoomable, both]
|
|
385
614
|
});
|
|
386
615
|
const data = useMemo(() => [xs, ...ys], [xs, ys]);
|
|
616
|
+
const leastSpan = (axis) => {
|
|
617
|
+
const u = uRef.current;
|
|
618
|
+
const f = fullRef.current;
|
|
619
|
+
if (!u || !f) return void 0;
|
|
620
|
+
if (axis === "x" ? x.time || x.log || Boolean(ticks) : Boolean(y.log)) return void 0;
|
|
621
|
+
const pr = uPlot.pxRatio || 1;
|
|
622
|
+
const room = (axis === "x" ? u.bbox.width : u.bbox.height) / pr;
|
|
623
|
+
return leastLabelSpan(axis === "x" ? x.format : y.format, f[axis], room, range ? PARITY_TICK_SPACE : axis === "x" ? 50 : 30);
|
|
624
|
+
};
|
|
387
625
|
const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
|
|
388
626
|
useEffect(() => {
|
|
389
627
|
const el = plotRef.current;
|
|
@@ -415,13 +653,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
415
653
|
return labels;
|
|
416
654
|
};
|
|
417
655
|
const near = { left: NaN, top: NaN, idx: null };
|
|
418
|
-
const nearest = (
|
|
419
|
-
const { left, top } =
|
|
656
|
+
const nearest = (u2) => {
|
|
657
|
+
const { left, top } = u2.cursor;
|
|
420
658
|
if (left === void 0 || top === void 0 || left < 0 || top < 0) return null;
|
|
421
659
|
if (left !== near.left || top !== near.top) {
|
|
422
|
-
const d =
|
|
423
|
-
const px = d[0].map((v) => v === null ? NaN :
|
|
424
|
-
const py = d.slice(1).map((col) => col.map((v) => v === null ? null :
|
|
660
|
+
const d = u2.data;
|
|
661
|
+
const px = d[0].map((v) => v === null ? NaN : u2.valToPos(v, "x"));
|
|
662
|
+
const py = d.slice(1).map((col) => col.map((v) => v === null ? null : u2.valToPos(v, "y")));
|
|
425
663
|
near.left = left;
|
|
426
664
|
near.top = top;
|
|
427
665
|
near.idx = nearestPoint(px, py, left, top);
|
|
@@ -430,11 +668,20 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
430
668
|
};
|
|
431
669
|
const xSteps = x.time || x.log ? void 0 : tickSteps(x.format);
|
|
432
670
|
const ySteps = y.log ? void 0 : tickSteps(y.format);
|
|
671
|
+
const fixedRange = (r) => (_u, min, max) => explicitRef.current && min !== null && max !== null ? [min, max] : [r[0], r[1]];
|
|
433
672
|
const opts = {
|
|
434
673
|
width,
|
|
435
674
|
height: h,
|
|
675
|
+
// the month and weekday names of a time axis in the page language: uPlot's own are English (0.19.1, SHC-099)
|
|
676
|
+
...x.time ? { fmtDate: /* @__PURE__ */ ((names) => (tpl) => uPlot.fmtDate(tpl, names))(dateNames(lang)) } : {},
|
|
436
677
|
legend: { show: false },
|
|
437
|
-
|
|
678
|
+
cursor: {
|
|
679
|
+
...scatter ? { dataIdx: (u2, sIdx, closest) => sIdx === 0 ? closest : nearest(u2) ?? closest } : {},
|
|
680
|
+
// the brush: a drag along x (and y where both axes zoom); a drag under DRAG_THRESHOLD px is a click, not a zoom. uPlot's own setScale
|
|
681
|
+
// is off: a fixed range (`range`, `x.range`) turns the scale it sets back into the fixed one, so the selection is applied by the
|
|
682
|
+
// `setSelect` hook below, through the same operation as the wheel and the keys
|
|
683
|
+
drag: zoomable ? { x: true, y: both, dist: DRAG_THRESHOLD, setScale: false } : { x: false, y: false, setScale: false }
|
|
684
|
+
},
|
|
438
685
|
// a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
|
|
439
686
|
scales: {
|
|
440
687
|
// a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
|
|
@@ -442,16 +689,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
442
689
|
x: {
|
|
443
690
|
time: Boolean(x.time),
|
|
444
691
|
distr: x.log ? 3 : 1,
|
|
445
|
-
|
|
692
|
+
// a fixed range is a function that gives way to a zoom: uPlot passes even an explicit range through the scale's range, so a constant
|
|
693
|
+
// (or an array) undid every zoom of a chart with `x.range` or `y.range` (Tier A, 0.19.0)
|
|
694
|
+
...range ? { range: fixedRange(range) } : xExt ? { range: fixedRange(xExt) } : x.log ? { range: (_u, min, max) => [min, max] } : {}
|
|
446
695
|
},
|
|
447
696
|
y: {
|
|
448
697
|
distr: y.log ? 3 : 1,
|
|
449
|
-
...range ? { range } : y.range ? { range: y.range } : yLevels ? {
|
|
698
|
+
...range ? { range: fixedRange(range) } : y.range ? { range: fixedRange(y.range) } : yLevels ? {
|
|
450
699
|
// uPlot's own range of the data and the levels: its padding, its decade snap on a log axis
|
|
451
|
-
range: (
|
|
700
|
+
range: (u2, dmin, dmax) => {
|
|
452
701
|
const w = rangeWithLevels(dmin, dmax, yLevels, Boolean(y.log), 0.05);
|
|
453
702
|
if (!w) return [dmin, dmax];
|
|
454
|
-
return y.log ? uPlot.rangeLog(w[0], w[1],
|
|
703
|
+
return y.log ? uPlot.rangeLog(w[0], w[1], u2.scales.y.log ?? 10, false) : uPlot.rangeNum(w[0], w[1], 0.1, true);
|
|
455
704
|
}
|
|
456
705
|
} : {}
|
|
457
706
|
}
|
|
@@ -462,7 +711,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
462
711
|
grid: { stroke: grid, width: 1 },
|
|
463
712
|
ticks: { stroke: grid },
|
|
464
713
|
font,
|
|
465
|
-
label:
|
|
714
|
+
label: xBelow ? void 0 : xTitle,
|
|
466
715
|
labelFont: font,
|
|
467
716
|
// a log x axis labels the decades only; the ticks between stay drawn, unlabelled
|
|
468
717
|
values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
|
|
@@ -480,7 +729,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
480
729
|
grid: { stroke: grid, width: 1 },
|
|
481
730
|
ticks: { stroke: grid },
|
|
482
731
|
font,
|
|
483
|
-
label:
|
|
732
|
+
label: yAbove ? void 0 : yTitle,
|
|
484
733
|
labelFont: font,
|
|
485
734
|
values: tick(y.format, "y"),
|
|
486
735
|
...ySteps ? { incrs: ySteps } : {},
|
|
@@ -489,12 +738,12 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
489
738
|
// Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
|
|
490
739
|
// label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
|
|
491
740
|
// made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
|
|
492
|
-
size: (
|
|
741
|
+
size: (u2, values) => {
|
|
493
742
|
if (!values || values.length === 0) return 48;
|
|
494
743
|
const pr = uPlot.pxRatio || 1;
|
|
495
|
-
|
|
496
|
-
const widest = Math.max(...values.map((v) =>
|
|
497
|
-
return Math.ceil(widest / pr) + 30;
|
|
744
|
+
u2.ctx.font = `${Math.round(11 * pr)}px ${family}`;
|
|
745
|
+
const widest = Math.max(...values.map((v) => u2.ctx.measureText(v ?? "").width));
|
|
746
|
+
return Math.ceil(widest / pr) + (yAbove ? 18 : 30);
|
|
498
747
|
}
|
|
499
748
|
}
|
|
500
749
|
],
|
|
@@ -509,16 +758,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
509
758
|
],
|
|
510
759
|
hooks: {
|
|
511
760
|
setCursor: [
|
|
512
|
-
(
|
|
513
|
-
const idx = scatter ? nearest(
|
|
761
|
+
(u2) => {
|
|
762
|
+
const idx = scatter ? nearest(u2) : u2.cursor.idx ?? null;
|
|
514
763
|
pickedRef.current = idx;
|
|
515
764
|
setCursor(idx);
|
|
516
765
|
onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
|
|
517
766
|
}
|
|
518
767
|
],
|
|
519
768
|
ready: [
|
|
520
|
-
(
|
|
521
|
-
|
|
769
|
+
(u2) => {
|
|
770
|
+
u2.over.addEventListener("click", () => {
|
|
522
771
|
const idx = pickedRef.current;
|
|
523
772
|
if (idx === null || idx === void 0 || !onPickRef.current) return;
|
|
524
773
|
onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
|
|
@@ -543,7 +792,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
543
792
|
if (v === liveRef.current[i]) return;
|
|
544
793
|
liveRef.current[i] = v;
|
|
545
794
|
onYMarkChangeRef.current?.(i, v);
|
|
546
|
-
|
|
795
|
+
u2.redraw(false);
|
|
547
796
|
};
|
|
548
797
|
let dragging = false;
|
|
549
798
|
el2.addEventListener("pointerdown", (e) => {
|
|
@@ -555,7 +804,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
555
804
|
});
|
|
556
805
|
el2.addEventListener("pointermove", (e) => {
|
|
557
806
|
if (!dragging) return;
|
|
558
|
-
set(
|
|
807
|
+
set(u2.posToVal(e.clientY - u2.over.getBoundingClientRect().top, "y"));
|
|
559
808
|
});
|
|
560
809
|
const end = (e) => {
|
|
561
810
|
dragging = false;
|
|
@@ -569,68 +818,100 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
569
818
|
e.preventDefault();
|
|
570
819
|
set(v);
|
|
571
820
|
});
|
|
572
|
-
|
|
821
|
+
u2.over.appendChild(el2);
|
|
573
822
|
});
|
|
574
|
-
if (dragCount) queueMicrotask(() => uRef.current ===
|
|
823
|
+
if (dragCount) queueMicrotask(() => uRef.current === u2 && u2.redraw(false));
|
|
824
|
+
}
|
|
825
|
+
],
|
|
826
|
+
setSelect: [
|
|
827
|
+
(u2) => {
|
|
828
|
+
const sel = { left: u2.select.left, top: u2.select.top, width: u2.select.width, height: u2.select.height };
|
|
829
|
+
if (!(sel.width > 0 || sel.height > 0)) return;
|
|
830
|
+
const x0 = u2.posToVal(sel.left, "x");
|
|
831
|
+
const x1 = u2.posToVal(sel.left + sel.width, "x");
|
|
832
|
+
const y0 = u2.posToVal(sel.top + sel.height, "y");
|
|
833
|
+
const y1 = u2.posToVal(sel.top, "y");
|
|
834
|
+
u2.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
|
|
835
|
+
ctlRef.current?.brush(sel.width > 0 ? [x0, x1] : null, bothRef.current && sel.height > 0 ? [y0, y1] : void 0);
|
|
836
|
+
}
|
|
837
|
+
],
|
|
838
|
+
setScale: [
|
|
839
|
+
(u2, key) => {
|
|
840
|
+
if (key !== "x" && key !== "y") return;
|
|
841
|
+
const f2 = fullRef.current;
|
|
842
|
+
if (!f2) return;
|
|
843
|
+
const rx = [u2.scales.x.min ?? f2.x[0], u2.scales.x.max ?? f2.x[1]];
|
|
844
|
+
const ry = [u2.scales.y.min ?? f2.y[0], u2.scales.y.max ?? f2.y[1]];
|
|
845
|
+
hostRef.current?.setAttribute("data-view-x", `${rx[0]},${rx[1]}`);
|
|
846
|
+
const zx = isZoomed(rx, f2.x);
|
|
847
|
+
const zy = bothRef.current && isZoomed(ry, f2.y);
|
|
848
|
+
zoomRef.current = { x: zx ? rx : null, y: zy ? ry : null };
|
|
849
|
+
const z = zx || zy;
|
|
850
|
+
if (z !== zoomedRef.current) {
|
|
851
|
+
zoomedRef.current = z;
|
|
852
|
+
setZoomed(z);
|
|
853
|
+
}
|
|
854
|
+
u2.over.style.touchAction = zoomableRef.current ? z ? "none" : "pan-y" : "";
|
|
575
855
|
}
|
|
576
856
|
],
|
|
577
857
|
draw: [
|
|
578
|
-
(
|
|
858
|
+
(u2) => {
|
|
579
859
|
if (range) {
|
|
580
|
-
const ctx =
|
|
581
|
-
const
|
|
860
|
+
const ctx = u2.ctx;
|
|
861
|
+
const pr2 = uPlot.pxRatio || 1;
|
|
582
862
|
ctx.save();
|
|
583
863
|
ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
|
|
584
|
-
ctx.lineWidth = Math.max(1,
|
|
585
|
-
ctx.setLineDash([5 *
|
|
864
|
+
ctx.lineWidth = Math.max(1, pr2);
|
|
865
|
+
ctx.setLineDash([5 * pr2, 4 * pr2]);
|
|
586
866
|
ctx.beginPath();
|
|
587
|
-
ctx.moveTo(
|
|
588
|
-
ctx.lineTo(
|
|
867
|
+
ctx.moveTo(u2.valToPos(range[0], "x", true), u2.valToPos(range[0], "y", true));
|
|
868
|
+
ctx.lineTo(u2.valToPos(range[1], "x", true), u2.valToPos(range[1], "y", true));
|
|
589
869
|
ctx.stroke();
|
|
590
870
|
ctx.restore();
|
|
591
871
|
}
|
|
592
872
|
const dots = overlaysRef.current;
|
|
593
873
|
if (dots.length) {
|
|
594
|
-
const ctx =
|
|
595
|
-
const
|
|
596
|
-
let
|
|
874
|
+
const ctx = u2.ctx;
|
|
875
|
+
const pr2 = uPlot.pxRatio || 1;
|
|
876
|
+
let drawn2 = 0;
|
|
597
877
|
ctx.save();
|
|
598
878
|
ctx.beginPath();
|
|
599
|
-
ctx.rect(
|
|
879
|
+
ctx.rect(u2.bbox.left, u2.bbox.top, u2.bbox.width, u2.bbox.height);
|
|
600
880
|
ctx.clip();
|
|
601
881
|
for (const { s, i } of dots) {
|
|
602
882
|
ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
|
|
603
883
|
ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
|
|
604
884
|
ctx.strokeStyle = surface;
|
|
605
|
-
ctx.lineWidth = Math.max(1,
|
|
606
|
-
const r = (s.size ?? 9) / 2 *
|
|
885
|
+
ctx.lineWidth = Math.max(1, pr2);
|
|
886
|
+
const r = (s.size ?? 9) / 2 * pr2;
|
|
607
887
|
const ox = s.x ?? [];
|
|
608
888
|
for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
|
|
609
889
|
const v = s.values[k];
|
|
610
890
|
if (v === null || v === void 0 || !Number.isFinite(v) || !Number.isFinite(ox[k])) continue;
|
|
611
|
-
const px =
|
|
612
|
-
const py =
|
|
613
|
-
if (!Number.isFinite(px) || !Number.isFinite(py) || px <
|
|
891
|
+
const px = u2.valToPos(ox[k], "x", true);
|
|
892
|
+
const py = u2.valToPos(v, "y", true);
|
|
893
|
+
if (!Number.isFinite(px) || !Number.isFinite(py) || px < u2.bbox.left - 0.5 || px > u2.bbox.left + u2.bbox.width + 0.5 || py < u2.bbox.top - 0.5 || py > u2.bbox.top + u2.bbox.height + 0.5) continue;
|
|
614
894
|
ctx.beginPath();
|
|
615
895
|
ctx.arc(px, py, r, 0, Math.PI * 2);
|
|
616
896
|
ctx.fill();
|
|
617
897
|
ctx.stroke();
|
|
618
|
-
|
|
898
|
+
drawn2++;
|
|
619
899
|
}
|
|
620
900
|
}
|
|
621
901
|
ctx.restore();
|
|
622
|
-
hostRef.current?.setAttribute("data-overlay-drawn", String(
|
|
902
|
+
hostRef.current?.setAttribute("data-overlay-drawn", String(drawn2));
|
|
623
903
|
}
|
|
624
904
|
const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
|
|
625
905
|
const lines = yMarksRef.current ?? [];
|
|
626
|
-
|
|
627
|
-
|
|
906
|
+
const boxes = [];
|
|
907
|
+
const ycut = lines.length ? drawYMarks(u2, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: m.label ? pick(m.label, lang) : void 0, color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle, boxes) : [];
|
|
908
|
+
u2.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
|
|
628
909
|
const i = Number(el2.getAttribute("data-y-handle"));
|
|
629
910
|
const m = lines[i];
|
|
630
911
|
const v = liveRef.current[i];
|
|
631
912
|
if (!m || v === void 0) return;
|
|
632
|
-
const top =
|
|
633
|
-
el2.style.display = Number.isFinite(top) && top >= 0 && top <=
|
|
913
|
+
const top = u2.valToPos(v, "y");
|
|
914
|
+
el2.style.display = Number.isFinite(top) && top >= 0 && top <= u2.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
|
|
634
915
|
el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
|
|
635
916
|
const label = m.label ? pick(m.label, lang) : "";
|
|
636
917
|
const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
|
|
@@ -639,7 +920,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
639
920
|
el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
|
|
640
921
|
el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
|
|
641
922
|
});
|
|
642
|
-
|
|
923
|
+
const cut = [...ycut, ...marks?.length ? drawMarks(u2, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle, boxes) : []];
|
|
924
|
+
const pr = uPlot.pxRatio || 1;
|
|
925
|
+
const rounded = (v) => Math.round(v / pr * 10) / 10;
|
|
926
|
+
hostRef.current?.setAttribute("data-mark-overlaps", String(countOverlaps(boxes)));
|
|
927
|
+
if (boxes.length) hostRef.current?.setAttribute("data-mark-boxes", JSON.stringify(boxes.map((b) => ({ k: b.kind, t: b.text, x0: rounded(b.x0), y0: rounded(b.y0), x1: rounded(b.x1), y1: rounded(b.y1), ...b.text !== b.full ? { cut: true } : {} }))));
|
|
928
|
+
else hostRef.current?.removeAttribute("data-mark-boxes");
|
|
929
|
+
if (cut.length) hostRef.current?.setAttribute("data-mark-cut", cut.join("|"));
|
|
930
|
+
else hostRef.current?.removeAttribute("data-mark-cut");
|
|
931
|
+
const now = cut.join(CUT_SEP);
|
|
932
|
+
setCutKey((prev) => prev === now ? prev : now);
|
|
643
933
|
}
|
|
644
934
|
]
|
|
645
935
|
}
|
|
@@ -649,14 +939,60 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
649
939
|
hostRef.current?.removeAttribute("data-ticks-repeated");
|
|
650
940
|
}
|
|
651
941
|
uRef.current?.destroy();
|
|
652
|
-
|
|
942
|
+
fullRef.current = null;
|
|
943
|
+
dataRef.current = data;
|
|
944
|
+
const u = new uPlot(opts, data, el);
|
|
945
|
+
uRef.current = u;
|
|
946
|
+
u.batch(() => void 0);
|
|
947
|
+
const f = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
|
|
948
|
+
fullRef.current = f;
|
|
949
|
+
hostRef.current?.setAttribute("data-view-x", `${f.x[0]},${f.x[1]}`);
|
|
950
|
+
const ctl = createZoom(u, { plot: el, log: { x: Boolean(x.log), y: Boolean(y.log) }, both, enabled: () => zoomableRef.current, full: () => fullRef.current, explicit: explicitRef, least: leastSpan });
|
|
951
|
+
ctlRef.current = ctl;
|
|
952
|
+
u.over.style.touchAction = zoomable ? "pan-y" : "";
|
|
953
|
+
const held = zoomRef.current;
|
|
954
|
+
const meets = (r, full) => r && r[1] > full[0] && r[0] < full[1] ? [Math.max(r[0], full[0]), Math.min(r[1], full[1])] : null;
|
|
955
|
+
const hx = zoomable ? meets(held.x, f.x) : null;
|
|
956
|
+
const hy = zoomable && both ? meets(held.y, f.y) : null;
|
|
957
|
+
if (hx || hy) {
|
|
958
|
+
explicitRef.current = true;
|
|
959
|
+
u.batch(() => {
|
|
960
|
+
if (hx) u.setScale("x", { min: hx[0], max: hx[1] });
|
|
961
|
+
if (hy) u.setScale("y", { min: hy[0], max: hy[1] });
|
|
962
|
+
});
|
|
963
|
+
explicitRef.current = false;
|
|
964
|
+
} else {
|
|
965
|
+
zoomRef.current = { x: null, y: null };
|
|
966
|
+
if (zoomedRef.current) {
|
|
967
|
+
zoomedRef.current = false;
|
|
968
|
+
setZoomed(false);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (!yAbove && yTitle) {
|
|
972
|
+
const pr = uPlot.pxRatio || 1;
|
|
973
|
+
const cy = (u.bbox.top + u.bbox.height / 2) / pr;
|
|
974
|
+
if (!axisTitleFits(yTitle, 2 * Math.min(cy, h - cy) - 4)) setForced(yTitle);
|
|
975
|
+
}
|
|
653
976
|
return () => {
|
|
977
|
+
ctl.detach();
|
|
978
|
+
ctlRef.current = null;
|
|
654
979
|
uRef.current?.destroy();
|
|
655
980
|
uRef.current = null;
|
|
656
981
|
};
|
|
657
982
|
}, [structure]);
|
|
658
983
|
useEffect(() => {
|
|
659
|
-
uRef.current
|
|
984
|
+
const u = uRef.current;
|
|
985
|
+
if (!u) return;
|
|
986
|
+
if (dataRef.current && sameData(dataRef.current, data)) return;
|
|
987
|
+
dataRef.current = data;
|
|
988
|
+
fullRef.current = null;
|
|
989
|
+
zoomRef.current = { x: null, y: null };
|
|
990
|
+
u.batch(() => u.setData(data));
|
|
991
|
+
fullRef.current = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
|
|
992
|
+
if (zoomedRef.current) {
|
|
993
|
+
zoomedRef.current = false;
|
|
994
|
+
setZoomed(false);
|
|
995
|
+
}
|
|
660
996
|
}, [data]);
|
|
661
997
|
const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
|
|
662
998
|
const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
|
|
@@ -675,11 +1011,144 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
675
1011
|
});
|
|
676
1012
|
u.redraw(false);
|
|
677
1013
|
}, [soloIdx, baseEntries]);
|
|
1014
|
+
const timeSpan = xs.length > 1 ? Math.abs(xs[xs.length - 1] - xs[0]) : 0;
|
|
1015
|
+
const xValue = (v) => x.time ? formatTime(v, lang, timeSpan) : formatNumber(v, lang, x.format ?? { digits: 4 });
|
|
678
1016
|
const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
|
|
679
1017
|
// an x that sits on a named tick is named by it, not by its number
|
|
680
|
-
tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${
|
|
1018
|
+
tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${xValue(xs[cursor])}${xUnit && !x.time ? `${NBSP}${xUnit}` : ""}`,
|
|
681
1019
|
...baseEntries.map(({ s }, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
|
|
682
1020
|
].join(" \xB7 ") : hint ? pick(hint, lang) : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
|
|
1021
|
+
const cutTexts = cutKey ? cutKey.split(CUT_SEP) : [];
|
|
1022
|
+
const readFull = cutTexts.length ? [read, ...cutTexts].join(" \xB7 ") : read;
|
|
1023
|
+
const xText = (v) => tickLabelAt(ticks, v, lang, xSpan) ?? xValue(v);
|
|
1024
|
+
const yText = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : `${formatNumber(v, lang, y.format ?? { digits: 4 })}`;
|
|
1025
|
+
const xFormatKey = JSON.stringify(x.format ?? null);
|
|
1026
|
+
const yFormatKey = JSON.stringify(y.format ?? null);
|
|
1027
|
+
const a11y = useMemo(() => {
|
|
1028
|
+
if (!drawn) return null;
|
|
1029
|
+
const head = [xTitle, ...baseEntries.map(({ s: one }) => yUnit ? `${pick(one.label, lang)} (${yUnit})` : pick(one.label, lang)), ...overlays.map(({ s: one }) => yUnit ? `${pick(one.label, lang)} (${yUnit})` : pick(one.label, lang))];
|
|
1030
|
+
const kept = thinRows(xs.length);
|
|
1031
|
+
const blanks = (n) => Array.from({ length: n }, () => "");
|
|
1032
|
+
const rows = kept.map((i) => [xText(xs[i]), ...ys.map((col) => yText(col[i])), ...blanks(overlays.length)]);
|
|
1033
|
+
overlays.forEach(({ s: one }, o) => {
|
|
1034
|
+
const ox = one.x ?? [];
|
|
1035
|
+
for (const k of thinRows(Math.min(ox.length, one.values.length))) {
|
|
1036
|
+
if (!Number.isFinite(ox[k]) || one.values[k] === null || one.values[k] === void 0) continue;
|
|
1037
|
+
const cells = [...blanks(ys.length), ...blanks(overlays.length)];
|
|
1038
|
+
cells[ys.length + o] = yText(one.values[k]);
|
|
1039
|
+
rows.push([xText(ox[k]), ...cells]);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
const multi = baseEntries.length + overlays.length > 1;
|
|
1043
|
+
let low = null;
|
|
1044
|
+
let high = null;
|
|
1045
|
+
baseEntries.forEach(({ s: one }, k) => {
|
|
1046
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1047
|
+
const v = ys[k][i];
|
|
1048
|
+
if (v === null || v === void 0 || !Number.isFinite(v)) continue;
|
|
1049
|
+
const at = `${multi ? `${pick(one.label, lang)}, ` : ""}${xLabel} ${xText(xs[i])}`;
|
|
1050
|
+
if (!low || v < low.v) low = { v, at };
|
|
1051
|
+
if (!high || v > high.v) high = { v, at };
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
const unit = yUnit ? `${NBSP}${yUnit}` : "";
|
|
1055
|
+
const lo = low;
|
|
1056
|
+
const hi = high;
|
|
1057
|
+
const summary = chartSummary(lang, {
|
|
1058
|
+
kind: "line",
|
|
1059
|
+
name,
|
|
1060
|
+
count: xs.length,
|
|
1061
|
+
series: series.map((one) => pick(one.label, lang)),
|
|
1062
|
+
xLabel,
|
|
1063
|
+
xFrom: xs.length ? xText(xs[0]) : void 0,
|
|
1064
|
+
xTo: xs.length ? xText(xs[xs.length - 1]) : void 0,
|
|
1065
|
+
yLabel: yTitle,
|
|
1066
|
+
low: lo ? { text: `${yText(lo.v)}${unit}`, at: lo.at } : void 0,
|
|
1067
|
+
high: hi ? { text: `${yText(hi.v)}${unit}`, at: hi.at } : void 0
|
|
1068
|
+
});
|
|
1069
|
+
return { head, rows, summary, thinned: kept.length < xs.length };
|
|
1070
|
+
}, [drawn, xs, ys, baseEntries, overlays, series, lang, ticks, xSpan, xTitle, yTitle, xLabel, yUnit, name, xFormatKey, yFormatKey, x.time, timeSpan]);
|
|
1071
|
+
const marksId = `${ids}-marks`;
|
|
1072
|
+
const markRows = useMemo(() => {
|
|
1073
|
+
const unnamed = (yMarks ?? []).flatMap((m, k) => m.label ? [] : [k]);
|
|
1074
|
+
const xs2 = (marks ?? []).map((m) => [pick(m.label, lang), `${xLabel} ${xText(m.x)}${xUnit && !x.time && tickLabelAt(ticks, m.x, lang, xSpan) === null ? `${NBSP}${xUnit}` : ""}`]);
|
|
1075
|
+
const ys2 = (yMarks ?? []).map((m, k) => [m.label ? pick(m.label, lang) : referenceLineName(lang, unnamed.indexOf(k) + 1, unnamed.length), `${yText(m.y)}${yUnit ? `${NBSP}${yUnit}` : ""}`]);
|
|
1076
|
+
return [...xs2, ...ys2];
|
|
1077
|
+
}, [marks, yMarks, lang, xLabel, xUnit, yUnit, ticks, xSpan, xFormatKey, yFormatKey, x.time, timeSpan]);
|
|
1078
|
+
const onKeyDown = (e) => {
|
|
1079
|
+
if (e.target !== e.currentTarget) return;
|
|
1080
|
+
const u = uRef.current;
|
|
1081
|
+
const ctl = ctlRef.current;
|
|
1082
|
+
if (!u || !ctl) return;
|
|
1083
|
+
const act = keyAction(e.key, { shift: e.shiftKey, ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
|
|
1084
|
+
if (!act) return;
|
|
1085
|
+
const n = xs.length;
|
|
1086
|
+
const stand = kbRef.current;
|
|
1087
|
+
switch (act) {
|
|
1088
|
+
case "prev":
|
|
1089
|
+
case "next":
|
|
1090
|
+
case "first":
|
|
1091
|
+
case "last": {
|
|
1092
|
+
let idx;
|
|
1093
|
+
if (stand === null && zoomedRef.current && (act === "next" || act === "prev")) {
|
|
1094
|
+
const v = ctl.view().x;
|
|
1095
|
+
let last = null;
|
|
1096
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1097
|
+
if (xs[i] >= v[0] && xs[i] <= v[1]) {
|
|
1098
|
+
last = i;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
idx = act === "next" ? firstVisible(xs, v[0], v[1]) : last;
|
|
1103
|
+
} else idx = stepIndex(stand, act, n);
|
|
1104
|
+
if (idx === null) return;
|
|
1105
|
+
kbRef.current = idx;
|
|
1106
|
+
ctl.reveal(xs[idx]);
|
|
1107
|
+
ctl.place(idx);
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
case "zoomIn":
|
|
1111
|
+
case "zoomOut": {
|
|
1112
|
+
if (!zoomableRef.current) return;
|
|
1113
|
+
const v = ctl.view();
|
|
1114
|
+
const ax = stand !== null ? xs[stand] : x.log && v.x[0] > 0 ? Math.sqrt(v.x[0] * v.x[1]) : (v.x[0] + v.x[1]) / 2;
|
|
1115
|
+
const firstY = stand !== null ? ys.map((col) => col[stand]).find((val) => val !== null && val !== void 0 && Number.isFinite(val)) : void 0;
|
|
1116
|
+
const ay = firstY ?? (y.log && v.y[0] > 0 ? Math.sqrt(v.y[0] * v.y[1]) : (v.y[0] + v.y[1]) / 2);
|
|
1117
|
+
ctl.zoomBy(act === "zoomIn" ? KEY_ZOOM : 1 / KEY_ZOOM, ax, ay);
|
|
1118
|
+
if (stand !== null) ctl.place(stand);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "panPrev":
|
|
1122
|
+
case "panNext":
|
|
1123
|
+
if (!zoomableRef.current || !zoomedRef.current) return;
|
|
1124
|
+
ctl.panBy(act === "panNext" ? KEY_PAN : -KEY_PAN);
|
|
1125
|
+
break;
|
|
1126
|
+
case "reset":
|
|
1127
|
+
if (e.key === "Escape" && !zoomedRef.current) return;
|
|
1128
|
+
ctl.reset();
|
|
1129
|
+
if (stand !== null) ctl.place(stand);
|
|
1130
|
+
e.stopPropagation();
|
|
1131
|
+
break;
|
|
1132
|
+
case "activate": {
|
|
1133
|
+
if (stand === null || !onPickRef.current) return;
|
|
1134
|
+
onPickRef.current(orderRef.current ? orderRef.current[stand] : stand);
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
e.preventDefault();
|
|
1139
|
+
};
|
|
1140
|
+
const onBlur = (e) => {
|
|
1141
|
+
if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
|
|
1142
|
+
if (kbRef.current === null) return;
|
|
1143
|
+
kbRef.current = null;
|
|
1144
|
+
ctlRef.current?.clear();
|
|
1145
|
+
};
|
|
1146
|
+
const resetZoom = () => {
|
|
1147
|
+
ctlRef.current?.reset();
|
|
1148
|
+
plotRef.current?.focus({ preventScroll: true });
|
|
1149
|
+
};
|
|
1150
|
+
const moved = [xBelow ? "x" : "", yAbove ? "y" : ""].filter(Boolean).join("");
|
|
1151
|
+
const readoutEl = /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: readFull, children: readFull });
|
|
683
1152
|
return /* @__PURE__ */ jsxs(
|
|
684
1153
|
"div",
|
|
685
1154
|
{
|
|
@@ -697,8 +1166,48 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
697
1166
|
"data-y-drag": dragCount ? String(dragCount) : void 0,
|
|
698
1167
|
"data-overlay": overlays.length ? String(overlays.length) : void 0,
|
|
699
1168
|
"data-solo": soloIdx !== null ? String(soloIdx) : void 0,
|
|
1169
|
+
"data-chart-host": drawn ? "line" : void 0,
|
|
1170
|
+
"data-points": drawn ? String(xs.length) : void 0,
|
|
1171
|
+
"data-zoomable": drawn ? zoomable ? "1" : "0" : void 0,
|
|
1172
|
+
"data-zoomed": drawn && zoomed ? "1" : void 0,
|
|
1173
|
+
"data-zoom-off": drawn && zoomIsOff ? zoomWhy : void 0,
|
|
1174
|
+
"data-axis-title-moved": drawn && moved ? moved : void 0,
|
|
700
1175
|
children: [
|
|
701
|
-
/* @__PURE__ */ jsx("
|
|
1176
|
+
drawn && yAbove && yTitle && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "y", children: yTitle }),
|
|
1177
|
+
/* @__PURE__ */ jsx(
|
|
1178
|
+
"div",
|
|
1179
|
+
{
|
|
1180
|
+
ref: setPlot,
|
|
1181
|
+
className: "caos-chart-plot",
|
|
1182
|
+
title: cutTexts.length ? cutTexts.join("; ") : void 0,
|
|
1183
|
+
...drawn ? {
|
|
1184
|
+
tabIndex: 0,
|
|
1185
|
+
role: "group",
|
|
1186
|
+
"aria-roledescription": CHART_ROLE[lang],
|
|
1187
|
+
"aria-label": name,
|
|
1188
|
+
"aria-describedby": summaryId,
|
|
1189
|
+
"aria-details": markRows.length ? `${tableId} ${marksId}` : tableId,
|
|
1190
|
+
"aria-description": zoomable ? ZOOM_HINT[lang] : void 0,
|
|
1191
|
+
"data-chart-focus": "",
|
|
1192
|
+
onKeyDown,
|
|
1193
|
+
onBlur
|
|
1194
|
+
} : {}
|
|
1195
|
+
}
|
|
1196
|
+
),
|
|
1197
|
+
drawn && xBelow && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "x", children: xTitle }),
|
|
1198
|
+
drawn && a11y && /* @__PURE__ */ jsx(ChartSummary, { id: summaryId, text: a11y.summary }),
|
|
1199
|
+
drawn && markRows.length > 0 && /* @__PURE__ */ jsx("div", { className: "sr-only", "data-chart-marks-box": "", children: /* @__PURE__ */ jsxs("table", { id: marksId, className: "caos-chart-table", "data-chart-marks": "", children: [
|
|
1200
|
+
/* @__PURE__ */ jsx("caption", { children: lang === "es" ? `Marcas de ${name}` : `Marks of ${name}` }),
|
|
1201
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1202
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Marca" : "Mark" }),
|
|
1203
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Posici\xF3n" : "Position" })
|
|
1204
|
+
] }) }),
|
|
1205
|
+
/* @__PURE__ */ jsx("tbody", { children: markRows.map((row, r) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
1206
|
+
/* @__PURE__ */ jsx("th", { scope: "row", children: row[0] }),
|
|
1207
|
+
/* @__PURE__ */ jsx("td", { children: row[1] })
|
|
1208
|
+
] }, r)) })
|
|
1209
|
+
] }) }),
|
|
1210
|
+
drawn && a11y && /* @__PURE__ */ jsx(ChartDataTable, { id: tableId, caption: a11y.thinned ? `${name} (${lang === "es" ? "una de cada varias filas" : "every few rows"})` : name, head: a11y.head, rows: a11y.rows }),
|
|
702
1211
|
(series.length > 1 || solo) && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsx("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: solo ? /* @__PURE__ */ jsxs("button", { type: "button", className: "caos-chart-key-button", "aria-pressed": soloIdx === i, onClick: () => setSoloPick(soloIdx === i ? null : soloKey(s)), children: [
|
|
703
1212
|
/* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
|
|
704
1213
|
pick(s.label, lang)
|
|
@@ -706,10 +1215,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
706
1215
|
/* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
|
|
707
1216
|
pick(s.label, lang)
|
|
708
1217
|
] }) }, i)) }),
|
|
709
|
-
/* @__PURE__ */
|
|
1218
|
+
drawn ? /* @__PURE__ */ jsxs("div", { className: "caos-chart-foot", children: [
|
|
1219
|
+
readoutEl,
|
|
1220
|
+
zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
|
|
1221
|
+
] }) : readoutEl
|
|
710
1222
|
]
|
|
711
1223
|
}
|
|
712
1224
|
);
|
|
713
1225
|
}
|
|
714
1226
|
|
|
715
|
-
export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels,
|
|
1227
|
+
export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, seriesStyle, smallestPositive, soloKey, soloStyle, sortOrder, tickLabelAt, tickSplits, tickSteps, xScaleRange };
|