@weasel-js/plot 0.0.0-stage → 1.15.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/index.js ADDED
@@ -0,0 +1,860 @@
1
+ import { ellipsePath, pathFromD, boundsOfPath } from '@weasel-js/geom';
2
+ import { THEMES } from '@weasel-js/theme';
3
+ import { measureTextBounds } from '@weasel-js/text';
4
+ import { composeWorldPose, scenePoseAdapter, composeAffordanceLayer, WeaselProvider, useScene, useLatest, SceneCanvas } from '@weasel-js/core';
5
+ import { useState, useEffect, useMemo } from 'react';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ // src/scales/linear.ts
9
+ var DEFAULT_TICKS = 5;
10
+ function tickStep(lo, hi, count = DEFAULT_TICKS) {
11
+ const span = Math.abs(hi - lo);
12
+ if (span === 0 || !Number.isFinite(span)) return 1;
13
+ const raw = span / Math.max(1, count);
14
+ const power = 10 ** Math.floor(Math.log10(raw));
15
+ const error = raw / power;
16
+ const factor = error >= Math.sqrt(50) ? 10 : error >= Math.sqrt(10) ? 5 : error >= Math.sqrt(2) ? 2 : 1;
17
+ return factor * power;
18
+ }
19
+ var decimalsOf = (step) => Math.max(0, -Math.floor(Math.log10(step) + 1e-9));
20
+ var onGrid = (i, step) => Number((i * step).toFixed(decimalsOf(step)));
21
+ function stepTicks(lo, hi, step) {
22
+ const [a, b] = lo <= hi ? [lo, hi] : [hi, lo];
23
+ const out = [];
24
+ for (let i = Math.ceil(a / step - 1e-9); i <= Math.floor(b / step + 1e-9); i++) out.push(onGrid(i, step));
25
+ return lo <= hi ? out : out.reverse();
26
+ }
27
+ function formatNumber(value, step) {
28
+ const fixed = value.toFixed(Math.min(20, decimalsOf(step)));
29
+ return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
30
+ }
31
+ function niceDomain(domain, count = DEFAULT_TICKS) {
32
+ const [lo, hi] = domain;
33
+ if (lo === hi) return lo === 0 ? [0, 1] : [Math.min(0, lo), Math.max(0, hi)];
34
+ const step = tickStep(lo, hi, count);
35
+ const down = (v) => onGrid(Math.floor(v / step + 1e-9), step);
36
+ const up = (v) => onGrid(Math.ceil(v / step - 1e-9), step);
37
+ return lo < hi ? [down(lo), up(hi)] : [up(lo), down(hi)];
38
+ }
39
+ function linearScale(opts) {
40
+ const domain = opts.nice !== void 0 ? niceDomain(opts.domain, opts.nice) : [opts.domain[0], opts.domain[1]];
41
+ const range = [opts.range[0], opts.range[1]];
42
+ const [d0, d1] = domain;
43
+ const [r0, r1] = range;
44
+ const span = d1 - d0;
45
+ const map = (v) => span === 0 ? (r0 + r1) / 2 : r0 + (v - d0) / span * (r1 - r0);
46
+ const step = (count = DEFAULT_TICKS) => tickStep(d0, d1, count);
47
+ return {
48
+ kind: "linear",
49
+ domain,
50
+ range,
51
+ map,
52
+ position: map,
53
+ invert: (at2) => r1 === r0 ? d0 : d0 + (at2 - r0) / (r1 - r0) * span,
54
+ tickStep: step,
55
+ ticks: (count = DEFAULT_TICKS) => stepTicks(d0, d1, step(count)),
56
+ format: (v) => formatNumber(v, step())
57
+ };
58
+ }
59
+
60
+ // src/scales/log.ts
61
+ function logScale(opts) {
62
+ const base = opts.base ?? 10;
63
+ const [d0, d1] = opts.domain;
64
+ if (!(d0 > 0 && d1 > 0)) throw new Error(`logScale: a domain of ${d0}..${d1} does not lie above zero`);
65
+ const [r0, r1] = opts.range;
66
+ const log = (v) => Math.log(v) / Math.log(base);
67
+ const l0 = log(d0);
68
+ const l1 = log(d1);
69
+ const map = (v) => l1 === l0 ? (r0 + r1) / 2 : r0 + (log(v) - l0) / (l1 - l0) * (r1 - r0);
70
+ const ticks = (count = 5) => {
71
+ const [lo, hi] = l0 <= l1 ? [l0, l1] : [l1, l0];
72
+ const first = Math.floor(lo + 1e-9);
73
+ const last = Math.ceil(hi - 1e-9);
74
+ const steps = last - first < count / 2 && base === 10 ? [1, 2, 5] : [1];
75
+ const out = [];
76
+ for (let p = first; p <= last; p++) {
77
+ for (const m of steps) {
78
+ const v = Number((m * base ** p).toPrecision(12));
79
+ if (log(v) >= lo - 1e-9 && log(v) <= hi + 1e-9) out.push(v);
80
+ }
81
+ }
82
+ return l0 <= l1 ? out : out.reverse();
83
+ };
84
+ return {
85
+ kind: "log",
86
+ base,
87
+ domain: [d0, d1],
88
+ range: [r0, r1],
89
+ map,
90
+ position: map,
91
+ invert: (at2) => base ** (r1 === r0 ? l0 : l0 + (at2 - r0) / (r1 - r0) * (l1 - l0)),
92
+ ticks,
93
+ format: (v) => v >= 1 ? formatNumber(v, 1) : formatNumber(v, 10 ** Math.floor(Math.log10(v)))
94
+ };
95
+ }
96
+
97
+ // src/scales/band.ts
98
+ function bandScale(opts) {
99
+ const domain = [...opts.domain];
100
+ const [r0, r1] = opts.range;
101
+ const inner = Math.min(1, Math.max(0, opts.paddingInner ?? 0.2));
102
+ const outer = Math.max(0, opts.paddingOuter ?? inner / 2);
103
+ const n2 = domain.length;
104
+ const step = n2 === 0 ? 0 : (r1 - r0) / Math.max(1e-9, n2 - inner + 2 * outer);
105
+ const bandwidth = Math.abs(step * (1 - inner));
106
+ const index = new Map(domain.map((key, i) => [key, i]));
107
+ const startAt = (i) => r0 + step * (outer + i) + (step < 0 ? step * (1 - inner) : 0);
108
+ const start = (key) => {
109
+ const i = index.get(key);
110
+ return i === void 0 ? Number.NaN : startAt(i);
111
+ };
112
+ return {
113
+ kind: "band",
114
+ domain,
115
+ range: [r0, r1],
116
+ bandwidth,
117
+ step: Math.abs(step),
118
+ start,
119
+ position: (key) => start(key) + bandwidth / 2,
120
+ invert: (at2) => {
121
+ if (n2 === 0) return void 0;
122
+ const i = Math.round((at2 - r0) / step - outer - (1 - inner) / 2);
123
+ return domain[Math.min(n2 - 1, Math.max(0, i))];
124
+ },
125
+ ticks: () => [...domain],
126
+ format: (key) => key
127
+ };
128
+ }
129
+
130
+ // src/scales/time.ts
131
+ var SECOND = 1e3;
132
+ var MINUTE = 60 * SECOND;
133
+ var HOUR = 60 * MINUTE;
134
+ var DAY = 24 * HOUR;
135
+ var INTERVALS = [
136
+ { ms: SECOND, unit: "second" },
137
+ { ms: 5 * SECOND, unit: "second" },
138
+ { ms: 15 * SECOND, unit: "second" },
139
+ { ms: 30 * SECOND, unit: "second" },
140
+ { ms: MINUTE, unit: "minute" },
141
+ { ms: 5 * MINUTE, unit: "minute" },
142
+ { ms: 15 * MINUTE, unit: "minute" },
143
+ { ms: 30 * MINUTE, unit: "minute" },
144
+ { ms: HOUR, unit: "hour" },
145
+ { ms: 3 * HOUR, unit: "hour" },
146
+ { ms: 6 * HOUR, unit: "hour" },
147
+ { ms: 12 * HOUR, unit: "hour" },
148
+ { ms: DAY, unit: "day" },
149
+ { ms: 2 * DAY, unit: "day" },
150
+ { ms: 7 * DAY, unit: "day" },
151
+ { months: 1, unit: "month" },
152
+ { months: 3, unit: "month" },
153
+ { months: 12, unit: "year" }
154
+ ];
155
+ var MONTH_MS = 30 * DAY;
156
+ var spanOf = (i) => i.ms ?? i.months * MONTH_MS;
157
+ function intervalFor(span, count) {
158
+ const target = span / Math.max(1, count);
159
+ let best = INTERVALS[0];
160
+ for (const i of INTERVALS) if (Math.abs(Math.log(spanOf(i) / target)) < Math.abs(Math.log(spanOf(best) / target))) best = i;
161
+ if (best.unit === "year" && target > 12 * MONTH_MS) {
162
+ const years = 10 ** Math.floor(Math.log10(target / (12 * MONTH_MS)));
163
+ return { months: 12 * years, unit: "year" };
164
+ }
165
+ return best;
166
+ }
167
+ function gridTicks(lo, hi, interval) {
168
+ const out = [];
169
+ if (interval.ms !== void 0) {
170
+ for (let t = Math.ceil(lo / interval.ms) * interval.ms; t <= hi; t += interval.ms) out.push(t);
171
+ return out;
172
+ }
173
+ const at2 = new Date(lo);
174
+ let month = at2.getUTCFullYear() * 12 + at2.getUTCMonth();
175
+ month = Math.ceil(month / interval.months) * interval.months;
176
+ for (; ; month += interval.months) {
177
+ const t = Date.UTC(Math.floor(month / 12), month % 12, 1);
178
+ if (t < lo) continue;
179
+ if (t > hi) break;
180
+ out.push(t);
181
+ }
182
+ return out;
183
+ }
184
+ var pad = (n2) => String(n2).padStart(2, "0");
185
+ function formatInstant(t, unit) {
186
+ const d = new Date(t);
187
+ const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
188
+ const time = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
189
+ switch (unit) {
190
+ case "year":
191
+ return String(d.getUTCFullYear());
192
+ case "month":
193
+ return date.slice(0, 7);
194
+ case "day":
195
+ return date;
196
+ case "hour":
197
+ case "minute":
198
+ return d.getUTCHours() === 0 && d.getUTCMinutes() === 0 ? date : time;
199
+ case "second":
200
+ return `${time}:${pad(d.getUTCSeconds())}`;
201
+ }
202
+ }
203
+ function timeScale(opts) {
204
+ const [d0, d1] = opts.domain;
205
+ const [r0, r1] = opts.range;
206
+ const span = d1 - d0;
207
+ const map = (v) => span === 0 ? (r0 + r1) / 2 : r0 + (v - d0) / span * (r1 - r0);
208
+ const interval = (count = 5) => intervalFor(Math.abs(span), count);
209
+ return {
210
+ kind: "time",
211
+ domain: [d0, d1],
212
+ range: [r0, r1],
213
+ map,
214
+ position: map,
215
+ invert: (at2) => r1 === r0 ? d0 : d0 + (at2 - r0) / (r1 - r0) * span,
216
+ ticks: (count = 5) => {
217
+ const out = gridTicks(Math.min(d0, d1), Math.max(d0, d1), interval(count));
218
+ return d0 <= d1 ? out : out.reverse();
219
+ },
220
+ format: (v) => formatInstant(v, interval().unit)
221
+ };
222
+ }
223
+
224
+ // src/scales/ordinal.ts
225
+ function ordinalScale(opts) {
226
+ if (opts.range.length === 0) throw new Error("ordinalScale: an empty range");
227
+ const domain = [...opts.domain];
228
+ const index = new Map(domain.map((key, i) => [key, i]));
229
+ const range = [...opts.range];
230
+ return {
231
+ kind: "ordinal",
232
+ domain,
233
+ range,
234
+ map: (key) => {
235
+ let i = index.get(key);
236
+ if (i === void 0) {
237
+ i = domain.length;
238
+ domain.push(key);
239
+ index.set(key, i);
240
+ }
241
+ return range[i % range.length];
242
+ },
243
+ ticks: () => [...domain],
244
+ format: (key) => key
245
+ };
246
+ }
247
+ var rectOf = (b) => ({ kind: "rect", x: b.x, y: b.y, width: b.width, height: b.height });
248
+ var fillOf = (color, opacity) => ({ paint: { color, ...opacity !== void 0 ? { opacity } : {} } });
249
+ function barMark(opts) {
250
+ const lo = Math.min(opts.from, opts.to);
251
+ const length = Math.abs(opts.to - opts.from);
252
+ const pose = opts.orient === "horizontal" ? { x: lo, y: opts.x, width: length, height: opts.width } : { x: opts.x, y: lo, width: opts.width, height: length };
253
+ return {
254
+ key: opts.key,
255
+ pose,
256
+ data: { plotMark: { kind: "bar", ...opts.datum ? { datum: opts.datum } : {} }, path: rectOf(pose), fill: fillOf(opts.color) }
257
+ };
258
+ }
259
+ function spanMark(opts) {
260
+ const pose = { x: Math.min(opts.x0, opts.x1), y: opts.y, width: Math.abs(opts.x1 - opts.x0), height: opts.height };
261
+ return {
262
+ key: opts.key,
263
+ pose,
264
+ data: {
265
+ plotMark: { kind: "span", ...opts.datum ? { datum: opts.datum } : {} },
266
+ path: rectOf(pose),
267
+ fill: fillOf(opts.color, opts.opacity)
268
+ }
269
+ };
270
+ }
271
+ function pointMark(opts) {
272
+ const r = opts.radius ?? 3;
273
+ const pose = { x: opts.x - r, y: opts.y - r, width: 2 * r, height: 2 * r };
274
+ return {
275
+ key: opts.key,
276
+ pose,
277
+ data: { plotMark: { kind: "point", ...opts.datum ? { datum: opts.datum } : {} }, path: ellipsePath(pose), fill: fillOf(opts.color) }
278
+ };
279
+ }
280
+ var n = (v) => Number(v.toFixed(3));
281
+ var at = (p) => `${n(p.x)} ${n(p.y)}`;
282
+ function pathMark(key, path, data) {
283
+ const b = boundsOfPath(path);
284
+ return { key, pose: { x: b.x, y: b.y, width: b.width, height: b.height }, data: { ...data, path } };
285
+ }
286
+ var traitOf = (kind, datum) => ({ kind, ...datum ? { datum } : {} });
287
+ function lineMark(opts) {
288
+ const [first, ...rest] = opts.points;
289
+ if (first === void 0) throw new Error(`lineMark "${opts.key}": no points`);
290
+ const d = `M ${at(first)}${rest.map((p) => ` L ${at(p)}`).join("")}`;
291
+ const stroke = {
292
+ paint: { color: opts.color },
293
+ width: opts.width ?? 2,
294
+ join: "round",
295
+ cap: "round",
296
+ ...opts.dash ? { dash: opts.dash } : {}
297
+ };
298
+ return pathMark(opts.key, pathFromD(d), { plotMark: traitOf("line", opts.datum), stroke });
299
+ }
300
+ function ruleMark(opts) {
301
+ const stroke = { paint: { color: opts.color }, width: opts.width ?? 1, cap: "butt", ...opts.dash ? { dash: opts.dash } : {} };
302
+ return pathMark(opts.key, pathFromD(`M ${at(opts.from)} L ${at(opts.to)}`), { plotMark: traitOf("rule", opts.datum), stroke });
303
+ }
304
+ function areaMark(opts) {
305
+ if (opts.top.length === 0) throw new Error(`areaMark "${opts.key}": no points`);
306
+ const bottom = opts.bottom ?? (opts.baseline !== void 0 ? opts.top.map((p) => ({ x: p.x, y: opts.baseline })) : []);
307
+ const ring = [...opts.top, ...[...bottom].reverse()];
308
+ const d = `M ${ring.map(at).join(" L ")} Z`;
309
+ return pathMark(opts.key, pathFromD(d), {
310
+ plotMark: traitOf("area", opts.datum),
311
+ fill: { paint: { color: opts.color, opacity: opts.opacity ?? 0.35 } }
312
+ });
313
+ }
314
+ var polar = (c, r, a) => ({ x: c.x + r * Math.sin(a), y: c.y - r * Math.cos(a) });
315
+ function arcTo(c, r, a0, a1) {
316
+ const sweep = a1 >= a0 ? 1 : 0;
317
+ const mid = (a0 + a1) / 2;
318
+ const to = (a) => ` A ${n(r)} ${n(r)} 0 0 ${sweep} ${at(polar(c, r, a))}`;
319
+ return Math.abs(a1 - a0) > Math.PI ? to(mid) + to(a1) : to(a1);
320
+ }
321
+ function arcMark(opts) {
322
+ const { center: c, innerRadius: r0, outerRadius: r1, startAngle: a0 } = opts;
323
+ const a1 = Math.abs(opts.endAngle - a0) >= 2 * Math.PI ? a0 + Math.sign(opts.endAngle - a0) * (2 * Math.PI - 1e-6) : opts.endAngle;
324
+ let d = `M ${at(polar(c, r1, a0))}${arcTo(c, r1, a0, a1)}`;
325
+ d += r0 > 0 ? ` L ${at(polar(c, r0, a1))}${arcTo(c, r0, a1, a0)} Z` : ` L ${at(c)} Z`;
326
+ return pathMark(opts.key, pathFromD(d), { plotMark: traitOf("arc", opts.datum), fill: { paint: { color: opts.color } } });
327
+ }
328
+ function ribbonMark(opts) {
329
+ const { from: f, to: t } = opts;
330
+ const mx = (f.x + t.x) / 2;
331
+ const d = `M ${n(f.x)} ${n(f.y0)} C ${n(mx)} ${n(f.y0)} ${n(mx)} ${n(t.y0)} ${n(t.x)} ${n(t.y0)} L ${n(t.x)} ${n(t.y1)} C ${n(mx)} ${n(t.y1)} ${n(mx)} ${n(f.y1)} ${n(f.x)} ${n(f.y1)} Z`;
332
+ return pathMark(opts.key, pathFromD(d), {
333
+ plotMark: traitOf("ribbon", opts.datum),
334
+ fill: { paint: { color: opts.color, opacity: opts.opacity ?? 0.45 } }
335
+ });
336
+ }
337
+
338
+ // src/marks/text.ts
339
+ var ALIGN = { start: "left", middle: "center", end: "right" };
340
+ function textMark(opts) {
341
+ const { width, height } = opts.measure(opts.text, opts.font);
342
+ const anchor = opts.anchor ?? "middle";
343
+ const baseline = opts.baseline ?? "middle";
344
+ const along = anchor === "start" ? 0 : anchor === "middle" ? -width / 2 : -width;
345
+ const across = baseline === "top" ? 0 : baseline === "middle" ? -height / 2 : -height;
346
+ const center = opts.vertical ? { x: opts.x + across + height / 2, y: opts.y - along - width / 2 } : { x: opts.x + along + width / 2, y: opts.y + across + height / 2 };
347
+ return {
348
+ key: opts.key,
349
+ pose: {
350
+ x: center.x - width / 2,
351
+ y: center.y - height / 2,
352
+ width,
353
+ height,
354
+ ...opts.vertical ? { rotation: -Math.PI / 2 } : {}
355
+ },
356
+ data: {
357
+ plotMark: { kind: "text", ...opts.datum ? { datum: opts.datum } : {} },
358
+ text: opts.text,
359
+ style: { fontFamily: opts.font.fontFamily, fontSize: opts.font.fontSize, align: ALIGN[anchor], ...opts.bold ? { fontWeight: 700 } : {} },
360
+ fill: { paint: { color: opts.color } },
361
+ verticalAlign: "center"
362
+ }
363
+ };
364
+ }
365
+
366
+ // src/axes.ts
367
+ var TICK = 5;
368
+ var GAP = 4;
369
+ var isHorizontal = (side) => side === "top" || side === "bottom";
370
+ var outward = (side) => side === "bottom" || side === "right" ? 1 : -1;
371
+ function axisRoom(opts) {
372
+ const { scale, side, theme, measure } = opts;
373
+ const sizes = scale.ticks(opts.ticks).map((t) => measure(scale.format(t), theme.font));
374
+ const labels = sizes.length === 0 ? 0 : Math.max(...sizes.map((s) => isHorizontal(side) ? s.height : s.width));
375
+ const title = opts.title ? measure(opts.title, theme.font).height + GAP : 0;
376
+ return TICK + GAP + labels + title;
377
+ }
378
+ function axisMarks(opts) {
379
+ const { scale, side, at: at2, theme, measure } = opts;
380
+ const key = opts.key ?? `axis:${side}`;
381
+ const h = isHorizontal(side);
382
+ const out = outward(side);
383
+ const pt = (along, across) => h ? { x: along, y: across } : { x: across, y: along };
384
+ const [r0, r1] = scale.range;
385
+ const marks = [];
386
+ const ticks = scale.ticks(opts.ticks);
387
+ ticks.forEach((t, i) => {
388
+ const p = scale.position(t);
389
+ if (opts.gridTo !== void 0) {
390
+ marks.push(ruleMark({ key: `${key}:grid:${i}`, from: pt(p, at2), to: pt(p, opts.gridTo), color: theme.grid }));
391
+ }
392
+ });
393
+ marks.push(ruleMark({ key: `${key}:rule`, from: pt(r0, at2), to: pt(r1, at2), color: theme.axis }));
394
+ let reach = 0;
395
+ ticks.forEach((t, i) => {
396
+ const p = scale.position(t);
397
+ marks.push(ruleMark({ key: `${key}:tick:${i}`, from: pt(p, at2), to: pt(p, at2 + out * TICK), color: theme.axis }));
398
+ const anchorAt = pt(p, at2 + out * (TICK + GAP));
399
+ const label = textMark({
400
+ key: `${key}:label:${i}`,
401
+ text: scale.format(t),
402
+ ...anchorAt,
403
+ font: theme.font,
404
+ color: theme.text,
405
+ measure,
406
+ anchor: h ? "middle" : out > 0 ? "start" : "end",
407
+ baseline: h ? out > 0 ? "top" : "bottom" : "middle"
408
+ });
409
+ reach = Math.max(reach, h ? label.pose.height : label.pose.width);
410
+ marks.push(label);
411
+ });
412
+ if (opts.title) {
413
+ const across = at2 + out * (TICK + GAP + reach + GAP);
414
+ marks.push(textMark({
415
+ key: `${key}:title`,
416
+ text: opts.title,
417
+ ...pt((r0 + r1) / 2, across),
418
+ font: theme.font,
419
+ color: theme.text,
420
+ measure,
421
+ anchor: "middle",
422
+ baseline: out > 0 ? "top" : "bottom",
423
+ vertical: !h
424
+ }));
425
+ }
426
+ return marks;
427
+ }
428
+ var SWATCHES = ["sky", "amber", "green", "rose", "violet", "teal", "citron", "fuchsia", "blue", "red"];
429
+ function plotThemeFrom(resolved, font = PLOT_FONT) {
430
+ const at2 = (token) => resolved[`--wzl-${token}`] ?? "#888888";
431
+ return {
432
+ text: at2("fg"),
433
+ axis: at2("border-strong"),
434
+ grid: at2("border"),
435
+ series: SWATCHES.map((s) => at2(`swatch-${s}`)),
436
+ font
437
+ };
438
+ }
439
+ var PLOT_FONT = { fontFamily: "sans-serif", fontSize: 12 };
440
+ var PLOT_THEME = plotThemeFrom(THEMES.weasel.selections["mode=dark,density=comfortable"]);
441
+ var seriesColor = (theme, i) => theme.series[(i % theme.series.length + theme.series.length) % theme.series.length] ?? theme.text;
442
+ var textMeasure = (text, font) => {
443
+ const { width } = measureTextBounds(text, { fontFamily: font.fontFamily, fontSize: font.fontSize });
444
+ return { width: Math.ceil(width), height: Math.ceil(font.fontSize * 1.2) };
445
+ };
446
+
447
+ // src/xychart/compile.ts
448
+ var PAD = 12;
449
+ var GAP2 = 8;
450
+ var BAND_PADDING = 0.2;
451
+ var xyLength = (data) => Math.max(0, ...data.series.map((s) => s.values.length));
452
+ function categoriesOf(data) {
453
+ if (data.x?.kind === "band" && data.x.categories.length > 0) return [...data.x.categories];
454
+ return Array.from({ length: xyLength(data) }, (_, i) => String(i + 1));
455
+ }
456
+ function xyValueDomain(data) {
457
+ const values = data.series.flatMap((s) => s.values).filter(Number.isFinite);
458
+ const lo = Math.min(0, ...values);
459
+ const hi = Math.max(0, ...values);
460
+ const [niceLo, niceHi] = niceDomain([lo, hi === lo ? lo + 1 : hi]);
461
+ const min = data.y?.min ?? niceLo;
462
+ const max = data.y?.max ?? niceHi;
463
+ return min === max ? [min, min + 1] : [min, max];
464
+ }
465
+ function categoryAxis(data, range) {
466
+ const n2 = xyLength(data);
467
+ if (data.x?.kind === "range") {
468
+ const scale2 = linearScale({ domain: [data.x.min, data.x.max], range });
469
+ const step = (range[1] - range[0]) / Math.max(1, n2);
470
+ const slots2 = Array.from({ length: n2 }, (_, i) => range[0] + step * (i + 0.5));
471
+ return { scale: scale2, slots: slots2, slotWidth: Math.abs(step) * (1 - BAND_PADDING) };
472
+ }
473
+ const categories = categoriesOf(data);
474
+ const scale = bandScale({ domain: categories, range, paddingInner: BAND_PADDING });
475
+ const slots = Array.from({ length: n2 }, (_, i) => i < categories.length ? scale.position(categories[i]) : Number.NaN);
476
+ return { scale, slots, slotWidth: scale.bandwidth };
477
+ }
478
+ function compileXyChart(data, size, theme, measure) {
479
+ const vertical = data.orientation !== "horizontal";
480
+ const marks = [];
481
+ let top = PAD;
482
+ if (data.title) {
483
+ const title = textMark({
484
+ key: "title",
485
+ text: data.title,
486
+ x: size.width / 2,
487
+ y: PAD,
488
+ baseline: "top",
489
+ font: theme.font,
490
+ color: theme.text,
491
+ measure,
492
+ bold: true
493
+ });
494
+ marks.push(title);
495
+ top += title.pose.height + GAP2;
496
+ }
497
+ const domain = xyValueDomain(data);
498
+ const base = { theme, measure, at: 0 };
499
+ const valueTitle = data.y?.title !== void 0 ? { title: data.y.title } : {};
500
+ const categoryTitle = data.x?.title !== void 0 ? { title: data.x.title } : {};
501
+ const probe = categoryAxis(data, [0, 1]);
502
+ const valueRoom = axisRoom({ ...base, ...valueTitle, scale: linearScale({ domain, range: [0, 1] }), side: vertical ? "left" : "bottom" });
503
+ const categoryRoom = axisRoom({ ...base, ...categoryTitle, scale: probe.scale, side: vertical ? "bottom" : "left" });
504
+ const left = PAD + (vertical ? valueRoom : categoryRoom);
505
+ const bottom = size.height - PAD - (vertical ? categoryRoom : valueRoom);
506
+ const plot = { x: left, y: top, width: Math.max(1, size.width - PAD - left), height: Math.max(1, bottom - top) };
507
+ const right = plot.x + plot.width;
508
+ const value = linearScale({ domain, range: vertical ? [bottom, top] : [left, right] });
509
+ const category = categoryAxis(data, vertical ? [left, right] : [top, bottom]);
510
+ marks.push(...axisMarks({ ...base, ...valueTitle, key: "axis:value", scale: value, side: vertical ? "left" : "bottom", at: vertical ? left : bottom, gridTo: vertical ? right : top }));
511
+ marks.push(...axisMarks({ ...base, ...categoryTitle, key: "axis:category", scale: category.scale, side: vertical ? "bottom" : "left", at: vertical ? bottom : left }));
512
+ const layout = { orientation: vertical ? "vertical" : "horizontal", plot, value, slots: category.slots, slotWidth: category.slotWidth };
513
+ marks.push(...seriesMarks(data, layout, theme));
514
+ return { marks, layout };
515
+ }
516
+ function seriesMarks(data, layout, theme) {
517
+ const { value, slots, slotWidth } = layout;
518
+ const vertical = layout.orientation === "vertical";
519
+ const at2 = (along, v) => vertical ? { x: along, y: value.map(v) } : { x: value.map(v), y: along };
520
+ const [d0, d1] = value.domain;
521
+ const baseline = value.map(Math.min(Math.max(0, Math.min(d0, d1)), Math.max(d0, d1)));
522
+ const bars = data.series.filter((s) => s.kind === "bar");
523
+ const barWidth = slotWidth / Math.max(1, bars.length);
524
+ const categories = categoriesOf(data);
525
+ const marks = [];
526
+ data.series.forEach((series, s) => {
527
+ const color = seriesColor(theme, s);
528
+ const datum = (i) => ({ series: s, index: i, value: series.values[i], category: categories[i] ?? String(i + 1) });
529
+ const placed = series.values.map((v, i) => ({ v, i })).filter(({ v, i }) => Number.isFinite(v) && Number.isFinite(slots[i] ?? Number.NaN));
530
+ if (series.kind === "bar") {
531
+ const j = bars.indexOf(series);
532
+ for (const { v, i } of placed) {
533
+ marks.push(barMark({
534
+ key: `bar:${s}:${i}`,
535
+ x: slots[i] - slotWidth / 2 + j * barWidth,
536
+ width: barWidth,
537
+ from: baseline,
538
+ to: value.map(v),
539
+ color,
540
+ datum: datum(i),
541
+ orient: layout.orientation
542
+ }));
543
+ }
544
+ return;
545
+ }
546
+ if (placed.length > 0) {
547
+ marks.push(lineMark({ key: `line:${s}`, points: placed.map(({ v, i }) => at2(slots[i], v)), color, datum: { series: s } }));
548
+ }
549
+ for (const { v, i } of placed) marks.push(pointMark({ key: `point:${s}:${i}`, ...at2(slots[i], v), radius: 3.5, color, datum: datum(i) }));
550
+ });
551
+ return marks;
552
+ }
553
+
554
+ // src/plotData.ts
555
+ var COMPILERS = {
556
+ xychart: compileXyChart
557
+ };
558
+ function compilePlot(data, size, theme, measure) {
559
+ const compile = COMPILERS[data.kind];
560
+ if (compile === void 0) throw new Error(`compilePlot: no chart of kind "${data.kind}"`);
561
+ return compile(data, size, theme, measure).marks;
562
+ }
563
+
564
+ // src/xychart/edit.ts
565
+ function setXyValue(data, series, index, value) {
566
+ const target = data.series[series];
567
+ if (target === void 0 || index < 0 || index >= target.values.length) {
568
+ throw new Error(`setXyValue: series ${series} holds no value ${index}`);
569
+ }
570
+ if (target.values[index] === value) return data;
571
+ const values = target.values.map((v, i) => i === index ? value : v);
572
+ return { ...data, series: data.series.map((s, i) => i === series ? { ...s, values } : s) };
573
+ }
574
+ function xyValueAt(layout, at2) {
575
+ const raw = layout.value.invert(layout.orientation === "vertical" ? at2.y : at2.x);
576
+ const step = layout.value.tickStep() / 10;
577
+ return Number((Math.round(raw / step) * step).toFixed(decimalsOf(step)));
578
+ }
579
+
580
+ // src/plotScene.ts
581
+ function plotRootOf(node) {
582
+ const plot = node?.data?.plot;
583
+ return plot !== void 0 && plot !== null && typeof plot === "object" && typeof plot.data?.kind === "string" ? plot : null;
584
+ }
585
+ function plotMarkOf(node) {
586
+ const mark = node?.data?.plotMark;
587
+ return mark !== void 0 && mark !== null && typeof mark.kind === "string" ? mark : null;
588
+ }
589
+ var plotMarkId = (rootId, key) => `${rootId}/${key}`;
590
+ function markSpecs(rootId, layer, marks) {
591
+ return marks.map((m) => ({
592
+ kind: "mark",
593
+ id: plotMarkId(rootId, m.key),
594
+ parent: rootId,
595
+ layer,
596
+ pose: { x: m.pose.x, y: m.pose.y, width: m.pose.width, height: m.pose.height, ...m.pose.rotation ? { rotation: m.pose.rotation } : {} },
597
+ data: m.data
598
+ }));
599
+ }
600
+ function plotSpecs(data, opts) {
601
+ const id = opts.id ?? "plot";
602
+ const layer = opts.layer ?? "main";
603
+ const theme = opts.theme ?? PLOT_THEME;
604
+ const { x, y, width, height } = opts.box;
605
+ const marks = compilePlot(data, { width, height }, theme, opts.measure ?? textMeasure);
606
+ const root = { kind: "group", id, layer, pose: { x, y, width, height }, data: { plot: { data, theme } } };
607
+ return [root, ...markSpecs(id, layer, marks)];
608
+ }
609
+ function rootTrait(scene, rootId) {
610
+ const node = scene.get(rootId);
611
+ const trait = plotRootOf(node);
612
+ if (node === void 0 || trait === null) throw new Error(`replacePlotData: "${rootId}" is not a plot root`);
613
+ return { node, trait };
614
+ }
615
+ function replacePlotData(scene, rootId, data, opts = {}) {
616
+ const s = scene;
617
+ const { node, trait } = rootTrait(s, rootId);
618
+ const theme = opts.theme ?? trait.theme;
619
+ const pose = node.pose;
620
+ const marks = compilePlot(data, { width: pose.width, height: pose.height }, theme, opts.measure ?? textMeasure);
621
+ s.batch(opts.label ?? "Edit chart", () => {
622
+ s.update(node.id, { data: { ...node.data, plot: { data, theme } } });
623
+ const children = s.childrenOf(node.id);
624
+ if (children.length > 0) s.removeMany(children);
625
+ for (const spec of markSpecs(rootId, node.layer, marks)) s.add(spec);
626
+ });
627
+ }
628
+ function plotRootsIn(scene) {
629
+ return scene.renderOrderNodes().filter((n2) => plotRootOf(n2) !== null).map((n2) => n2.id);
630
+ }
631
+
632
+ // src/xychart/editor.ts
633
+ var seriesIndex = (datum) => {
634
+ const d = datum;
635
+ return typeof d?.series === "number" && typeof d.index === "number" ? { series: d.series, index: d.index } : null;
636
+ };
637
+ var xyEditor = {
638
+ handle(data, { trait, pose }) {
639
+ if (trait.kind !== "bar" && trait.kind !== "point" || seriesIndex(trait.datum) === null) return null;
640
+ const vertical = data.orientation !== "horizontal";
641
+ const cursor = vertical ? "ns-resize" : "ew-resize";
642
+ const cx = pose.x + pose.width / 2;
643
+ const cy = pose.y + pose.height / 2;
644
+ if (trait.kind === "point") return { at: { x: cx, y: cy }, cursor };
645
+ const up = Number(trait.datum?.value ?? 0) >= 0;
646
+ return vertical ? { at: { x: cx, y: up ? pose.y : pose.y + pose.height }, cursor } : { at: { x: up ? pose.x + pose.width : pose.x, y: cy }, cursor };
647
+ },
648
+ drag(data, trait, size, theme, measure) {
649
+ const which = seriesIndex(trait.datum);
650
+ if (which === null) return null;
651
+ const { layout } = compileXyChart(data, size, theme, measure);
652
+ return (at2) => setXyValue(data, which.series, which.index, xyValueAt(layout, at2));
653
+ }
654
+ };
655
+
656
+ // src/editing/editors.ts
657
+ var EDITORS = {
658
+ xychart: xyEditor
659
+ };
660
+ var plotEditorOf = (data) => EDITORS[data.kind] ?? null;
661
+ var PLOT_HANDLE_LAYER_ID = "plot-handles";
662
+ var PLOT_HANDLE_AFFORDANCE_KIND = `layer:${PLOT_HANDLE_LAYER_ID}`;
663
+ function plotHandleScratchOf(hit) {
664
+ const p = hit?.payload;
665
+ return p !== void 0 && typeof p.rootId === "string" && typeof p.markId === "string" ? p : null;
666
+ }
667
+ function plotOrigin(scene, rootId) {
668
+ const pose = composeWorldPose(scenePoseAdapter(scene), rootId, scene.composition.compose);
669
+ return { x: pose.x, y: pose.y };
670
+ }
671
+ function* plotHandles(scene) {
672
+ for (const rootId of plotRootsIn(scene)) {
673
+ const root = scene.get(rootId);
674
+ const trait = plotRootOf(root);
675
+ const editor = trait && plotEditorOf(trait.data);
676
+ if (trait === null || editor === null || scene.isLocked(rootId) || scene.isHidden(rootId)) continue;
677
+ const origin = plotOrigin(scene, rootId);
678
+ for (const id of scene.childrenOf(rootId)) {
679
+ const node = scene.get(id);
680
+ const mark = plotMarkOf(node);
681
+ if (node === void 0 || mark === null) continue;
682
+ const handle = editor.handle(trait.data, { trait: mark, pose: node.pose });
683
+ if (handle === null) continue;
684
+ yield { rootId, markId: id, cursor: handle.cursor, at: { x: origin.x + handle.at.x, y: origin.y + handle.at.y } };
685
+ }
686
+ }
687
+ }
688
+ var HIT_RADIUS_PX = 6;
689
+ function handleAffordance(scene) {
690
+ const regions = () => [...plotHandles(scene)].map(({ rootId, markId, at: at2, cursor }) => ({
691
+ id: markId,
692
+ targetId: null,
693
+ shape: { kind: "point", x: at2.x, y: at2.y, hitRadiusPx: HIT_RADIUS_PX },
694
+ cursor,
695
+ strength: "exclusive",
696
+ claimedKinds: ["pointer"],
697
+ bind: () => ({ initialScratch: { rootId, markId, at: at2 } })
698
+ }));
699
+ return { id: PLOT_HANDLE_LAYER_ID, regions, regionsAt: regions };
700
+ }
701
+ function plotHandleLayer(scene) {
702
+ return composeAffordanceLayer(PLOT_HANDLE_LAYER_ID, "Plot handles", [handleAffordance(scene)]);
703
+ }
704
+
705
+ // src/editing/dragAction.ts
706
+ var PLOT_DRAG_ACTION_ID = "plot.dragValue";
707
+ function pressOf(ctx, measure) {
708
+ const pressed = plotHandleScratchOf(ctx.drag?.affordance);
709
+ const scene = ctx.deps.scene;
710
+ const root = pressed === null ? void 0 : scene?.get(pressed.rootId);
711
+ const trait = plotRootOf(root);
712
+ const mark = plotMarkOf(pressed === null ? void 0 : scene?.get(pressed.markId));
713
+ const editor = trait && plotEditorOf(trait.data);
714
+ if (pressed === null || scene === void 0 || root === void 0 || trait === null || mark === null || editor === null) return null;
715
+ const size = { width: root.pose.width, height: root.pose.height };
716
+ const propose = editor.drag(trait.data, mark, size, trait.theme, measure);
717
+ if (propose === null) return null;
718
+ const origin = plotOrigin(scene, pressed.rootId);
719
+ const start = ctx.drag?.start ?? ctx.world;
720
+ const grab = { x: pressed.at.x - start.x, y: pressed.at.y - start.y };
721
+ return {
722
+ rootId: pressed.rootId,
723
+ propose,
724
+ toPlot: (w) => ({ x: w.x + grab.x - origin.x, y: w.y + grab.y - origin.y }),
725
+ redraw: (data) => markSpecs(pressed.rootId, root.layer, compilePlot(data, size, trait.theme, measure)),
726
+ deps: ctx.deps
727
+ };
728
+ }
729
+ function commitPlotToScene(edit, ctx, measure = textMeasure) {
730
+ const scene = ctx.deps.scene;
731
+ if (scene === void 0 || plotRootOf(scene.get(edit.rootId)) === null) return;
732
+ replacePlotData(scene, edit.rootId, edit.data, { label: "Edit value", measure });
733
+ }
734
+ function createPlotDragAction(opts = {}) {
735
+ const measure = opts.measure ?? textMeasure;
736
+ const commit = opts.commit ?? ((edit, ctx) => commitPlotToScene(edit, ctx, measure));
737
+ return {
738
+ id: opts.id ?? PLOT_DRAG_ACTION_ID,
739
+ label: "Edit value",
740
+ group: "plot",
741
+ requires: ["scene"],
742
+ eligible: { mode: "normal" },
743
+ invoker: {
744
+ timing: "ongoing",
745
+ start(ctx) {
746
+ const p = pressOf(ctx, measure);
747
+ if (p === null) return {};
748
+ let next = null;
749
+ let shown = /* @__PURE__ */ new Map();
750
+ let open = true;
751
+ const scene = ctx.deps.scene;
752
+ const previewing = () => open && next !== null ? shown : null;
753
+ return {
754
+ kind: "plot-drag-value",
755
+ onMove(moveCtx) {
756
+ next = p.propose(p.toPlot(moveCtx.world));
757
+ shown = new Map(p.redraw(next).map((spec) => [spec.id, spec]));
758
+ },
759
+ previewIds: () => {
760
+ const specs = previewing();
761
+ return specs === null ? null : [...specs.keys()].filter((id) => scene.get(id) !== void 0);
762
+ },
763
+ previewPose: (id) => previewing()?.get(id)?.pose ?? null,
764
+ previewData: (id) => previewing()?.get(id)?.data ?? null,
765
+ onEnd(endCtx, reason) {
766
+ open = false;
767
+ if (reason !== "commit" || next === null) return;
768
+ commit({ rootId: p.rootId, data: p.propose(p.toPlot(endCtx.world)) }, { ...endCtx, deps: p.deps });
769
+ }
770
+ };
771
+ }
772
+ },
773
+ enabled: () => true
774
+ };
775
+ }
776
+
777
+ // src/editing/plotEditing.ts
778
+ var GRAB_PLOT_HANDLE_ACTION_ID = "plot.grabHandle";
779
+ var grabHandleAction = {
780
+ id: GRAB_PLOT_HANDLE_ACTION_ID,
781
+ label: "Grab chart handle",
782
+ group: "plot",
783
+ invoker: { timing: "immediate", run: () => {
784
+ } },
785
+ enabled: () => true
786
+ };
787
+ function plotEditing(opts) {
788
+ const actionId = opts.id ?? PLOT_DRAG_ACTION_ID;
789
+ const target = `affordance:${PLOT_HANDLE_AFFORDANCE_KIND}`;
790
+ return {
791
+ layer: plotHandleLayer(opts.scene),
792
+ contribution: {
793
+ id: "weasel-plot-editing",
794
+ eligibility: { claimed: true },
795
+ actions: [createPlotDragAction({ ...opts, id: actionId }), grabHandleAction],
796
+ bindings: [
797
+ { spec: { kind: "drag", target }, actionId, opts: { id: "weasel-plot-editing.drag" } },
798
+ { spec: { kind: "pointerDown", target }, actionId: GRAB_PLOT_HANDLE_ACTION_ID, opts: { id: "weasel-plot-editing.pointerDown" } },
799
+ { spec: { kind: "click", target }, actionId: GRAB_PLOT_HANDLE_ACTION_ID, opts: { id: "weasel-plot-editing.click" } }
800
+ ]
801
+ }
802
+ };
803
+ }
804
+ var ROOT = "plot";
805
+ var FEATURES = ["pick"];
806
+ function PlotView(props) {
807
+ return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props }) });
808
+ }
809
+ function Inner({ data, onChange, width, height, theme, measure, background, className, "aria-label": label }) {
810
+ const [initial] = useState(() => plotSpecs(data, {
811
+ id: ROOT,
812
+ box: { x: 0, y: 0, width, height },
813
+ ...theme ? { theme } : {},
814
+ ...measure ? { measure } : {}
815
+ }));
816
+ const scene = useScene({ systemLayers: [{ id: "main" }], initial });
817
+ const changed = useLatest(onChange);
818
+ useEffect(() => {
819
+ const root = scene.get(ROOT);
820
+ const pose = root?.pose;
821
+ const trait = plotRootOf(root);
822
+ const sized = pose !== void 0 && pose.width === width && pose.height === height;
823
+ if (trait === null || trait.data === data && sized && (theme === void 0 || trait.theme === theme)) return;
824
+ scene.untracked(() => {
825
+ if (!sized) scene.setPose(ROOT, { x: 0, y: 0, width, height });
826
+ replacePlotData(scene, ROOT, data, { ...theme ? { theme } : {}, ...measure ? { measure } : {} });
827
+ });
828
+ }, [scene, data, width, height, theme, measure]);
829
+ useEffect(() => {
830
+ let last = plotRootOf(scene.get(ROOT))?.data;
831
+ return scene.subscribe(() => {
832
+ const now = plotRootOf(scene.get(ROOT))?.data;
833
+ if (now === void 0 || now === last) return;
834
+ last = now;
835
+ changed.current?.(now);
836
+ });
837
+ }, [scene, changed]);
838
+ const editing = useMemo(() => plotEditing({ scene, ...measure ? { measure } : {} }), [scene, measure]);
839
+ const ambient = useMemo(() => [editing.contribution], [editing]);
840
+ const [canvas, setCanvas] = useState(null);
841
+ useEffect(() => canvas === null ? void 0 : canvas.registerLayer(editing.layer), [canvas, editing]);
842
+ return /* @__PURE__ */ jsx("div", { className, "data-wzl-plot": "", children: /* @__PURE__ */ jsx(
843
+ SceneCanvas,
844
+ {
845
+ ref: setCanvas,
846
+ scene,
847
+ features: FEATURES,
848
+ enableKeybindings: true,
849
+ ambient,
850
+ width,
851
+ height,
852
+ ...background ? { backgroundFill: background } : {},
853
+ ...label ? { "aria-label": label } : {}
854
+ }
855
+ ) });
856
+ }
857
+
858
+ export { GRAB_PLOT_HANDLE_ACTION_ID, PLOT_DRAG_ACTION_ID, PLOT_FONT, PLOT_HANDLE_AFFORDANCE_KIND, PLOT_HANDLE_LAYER_ID, PLOT_THEME, PlotView, arcMark, areaMark, axisMarks, axisRoom, bandScale, barMark, commitPlotToScene, compilePlot, compileXyChart, createPlotDragAction, formatInstant, formatNumber, lineMark, linearScale, logScale, markSpecs, niceDomain, ordinalScale, plotEditing, plotEditorOf, plotHandleLayer, plotHandleScratchOf, plotHandles, plotMarkId, plotMarkOf, plotOrigin, plotRootOf, plotRootsIn, plotSpecs, plotThemeFrom, pointMark, replacePlotData, ribbonMark, ruleMark, seriesColor, setXyValue, spanMark, stepTicks, textMark, textMeasure, tickStep, timeScale, xyLength, xyValueAt, xyValueDomain };
859
+ //# sourceMappingURL=index.js.map
860
+ //# sourceMappingURL=index.js.map