@fasl-work/caos-app-shell 0.18.0 → 0.19.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/dist/chart.d.ts +43 -10
- package/dist/chart.js +595 -85
- package/dist/chunk-3LONJM5B.js +680 -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 +32 -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, 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-3LONJM5B.js';
|
|
2
|
+
export { referenceLineName, repeatedLabels } from './chunk-3LONJM5B.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,18 @@ 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,
|
|
436
675
|
legend: { show: false },
|
|
437
|
-
|
|
676
|
+
cursor: {
|
|
677
|
+
...scatter ? { dataIdx: (u2, sIdx, closest) => sIdx === 0 ? closest : nearest(u2) ?? closest } : {},
|
|
678
|
+
// 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
|
|
679
|
+
// 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
|
|
680
|
+
// `setSelect` hook below, through the same operation as the wheel and the keys
|
|
681
|
+
drag: zoomable ? { x: true, y: both, dist: DRAG_THRESHOLD, setScale: false } : { x: false, y: false, setScale: false }
|
|
682
|
+
},
|
|
438
683
|
// a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
|
|
439
684
|
scales: {
|
|
440
685
|
// a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
|
|
@@ -442,16 +687,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
442
687
|
x: {
|
|
443
688
|
time: Boolean(x.time),
|
|
444
689
|
distr: x.log ? 3 : 1,
|
|
445
|
-
|
|
690
|
+
// 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
|
|
691
|
+
// (or an array) undid every zoom of a chart with `x.range` or `y.range` (Tier A, 0.19.0)
|
|
692
|
+
...range ? { range: fixedRange(range) } : xExt ? { range: fixedRange(xExt) } : x.log ? { range: (_u, min, max) => [min, max] } : {}
|
|
446
693
|
},
|
|
447
694
|
y: {
|
|
448
695
|
distr: y.log ? 3 : 1,
|
|
449
|
-
...range ? { range } : y.range ? { range: y.range } : yLevels ? {
|
|
696
|
+
...range ? { range: fixedRange(range) } : y.range ? { range: fixedRange(y.range) } : yLevels ? {
|
|
450
697
|
// uPlot's own range of the data and the levels: its padding, its decade snap on a log axis
|
|
451
|
-
range: (
|
|
698
|
+
range: (u2, dmin, dmax) => {
|
|
452
699
|
const w = rangeWithLevels(dmin, dmax, yLevels, Boolean(y.log), 0.05);
|
|
453
700
|
if (!w) return [dmin, dmax];
|
|
454
|
-
return y.log ? uPlot.rangeLog(w[0], w[1],
|
|
701
|
+
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
702
|
}
|
|
456
703
|
} : {}
|
|
457
704
|
}
|
|
@@ -462,7 +709,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
462
709
|
grid: { stroke: grid, width: 1 },
|
|
463
710
|
ticks: { stroke: grid },
|
|
464
711
|
font,
|
|
465
|
-
label:
|
|
712
|
+
label: xBelow ? void 0 : xTitle,
|
|
466
713
|
labelFont: font,
|
|
467
714
|
// a log x axis labels the decades only; the ticks between stay drawn, unlabelled
|
|
468
715
|
values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
|
|
@@ -480,7 +727,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
480
727
|
grid: { stroke: grid, width: 1 },
|
|
481
728
|
ticks: { stroke: grid },
|
|
482
729
|
font,
|
|
483
|
-
label:
|
|
730
|
+
label: yAbove ? void 0 : yTitle,
|
|
484
731
|
labelFont: font,
|
|
485
732
|
values: tick(y.format, "y"),
|
|
486
733
|
...ySteps ? { incrs: ySteps } : {},
|
|
@@ -489,12 +736,12 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
489
736
|
// Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
|
|
490
737
|
// label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
|
|
491
738
|
// made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
|
|
492
|
-
size: (
|
|
739
|
+
size: (u2, values) => {
|
|
493
740
|
if (!values || values.length === 0) return 48;
|
|
494
741
|
const pr = uPlot.pxRatio || 1;
|
|
495
|
-
|
|
496
|
-
const widest = Math.max(...values.map((v) =>
|
|
497
|
-
return Math.ceil(widest / pr) + 30;
|
|
742
|
+
u2.ctx.font = `${Math.round(11 * pr)}px ${family}`;
|
|
743
|
+
const widest = Math.max(...values.map((v) => u2.ctx.measureText(v ?? "").width));
|
|
744
|
+
return Math.ceil(widest / pr) + (yAbove ? 18 : 30);
|
|
498
745
|
}
|
|
499
746
|
}
|
|
500
747
|
],
|
|
@@ -509,16 +756,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
509
756
|
],
|
|
510
757
|
hooks: {
|
|
511
758
|
setCursor: [
|
|
512
|
-
(
|
|
513
|
-
const idx = scatter ? nearest(
|
|
759
|
+
(u2) => {
|
|
760
|
+
const idx = scatter ? nearest(u2) : u2.cursor.idx ?? null;
|
|
514
761
|
pickedRef.current = idx;
|
|
515
762
|
setCursor(idx);
|
|
516
763
|
onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
|
|
517
764
|
}
|
|
518
765
|
],
|
|
519
766
|
ready: [
|
|
520
|
-
(
|
|
521
|
-
|
|
767
|
+
(u2) => {
|
|
768
|
+
u2.over.addEventListener("click", () => {
|
|
522
769
|
const idx = pickedRef.current;
|
|
523
770
|
if (idx === null || idx === void 0 || !onPickRef.current) return;
|
|
524
771
|
onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
|
|
@@ -543,7 +790,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
543
790
|
if (v === liveRef.current[i]) return;
|
|
544
791
|
liveRef.current[i] = v;
|
|
545
792
|
onYMarkChangeRef.current?.(i, v);
|
|
546
|
-
|
|
793
|
+
u2.redraw(false);
|
|
547
794
|
};
|
|
548
795
|
let dragging = false;
|
|
549
796
|
el2.addEventListener("pointerdown", (e) => {
|
|
@@ -555,7 +802,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
555
802
|
});
|
|
556
803
|
el2.addEventListener("pointermove", (e) => {
|
|
557
804
|
if (!dragging) return;
|
|
558
|
-
set(
|
|
805
|
+
set(u2.posToVal(e.clientY - u2.over.getBoundingClientRect().top, "y"));
|
|
559
806
|
});
|
|
560
807
|
const end = (e) => {
|
|
561
808
|
dragging = false;
|
|
@@ -569,68 +816,100 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
569
816
|
e.preventDefault();
|
|
570
817
|
set(v);
|
|
571
818
|
});
|
|
572
|
-
|
|
819
|
+
u2.over.appendChild(el2);
|
|
573
820
|
});
|
|
574
|
-
if (dragCount) queueMicrotask(() => uRef.current ===
|
|
821
|
+
if (dragCount) queueMicrotask(() => uRef.current === u2 && u2.redraw(false));
|
|
822
|
+
}
|
|
823
|
+
],
|
|
824
|
+
setSelect: [
|
|
825
|
+
(u2) => {
|
|
826
|
+
const sel = { left: u2.select.left, top: u2.select.top, width: u2.select.width, height: u2.select.height };
|
|
827
|
+
if (!(sel.width > 0 || sel.height > 0)) return;
|
|
828
|
+
const x0 = u2.posToVal(sel.left, "x");
|
|
829
|
+
const x1 = u2.posToVal(sel.left + sel.width, "x");
|
|
830
|
+
const y0 = u2.posToVal(sel.top + sel.height, "y");
|
|
831
|
+
const y1 = u2.posToVal(sel.top, "y");
|
|
832
|
+
u2.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
|
|
833
|
+
ctlRef.current?.brush(sel.width > 0 ? [x0, x1] : null, bothRef.current && sel.height > 0 ? [y0, y1] : void 0);
|
|
834
|
+
}
|
|
835
|
+
],
|
|
836
|
+
setScale: [
|
|
837
|
+
(u2, key) => {
|
|
838
|
+
if (key !== "x" && key !== "y") return;
|
|
839
|
+
const f2 = fullRef.current;
|
|
840
|
+
if (!f2) return;
|
|
841
|
+
const rx = [u2.scales.x.min ?? f2.x[0], u2.scales.x.max ?? f2.x[1]];
|
|
842
|
+
const ry = [u2.scales.y.min ?? f2.y[0], u2.scales.y.max ?? f2.y[1]];
|
|
843
|
+
hostRef.current?.setAttribute("data-view-x", `${rx[0]},${rx[1]}`);
|
|
844
|
+
const zx = isZoomed(rx, f2.x);
|
|
845
|
+
const zy = bothRef.current && isZoomed(ry, f2.y);
|
|
846
|
+
zoomRef.current = { x: zx ? rx : null, y: zy ? ry : null };
|
|
847
|
+
const z = zx || zy;
|
|
848
|
+
if (z !== zoomedRef.current) {
|
|
849
|
+
zoomedRef.current = z;
|
|
850
|
+
setZoomed(z);
|
|
851
|
+
}
|
|
852
|
+
u2.over.style.touchAction = zoomableRef.current ? z ? "none" : "pan-y" : "";
|
|
575
853
|
}
|
|
576
854
|
],
|
|
577
855
|
draw: [
|
|
578
|
-
(
|
|
856
|
+
(u2) => {
|
|
579
857
|
if (range) {
|
|
580
|
-
const ctx =
|
|
581
|
-
const
|
|
858
|
+
const ctx = u2.ctx;
|
|
859
|
+
const pr2 = uPlot.pxRatio || 1;
|
|
582
860
|
ctx.save();
|
|
583
861
|
ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
|
|
584
|
-
ctx.lineWidth = Math.max(1,
|
|
585
|
-
ctx.setLineDash([5 *
|
|
862
|
+
ctx.lineWidth = Math.max(1, pr2);
|
|
863
|
+
ctx.setLineDash([5 * pr2, 4 * pr2]);
|
|
586
864
|
ctx.beginPath();
|
|
587
|
-
ctx.moveTo(
|
|
588
|
-
ctx.lineTo(
|
|
865
|
+
ctx.moveTo(u2.valToPos(range[0], "x", true), u2.valToPos(range[0], "y", true));
|
|
866
|
+
ctx.lineTo(u2.valToPos(range[1], "x", true), u2.valToPos(range[1], "y", true));
|
|
589
867
|
ctx.stroke();
|
|
590
868
|
ctx.restore();
|
|
591
869
|
}
|
|
592
870
|
const dots = overlaysRef.current;
|
|
593
871
|
if (dots.length) {
|
|
594
|
-
const ctx =
|
|
595
|
-
const
|
|
596
|
-
let
|
|
872
|
+
const ctx = u2.ctx;
|
|
873
|
+
const pr2 = uPlot.pxRatio || 1;
|
|
874
|
+
let drawn2 = 0;
|
|
597
875
|
ctx.save();
|
|
598
876
|
ctx.beginPath();
|
|
599
|
-
ctx.rect(
|
|
877
|
+
ctx.rect(u2.bbox.left, u2.bbox.top, u2.bbox.width, u2.bbox.height);
|
|
600
878
|
ctx.clip();
|
|
601
879
|
for (const { s, i } of dots) {
|
|
602
880
|
ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
|
|
603
881
|
ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
|
|
604
882
|
ctx.strokeStyle = surface;
|
|
605
|
-
ctx.lineWidth = Math.max(1,
|
|
606
|
-
const r = (s.size ?? 9) / 2 *
|
|
883
|
+
ctx.lineWidth = Math.max(1, pr2);
|
|
884
|
+
const r = (s.size ?? 9) / 2 * pr2;
|
|
607
885
|
const ox = s.x ?? [];
|
|
608
886
|
for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
|
|
609
887
|
const v = s.values[k];
|
|
610
888
|
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 <
|
|
889
|
+
const px = u2.valToPos(ox[k], "x", true);
|
|
890
|
+
const py = u2.valToPos(v, "y", true);
|
|
891
|
+
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
892
|
ctx.beginPath();
|
|
615
893
|
ctx.arc(px, py, r, 0, Math.PI * 2);
|
|
616
894
|
ctx.fill();
|
|
617
895
|
ctx.stroke();
|
|
618
|
-
|
|
896
|
+
drawn2++;
|
|
619
897
|
}
|
|
620
898
|
}
|
|
621
899
|
ctx.restore();
|
|
622
|
-
hostRef.current?.setAttribute("data-overlay-drawn", String(
|
|
900
|
+
hostRef.current?.setAttribute("data-overlay-drawn", String(drawn2));
|
|
623
901
|
}
|
|
624
902
|
const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
|
|
625
903
|
const lines = yMarksRef.current ?? [];
|
|
626
|
-
|
|
627
|
-
|
|
904
|
+
const boxes = [];
|
|
905
|
+
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) : [];
|
|
906
|
+
u2.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
|
|
628
907
|
const i = Number(el2.getAttribute("data-y-handle"));
|
|
629
908
|
const m = lines[i];
|
|
630
909
|
const v = liveRef.current[i];
|
|
631
910
|
if (!m || v === void 0) return;
|
|
632
|
-
const top =
|
|
633
|
-
el2.style.display = Number.isFinite(top) && top >= 0 && top <=
|
|
911
|
+
const top = u2.valToPos(v, "y");
|
|
912
|
+
el2.style.display = Number.isFinite(top) && top >= 0 && top <= u2.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
|
|
634
913
|
el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
|
|
635
914
|
const label = m.label ? pick(m.label, lang) : "";
|
|
636
915
|
const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
|
|
@@ -639,7 +918,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
639
918
|
el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
|
|
640
919
|
el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
|
|
641
920
|
});
|
|
642
|
-
|
|
921
|
+
const cut = [...ycut, ...marks?.length ? drawMarks(u2, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle, boxes) : []];
|
|
922
|
+
const pr = uPlot.pxRatio || 1;
|
|
923
|
+
const rounded = (v) => Math.round(v / pr * 10) / 10;
|
|
924
|
+
hostRef.current?.setAttribute("data-mark-overlaps", String(countOverlaps(boxes)));
|
|
925
|
+
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 } : {} }))));
|
|
926
|
+
else hostRef.current?.removeAttribute("data-mark-boxes");
|
|
927
|
+
if (cut.length) hostRef.current?.setAttribute("data-mark-cut", cut.join("|"));
|
|
928
|
+
else hostRef.current?.removeAttribute("data-mark-cut");
|
|
929
|
+
const now = cut.join(CUT_SEP);
|
|
930
|
+
setCutKey((prev) => prev === now ? prev : now);
|
|
643
931
|
}
|
|
644
932
|
]
|
|
645
933
|
}
|
|
@@ -649,14 +937,60 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
649
937
|
hostRef.current?.removeAttribute("data-ticks-repeated");
|
|
650
938
|
}
|
|
651
939
|
uRef.current?.destroy();
|
|
652
|
-
|
|
940
|
+
fullRef.current = null;
|
|
941
|
+
dataRef.current = data;
|
|
942
|
+
const u = new uPlot(opts, data, el);
|
|
943
|
+
uRef.current = u;
|
|
944
|
+
u.batch(() => void 0);
|
|
945
|
+
const f = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
|
|
946
|
+
fullRef.current = f;
|
|
947
|
+
hostRef.current?.setAttribute("data-view-x", `${f.x[0]},${f.x[1]}`);
|
|
948
|
+
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 });
|
|
949
|
+
ctlRef.current = ctl;
|
|
950
|
+
u.over.style.touchAction = zoomable ? "pan-y" : "";
|
|
951
|
+
const held = zoomRef.current;
|
|
952
|
+
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;
|
|
953
|
+
const hx = zoomable ? meets(held.x, f.x) : null;
|
|
954
|
+
const hy = zoomable && both ? meets(held.y, f.y) : null;
|
|
955
|
+
if (hx || hy) {
|
|
956
|
+
explicitRef.current = true;
|
|
957
|
+
u.batch(() => {
|
|
958
|
+
if (hx) u.setScale("x", { min: hx[0], max: hx[1] });
|
|
959
|
+
if (hy) u.setScale("y", { min: hy[0], max: hy[1] });
|
|
960
|
+
});
|
|
961
|
+
explicitRef.current = false;
|
|
962
|
+
} else {
|
|
963
|
+
zoomRef.current = { x: null, y: null };
|
|
964
|
+
if (zoomedRef.current) {
|
|
965
|
+
zoomedRef.current = false;
|
|
966
|
+
setZoomed(false);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (!yAbove && yTitle) {
|
|
970
|
+
const pr = uPlot.pxRatio || 1;
|
|
971
|
+
const cy = (u.bbox.top + u.bbox.height / 2) / pr;
|
|
972
|
+
if (!axisTitleFits(yTitle, 2 * Math.min(cy, h - cy) - 4)) setForced(yTitle);
|
|
973
|
+
}
|
|
653
974
|
return () => {
|
|
975
|
+
ctl.detach();
|
|
976
|
+
ctlRef.current = null;
|
|
654
977
|
uRef.current?.destroy();
|
|
655
978
|
uRef.current = null;
|
|
656
979
|
};
|
|
657
980
|
}, [structure]);
|
|
658
981
|
useEffect(() => {
|
|
659
|
-
uRef.current
|
|
982
|
+
const u = uRef.current;
|
|
983
|
+
if (!u) return;
|
|
984
|
+
if (dataRef.current && sameData(dataRef.current, data)) return;
|
|
985
|
+
dataRef.current = data;
|
|
986
|
+
fullRef.current = null;
|
|
987
|
+
zoomRef.current = { x: null, y: null };
|
|
988
|
+
u.batch(() => u.setData(data));
|
|
989
|
+
fullRef.current = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
|
|
990
|
+
if (zoomedRef.current) {
|
|
991
|
+
zoomedRef.current = false;
|
|
992
|
+
setZoomed(false);
|
|
993
|
+
}
|
|
660
994
|
}, [data]);
|
|
661
995
|
const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
|
|
662
996
|
const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
|
|
@@ -675,11 +1009,144 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
675
1009
|
});
|
|
676
1010
|
u.redraw(false);
|
|
677
1011
|
}, [soloIdx, baseEntries]);
|
|
1012
|
+
const timeSpan = xs.length > 1 ? Math.abs(xs[xs.length - 1] - xs[0]) : 0;
|
|
1013
|
+
const xValue = (v) => x.time ? formatTime(v, lang, timeSpan) : formatNumber(v, lang, x.format ?? { digits: 4 });
|
|
678
1014
|
const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
|
|
679
1015
|
// 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} ${
|
|
1016
|
+
tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${xValue(xs[cursor])}${xUnit && !x.time ? `${NBSP}${xUnit}` : ""}`,
|
|
681
1017
|
...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
1018
|
].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";
|
|
1019
|
+
const cutTexts = cutKey ? cutKey.split(CUT_SEP) : [];
|
|
1020
|
+
const readFull = cutTexts.length ? [read, ...cutTexts].join(" \xB7 ") : read;
|
|
1021
|
+
const xText = (v) => tickLabelAt(ticks, v, lang, xSpan) ?? xValue(v);
|
|
1022
|
+
const yText = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : `${formatNumber(v, lang, y.format ?? { digits: 4 })}`;
|
|
1023
|
+
const xFormatKey = JSON.stringify(x.format ?? null);
|
|
1024
|
+
const yFormatKey = JSON.stringify(y.format ?? null);
|
|
1025
|
+
const a11y = useMemo(() => {
|
|
1026
|
+
if (!drawn) return null;
|
|
1027
|
+
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))];
|
|
1028
|
+
const kept = thinRows(xs.length);
|
|
1029
|
+
const blanks = (n) => Array.from({ length: n }, () => "");
|
|
1030
|
+
const rows = kept.map((i) => [xText(xs[i]), ...ys.map((col) => yText(col[i])), ...blanks(overlays.length)]);
|
|
1031
|
+
overlays.forEach(({ s: one }, o) => {
|
|
1032
|
+
const ox = one.x ?? [];
|
|
1033
|
+
for (const k of thinRows(Math.min(ox.length, one.values.length))) {
|
|
1034
|
+
if (!Number.isFinite(ox[k]) || one.values[k] === null || one.values[k] === void 0) continue;
|
|
1035
|
+
const cells = [...blanks(ys.length), ...blanks(overlays.length)];
|
|
1036
|
+
cells[ys.length + o] = yText(one.values[k]);
|
|
1037
|
+
rows.push([xText(ox[k]), ...cells]);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
const multi = baseEntries.length + overlays.length > 1;
|
|
1041
|
+
let low = null;
|
|
1042
|
+
let high = null;
|
|
1043
|
+
baseEntries.forEach(({ s: one }, k) => {
|
|
1044
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1045
|
+
const v = ys[k][i];
|
|
1046
|
+
if (v === null || v === void 0 || !Number.isFinite(v)) continue;
|
|
1047
|
+
const at = `${multi ? `${pick(one.label, lang)}, ` : ""}${xLabel} ${xText(xs[i])}`;
|
|
1048
|
+
if (!low || v < low.v) low = { v, at };
|
|
1049
|
+
if (!high || v > high.v) high = { v, at };
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
const unit = yUnit ? `${NBSP}${yUnit}` : "";
|
|
1053
|
+
const lo = low;
|
|
1054
|
+
const hi = high;
|
|
1055
|
+
const summary = chartSummary(lang, {
|
|
1056
|
+
kind: "line",
|
|
1057
|
+
name,
|
|
1058
|
+
count: xs.length,
|
|
1059
|
+
series: series.map((one) => pick(one.label, lang)),
|
|
1060
|
+
xLabel,
|
|
1061
|
+
xFrom: xs.length ? xText(xs[0]) : void 0,
|
|
1062
|
+
xTo: xs.length ? xText(xs[xs.length - 1]) : void 0,
|
|
1063
|
+
yLabel: yTitle,
|
|
1064
|
+
low: lo ? { text: `${yText(lo.v)}${unit}`, at: lo.at } : void 0,
|
|
1065
|
+
high: hi ? { text: `${yText(hi.v)}${unit}`, at: hi.at } : void 0
|
|
1066
|
+
});
|
|
1067
|
+
return { head, rows, summary, thinned: kept.length < xs.length };
|
|
1068
|
+
}, [drawn, xs, ys, baseEntries, overlays, series, lang, ticks, xSpan, xTitle, yTitle, xLabel, yUnit, name, xFormatKey, yFormatKey, x.time, timeSpan]);
|
|
1069
|
+
const marksId = `${ids}-marks`;
|
|
1070
|
+
const markRows = useMemo(() => {
|
|
1071
|
+
const unnamed = (yMarks ?? []).flatMap((m, k) => m.label ? [] : [k]);
|
|
1072
|
+
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}` : ""}`]);
|
|
1073
|
+
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}` : ""}`]);
|
|
1074
|
+
return [...xs2, ...ys2];
|
|
1075
|
+
}, [marks, yMarks, lang, xLabel, xUnit, yUnit, ticks, xSpan, xFormatKey, yFormatKey, x.time, timeSpan]);
|
|
1076
|
+
const onKeyDown = (e) => {
|
|
1077
|
+
if (e.target !== e.currentTarget) return;
|
|
1078
|
+
const u = uRef.current;
|
|
1079
|
+
const ctl = ctlRef.current;
|
|
1080
|
+
if (!u || !ctl) return;
|
|
1081
|
+
const act = keyAction(e.key, { shift: e.shiftKey, ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
|
|
1082
|
+
if (!act) return;
|
|
1083
|
+
const n = xs.length;
|
|
1084
|
+
const stand = kbRef.current;
|
|
1085
|
+
switch (act) {
|
|
1086
|
+
case "prev":
|
|
1087
|
+
case "next":
|
|
1088
|
+
case "first":
|
|
1089
|
+
case "last": {
|
|
1090
|
+
let idx;
|
|
1091
|
+
if (stand === null && zoomedRef.current && (act === "next" || act === "prev")) {
|
|
1092
|
+
const v = ctl.view().x;
|
|
1093
|
+
let last = null;
|
|
1094
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1095
|
+
if (xs[i] >= v[0] && xs[i] <= v[1]) {
|
|
1096
|
+
last = i;
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
idx = act === "next" ? firstVisible(xs, v[0], v[1]) : last;
|
|
1101
|
+
} else idx = stepIndex(stand, act, n);
|
|
1102
|
+
if (idx === null) return;
|
|
1103
|
+
kbRef.current = idx;
|
|
1104
|
+
ctl.reveal(xs[idx]);
|
|
1105
|
+
ctl.place(idx);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case "zoomIn":
|
|
1109
|
+
case "zoomOut": {
|
|
1110
|
+
if (!zoomableRef.current) return;
|
|
1111
|
+
const v = ctl.view();
|
|
1112
|
+
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;
|
|
1113
|
+
const firstY = stand !== null ? ys.map((col) => col[stand]).find((val) => val !== null && val !== void 0 && Number.isFinite(val)) : void 0;
|
|
1114
|
+
const ay = firstY ?? (y.log && v.y[0] > 0 ? Math.sqrt(v.y[0] * v.y[1]) : (v.y[0] + v.y[1]) / 2);
|
|
1115
|
+
ctl.zoomBy(act === "zoomIn" ? KEY_ZOOM : 1 / KEY_ZOOM, ax, ay);
|
|
1116
|
+
if (stand !== null) ctl.place(stand);
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
case "panPrev":
|
|
1120
|
+
case "panNext":
|
|
1121
|
+
if (!zoomableRef.current || !zoomedRef.current) return;
|
|
1122
|
+
ctl.panBy(act === "panNext" ? KEY_PAN : -KEY_PAN);
|
|
1123
|
+
break;
|
|
1124
|
+
case "reset":
|
|
1125
|
+
if (e.key === "Escape" && !zoomedRef.current) return;
|
|
1126
|
+
ctl.reset();
|
|
1127
|
+
if (stand !== null) ctl.place(stand);
|
|
1128
|
+
e.stopPropagation();
|
|
1129
|
+
break;
|
|
1130
|
+
case "activate": {
|
|
1131
|
+
if (stand === null || !onPickRef.current) return;
|
|
1132
|
+
onPickRef.current(orderRef.current ? orderRef.current[stand] : stand);
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
e.preventDefault();
|
|
1137
|
+
};
|
|
1138
|
+
const onBlur = (e) => {
|
|
1139
|
+
if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
|
|
1140
|
+
if (kbRef.current === null) return;
|
|
1141
|
+
kbRef.current = null;
|
|
1142
|
+
ctlRef.current?.clear();
|
|
1143
|
+
};
|
|
1144
|
+
const resetZoom = () => {
|
|
1145
|
+
ctlRef.current?.reset();
|
|
1146
|
+
plotRef.current?.focus({ preventScroll: true });
|
|
1147
|
+
};
|
|
1148
|
+
const moved = [xBelow ? "x" : "", yAbove ? "y" : ""].filter(Boolean).join("");
|
|
1149
|
+
const readoutEl = /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: readFull, children: readFull });
|
|
683
1150
|
return /* @__PURE__ */ jsxs(
|
|
684
1151
|
"div",
|
|
685
1152
|
{
|
|
@@ -697,8 +1164,48 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
697
1164
|
"data-y-drag": dragCount ? String(dragCount) : void 0,
|
|
698
1165
|
"data-overlay": overlays.length ? String(overlays.length) : void 0,
|
|
699
1166
|
"data-solo": soloIdx !== null ? String(soloIdx) : void 0,
|
|
1167
|
+
"data-chart-host": drawn ? "line" : void 0,
|
|
1168
|
+
"data-points": drawn ? String(xs.length) : void 0,
|
|
1169
|
+
"data-zoomable": drawn ? zoomable ? "1" : "0" : void 0,
|
|
1170
|
+
"data-zoomed": drawn && zoomed ? "1" : void 0,
|
|
1171
|
+
"data-zoom-off": drawn && zoomIsOff ? zoomWhy : void 0,
|
|
1172
|
+
"data-axis-title-moved": drawn && moved ? moved : void 0,
|
|
700
1173
|
children: [
|
|
701
|
-
/* @__PURE__ */ jsx("
|
|
1174
|
+
drawn && yAbove && yTitle && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "y", children: yTitle }),
|
|
1175
|
+
/* @__PURE__ */ jsx(
|
|
1176
|
+
"div",
|
|
1177
|
+
{
|
|
1178
|
+
ref: setPlot,
|
|
1179
|
+
className: "caos-chart-plot",
|
|
1180
|
+
title: cutTexts.length ? cutTexts.join("; ") : void 0,
|
|
1181
|
+
...drawn ? {
|
|
1182
|
+
tabIndex: 0,
|
|
1183
|
+
role: "group",
|
|
1184
|
+
"aria-roledescription": CHART_ROLE[lang],
|
|
1185
|
+
"aria-label": name,
|
|
1186
|
+
"aria-describedby": summaryId,
|
|
1187
|
+
"aria-details": markRows.length ? `${tableId} ${marksId}` : tableId,
|
|
1188
|
+
"aria-description": zoomable ? ZOOM_HINT[lang] : void 0,
|
|
1189
|
+
"data-chart-focus": "",
|
|
1190
|
+
onKeyDown,
|
|
1191
|
+
onBlur
|
|
1192
|
+
} : {}
|
|
1193
|
+
}
|
|
1194
|
+
),
|
|
1195
|
+
drawn && xBelow && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "x", children: xTitle }),
|
|
1196
|
+
drawn && a11y && /* @__PURE__ */ jsx(ChartSummary, { id: summaryId, text: a11y.summary }),
|
|
1197
|
+
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: [
|
|
1198
|
+
/* @__PURE__ */ jsx("caption", { children: lang === "es" ? `Marcas de ${name}` : `Marks of ${name}` }),
|
|
1199
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1200
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Marca" : "Mark" }),
|
|
1201
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Posici\xF3n" : "Position" })
|
|
1202
|
+
] }) }),
|
|
1203
|
+
/* @__PURE__ */ jsx("tbody", { children: markRows.map((row, r) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
1204
|
+
/* @__PURE__ */ jsx("th", { scope: "row", children: row[0] }),
|
|
1205
|
+
/* @__PURE__ */ jsx("td", { children: row[1] })
|
|
1206
|
+
] }, r)) })
|
|
1207
|
+
] }) }),
|
|
1208
|
+
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
1209
|
(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
1210
|
/* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
|
|
704
1211
|
pick(s.label, lang)
|
|
@@ -706,10 +1213,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
|
|
|
706
1213
|
/* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
|
|
707
1214
|
pick(s.label, lang)
|
|
708
1215
|
] }) }, i)) }),
|
|
709
|
-
/* @__PURE__ */
|
|
1216
|
+
drawn ? /* @__PURE__ */ jsxs("div", { className: "caos-chart-foot", children: [
|
|
1217
|
+
readoutEl,
|
|
1218
|
+
zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
|
|
1219
|
+
] }) : readoutEl
|
|
710
1220
|
]
|
|
711
1221
|
}
|
|
712
1222
|
);
|
|
713
1223
|
}
|
|
714
1224
|
|
|
715
|
-
export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels,
|
|
1225
|
+
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 };
|