@danielsimonjr/mathts-plot 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +1140 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
// src/plot.ts
|
|
2
|
+
import { parse, evaluate, range } from "@danielsimonjr/mathts-functions";
|
|
3
|
+
|
|
4
|
+
// src/coerce.ts
|
|
5
|
+
import {
|
|
6
|
+
isMatrix,
|
|
7
|
+
isComplex,
|
|
8
|
+
isBigNumber,
|
|
9
|
+
isFraction,
|
|
10
|
+
number as toNumber
|
|
11
|
+
} from "@danielsimonjr/mathts-core";
|
|
12
|
+
function scalar(v) {
|
|
13
|
+
if (typeof v === "number") return v;
|
|
14
|
+
if (typeof v === "bigint") return Number(v);
|
|
15
|
+
if (isComplex(v)) return v.re;
|
|
16
|
+
if (isBigNumber(v) || isFraction(v)) {
|
|
17
|
+
try {
|
|
18
|
+
return toNumber(v);
|
|
19
|
+
} catch {
|
|
20
|
+
return NaN;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return Number(v);
|
|
24
|
+
}
|
|
25
|
+
function unwrap(raw) {
|
|
26
|
+
if (isMatrix(raw)) {
|
|
27
|
+
try {
|
|
28
|
+
return raw.toArray();
|
|
29
|
+
} catch {
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return raw;
|
|
34
|
+
}
|
|
35
|
+
function coerce1d(raw) {
|
|
36
|
+
const u = unwrap(raw);
|
|
37
|
+
const arr = Array.isArray(u) ? u.flat(Infinity) : u instanceof Float64Array ? Array.from(u) : [];
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const v of arr) {
|
|
40
|
+
const n = scalar(v);
|
|
41
|
+
if (Number.isFinite(n)) out.push(n);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function coerce1dPositional(raw) {
|
|
46
|
+
const u = unwrap(raw);
|
|
47
|
+
const arr = Array.isArray(u) ? u.flat(Infinity) : u instanceof Float64Array ? Array.from(u) : [];
|
|
48
|
+
return arr.map((v) => scalar(v));
|
|
49
|
+
}
|
|
50
|
+
function coerce2d(raw) {
|
|
51
|
+
const u = unwrap(raw);
|
|
52
|
+
if (!Array.isArray(u)) return [];
|
|
53
|
+
return u.map((row) => {
|
|
54
|
+
const r = row instanceof Float64Array ? Array.from(row) : Array.isArray(row) ? row : [row];
|
|
55
|
+
return r.map((v) => {
|
|
56
|
+
const n = scalar(v);
|
|
57
|
+
return Number.isFinite(n) ? n : NaN;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/scale.ts
|
|
63
|
+
function extent(xs) {
|
|
64
|
+
if (xs.length === 0) return [0, 1];
|
|
65
|
+
let lo = xs[0];
|
|
66
|
+
let hi = xs[0];
|
|
67
|
+
for (const v of xs) {
|
|
68
|
+
if (v < lo) lo = v;
|
|
69
|
+
if (v > hi) hi = v;
|
|
70
|
+
}
|
|
71
|
+
if (lo === hi) return [lo - 1, hi + 1];
|
|
72
|
+
return [lo, hi];
|
|
73
|
+
}
|
|
74
|
+
function linearScale(dom, range2) {
|
|
75
|
+
const [d0, d1] = dom;
|
|
76
|
+
const [r0, r1] = range2;
|
|
77
|
+
const m = (r1 - r0) / (d1 - d0);
|
|
78
|
+
return (v) => r0 + (v - d0) * m;
|
|
79
|
+
}
|
|
80
|
+
function logScale(dom, range2) {
|
|
81
|
+
const d0 = Math.log10(Math.max(dom[0], Number.MIN_VALUE));
|
|
82
|
+
const d1 = Math.log10(Math.max(dom[1], Number.MIN_VALUE));
|
|
83
|
+
const [r0, r1] = range2;
|
|
84
|
+
const m = (r1 - r0) / (d1 - d0);
|
|
85
|
+
return (v) => r0 + (Math.log10(Math.max(v, Number.MIN_VALUE)) - d0) * m;
|
|
86
|
+
}
|
|
87
|
+
function niceTicks(min, max, count = 5) {
|
|
88
|
+
if (!(max > min)) return [min];
|
|
89
|
+
const span = max - min;
|
|
90
|
+
const raw = span / count;
|
|
91
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
92
|
+
const norm = raw / mag;
|
|
93
|
+
const step2 = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
94
|
+
const start = Math.floor(min / step2) * step2;
|
|
95
|
+
const ticks = [];
|
|
96
|
+
for (let t = start; t <= max + step2 * 0.5; t += step2) ticks.push(Math.round(t / step2) * step2);
|
|
97
|
+
return ticks;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/svg.ts
|
|
101
|
+
var PALETTE = [
|
|
102
|
+
"#2a4d8f",
|
|
103
|
+
"#c0392b",
|
|
104
|
+
"#27ae60",
|
|
105
|
+
"#8e44ad",
|
|
106
|
+
"#d68910",
|
|
107
|
+
"#16a085",
|
|
108
|
+
"#7f8c8d",
|
|
109
|
+
"#2c3e50"
|
|
110
|
+
];
|
|
111
|
+
var THEMES = {
|
|
112
|
+
light: {
|
|
113
|
+
fg: "#1a1a2e",
|
|
114
|
+
muted: "#5a5a72",
|
|
115
|
+
grid: "#e6e6ee",
|
|
116
|
+
axis: "#5a5a72",
|
|
117
|
+
bg: "#ffffff",
|
|
118
|
+
series: PALETTE
|
|
119
|
+
},
|
|
120
|
+
dark: {
|
|
121
|
+
fg: "#e8e8f0",
|
|
122
|
+
muted: "#a0a0b0",
|
|
123
|
+
grid: "#2c2c3a",
|
|
124
|
+
axis: "#a0a0b0",
|
|
125
|
+
bg: "#14141c",
|
|
126
|
+
series: PALETTE
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function esc(s) {
|
|
130
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
131
|
+
}
|
|
132
|
+
function fmt(n) {
|
|
133
|
+
if (!Number.isFinite(n)) return "";
|
|
134
|
+
const a = Math.abs(n);
|
|
135
|
+
if (a !== 0 && (a < 1e-3 || a >= 1e6)) return n.toExponential(2);
|
|
136
|
+
return String(Math.round(n * 1e6) / 1e6);
|
|
137
|
+
}
|
|
138
|
+
var r2 = (n) => Math.round(n * 100) / 100;
|
|
139
|
+
function svgDoc(width, height, body, bg) {
|
|
140
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="100%" role="img"><rect x="0" y="0" width="${width}" height="${height}" fill="${bg}"/>${body}</svg>`;
|
|
141
|
+
}
|
|
142
|
+
function line(x1, y1, x2, y2, stroke, w = 1, opacity) {
|
|
143
|
+
const op = opacity !== void 0 && opacity < 1 ? ` stroke-opacity="${Math.round(opacity * 1e3) / 1e3}"` : "";
|
|
144
|
+
return `<line x1="${r2(x1)}" y1="${r2(y1)}" x2="${r2(x2)}" y2="${r2(y2)}" stroke="${stroke}" stroke-width="${w}"${op}/>`;
|
|
145
|
+
}
|
|
146
|
+
function circle(cx, cy, r, fill, opacity) {
|
|
147
|
+
const op = opacity !== void 0 && opacity < 1 ? ` fill-opacity="${Math.round(opacity * 1e3) / 1e3}"` : "";
|
|
148
|
+
return `<circle cx="${r2(cx)}" cy="${r2(cy)}" r="${r}" fill="${fill}"${op}/>`;
|
|
149
|
+
}
|
|
150
|
+
function rect(x, y, w, h, fill) {
|
|
151
|
+
return `<rect x="${r2(x)}" y="${r2(y)}" width="${r2(w)}" height="${r2(h)}" fill="${fill}"/>`;
|
|
152
|
+
}
|
|
153
|
+
function polyline(pts, stroke, w = 2) {
|
|
154
|
+
const p = pts.map(([x, y]) => `${r2(x)},${r2(y)}`).join(" ");
|
|
155
|
+
return `<polyline points="${p}" fill="none" stroke="${stroke}" stroke-width="${w}"/>`;
|
|
156
|
+
}
|
|
157
|
+
function polygon(pts, fill, stroke = "none") {
|
|
158
|
+
const p = pts.map(([x, y]) => `${r2(x)},${r2(y)}`).join(" ");
|
|
159
|
+
return `<polygon points="${p}" fill="${fill}" stroke="${stroke}"/>`;
|
|
160
|
+
}
|
|
161
|
+
function text(x, y, s, fill, anchor = "start", size = 12) {
|
|
162
|
+
return `<text x="${r2(x)}" y="${r2(y)}" text-anchor="${anchor}" font-family="system-ui,sans-serif" font-size="${size}" fill="${fill}">${esc(s)}</text>`;
|
|
163
|
+
}
|
|
164
|
+
function primSVG(p) {
|
|
165
|
+
switch (p.k) {
|
|
166
|
+
case "line":
|
|
167
|
+
return line(p.x1, p.y1, p.x2, p.y2, p.stroke, p.w, p.opacity);
|
|
168
|
+
case "circle":
|
|
169
|
+
return circle(p.cx, p.cy, p.r, p.fill, p.opacity);
|
|
170
|
+
case "rect":
|
|
171
|
+
return rect(p.x, p.y, p.w, p.h, p.fill);
|
|
172
|
+
case "polyline":
|
|
173
|
+
return polyline(p.pts, p.stroke, p.w);
|
|
174
|
+
case "polygon":
|
|
175
|
+
return polygon(p.pts, p.fill, p.stroke);
|
|
176
|
+
case "text":
|
|
177
|
+
return p.rotate !== void 0 ? `<text transform="translate(${p.x},${p.y}) rotate(${p.rotate})" text-anchor="${p.anchor}" font-family="system-ui,sans-serif" font-size="${p.size}" fill="${p.fill}">${esc(p.s)}</text>` : text(p.x, p.y, p.s, p.fill, p.anchor, p.size);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function emitSVG(scene) {
|
|
181
|
+
return svgDoc(scene.width, scene.height, scene.prims.map(primSVG).join(""), scene.bg);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/render-core.ts
|
|
185
|
+
function xy(layer) {
|
|
186
|
+
const ys = coerce1dPositional(layer.y);
|
|
187
|
+
const xs = layer.x ? coerce1dPositional(layer.x) : ys.map((_, i) => i);
|
|
188
|
+
const n = Math.min(xs.length, ys.length);
|
|
189
|
+
const pts = [];
|
|
190
|
+
for (let i = 0; i < n; i++) {
|
|
191
|
+
if (Number.isFinite(xs[i]) && Number.isFinite(ys[i])) pts.push([xs[i], ys[i]]);
|
|
192
|
+
}
|
|
193
|
+
return pts;
|
|
194
|
+
}
|
|
195
|
+
function renderLine(layer, f, i) {
|
|
196
|
+
const color = layer.color ?? f.color(i);
|
|
197
|
+
const pts = xy(layer).map(([x, y]) => [f.px(x), f.py(y)]);
|
|
198
|
+
return [
|
|
199
|
+
{ k: "polyline", pts, stroke: color, w: 2 },
|
|
200
|
+
...pts.map((p) => ({ k: "circle", cx: p[0], cy: p[1], r: 2.5, fill: color }))
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
function renderScatter(layer, f, i) {
|
|
204
|
+
const color = layer.color ?? f.color(i);
|
|
205
|
+
return xy(layer).map(
|
|
206
|
+
([x, y]) => ({ k: "circle", cx: f.px(x), cy: f.py(y), r: 3, fill: color })
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function renderBar(layer, f, i) {
|
|
210
|
+
const color = layer.color ?? f.color(i);
|
|
211
|
+
const pts = xy(layer);
|
|
212
|
+
const base = f.py(Math.max(0, f.ydom[0]));
|
|
213
|
+
const bw = pts.length > 1 ? Math.abs(f.px(pts[1][0]) - f.px(pts[0][0])) * 0.7 : 20;
|
|
214
|
+
return pts.map(([x, y]) => {
|
|
215
|
+
const yp = f.py(y);
|
|
216
|
+
return {
|
|
217
|
+
k: "rect",
|
|
218
|
+
x: f.px(x) - bw / 2,
|
|
219
|
+
y: Math.min(yp, base),
|
|
220
|
+
w: bw,
|
|
221
|
+
h: Math.abs(base - yp),
|
|
222
|
+
fill: color
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function renderArea(layer, f, i) {
|
|
227
|
+
const color = layer.color ?? f.color(i);
|
|
228
|
+
const pts = xy(layer).map(([x, y]) => [f.px(x), f.py(y)]);
|
|
229
|
+
if (pts.length === 0) return [];
|
|
230
|
+
const base = f.py(Math.max(0, f.ydom[0]));
|
|
231
|
+
const poly = [[pts[0][0], base], ...pts, [pts[pts.length - 1][0], base]];
|
|
232
|
+
return [
|
|
233
|
+
{ k: "polygon", pts: poly, fill: color + "55", stroke: "none" },
|
|
234
|
+
{ k: "polyline", pts, stroke: color, w: 2 }
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
function renderStep(layer, f, i) {
|
|
238
|
+
const color = layer.color ?? f.color(i);
|
|
239
|
+
const pts = xy(layer).map(([x, y]) => [f.px(x), f.py(y)]);
|
|
240
|
+
const stepped = [];
|
|
241
|
+
for (let k = 0; k < pts.length; k++) {
|
|
242
|
+
stepped.push(pts[k]);
|
|
243
|
+
if (k < pts.length - 1) stepped.push([pts[k + 1][0], pts[k][1]]);
|
|
244
|
+
}
|
|
245
|
+
return [{ k: "polyline", pts: stepped, stroke: color, w: 2 }];
|
|
246
|
+
}
|
|
247
|
+
function renderErrorbar(layer, f, i) {
|
|
248
|
+
const color = layer.color ?? f.color(i);
|
|
249
|
+
const ys = coerce1dPositional(layer.y);
|
|
250
|
+
const xs = layer.x ? coerce1dPositional(layer.x) : ys.map((_, k) => k);
|
|
251
|
+
const errs = coerce1dPositional(layer.yerr ?? []);
|
|
252
|
+
const n = Math.min(xs.length, ys.length);
|
|
253
|
+
const out = [];
|
|
254
|
+
for (let k = 0; k < n; k++) {
|
|
255
|
+
if (!Number.isFinite(xs[k]) || !Number.isFinite(ys[k])) continue;
|
|
256
|
+
const e = Number.isFinite(errs[k]) ? errs[k] : 0;
|
|
257
|
+
const cx = f.px(xs[k]);
|
|
258
|
+
const top = f.py(ys[k] + e);
|
|
259
|
+
const bot = f.py(ys[k] - e);
|
|
260
|
+
out.push(
|
|
261
|
+
{ k: "line", x1: cx, y1: top, x2: cx, y2: bot, stroke: color, w: 1.5 },
|
|
262
|
+
{ k: "line", x1: cx - 3, y1: top, x2: cx + 3, y2: top, stroke: color, w: 1.5 },
|
|
263
|
+
{ k: "line", x1: cx - 3, y1: bot, x2: cx + 3, y2: bot, stroke: color, w: 1.5 },
|
|
264
|
+
{ k: "circle", cx, cy: f.py(ys[k]), r: 3, fill: color }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function renderQuiver(layer, f, i) {
|
|
270
|
+
const color = layer.color ?? f.color(i);
|
|
271
|
+
const xs = layer.x ? coerce1dPositional(layer.x) : [];
|
|
272
|
+
const ys = coerce1dPositional(layer.y);
|
|
273
|
+
const us = coerce1dPositional(layer.u ?? []);
|
|
274
|
+
const vs = coerce1dPositional(layer.v ?? []);
|
|
275
|
+
const n = Math.min(xs.length, ys.length, us.length, vs.length);
|
|
276
|
+
const out = [];
|
|
277
|
+
for (let k = 0; k < n; k++) {
|
|
278
|
+
if (![xs[k], ys[k], us[k], vs[k]].every(Number.isFinite)) continue;
|
|
279
|
+
const x0 = f.px(xs[k]);
|
|
280
|
+
const y0 = f.py(ys[k]);
|
|
281
|
+
const x1 = f.px(xs[k] + us[k]);
|
|
282
|
+
const y1 = f.py(ys[k] + vs[k]);
|
|
283
|
+
const ang = Math.atan2(y1 - y0, x1 - x0);
|
|
284
|
+
const ah = 5;
|
|
285
|
+
out.push(
|
|
286
|
+
{ k: "line", x1: x0, y1: y0, x2: x1, y2: y1, stroke: color, w: 1.5 },
|
|
287
|
+
{
|
|
288
|
+
k: "line",
|
|
289
|
+
x1,
|
|
290
|
+
y1,
|
|
291
|
+
x2: x1 - ah * Math.cos(ang - 0.4),
|
|
292
|
+
y2: y1 - ah * Math.sin(ang - 0.4),
|
|
293
|
+
stroke: color,
|
|
294
|
+
w: 1.5
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
k: "line",
|
|
298
|
+
x1,
|
|
299
|
+
y1,
|
|
300
|
+
x2: x1 - ah * Math.cos(ang + 0.4),
|
|
301
|
+
y2: y1 - ah * Math.sin(ang + 0.4),
|
|
302
|
+
stroke: color,
|
|
303
|
+
w: 1.5
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
function renderLayer(layer, f, i) {
|
|
310
|
+
switch (layer.type) {
|
|
311
|
+
case "line":
|
|
312
|
+
return renderLine(layer, f, i);
|
|
313
|
+
case "scatter":
|
|
314
|
+
return renderScatter(layer, f, i);
|
|
315
|
+
case "bar":
|
|
316
|
+
return renderBar(layer, f, i);
|
|
317
|
+
case "area":
|
|
318
|
+
return renderArea(layer, f, i);
|
|
319
|
+
case "step":
|
|
320
|
+
return renderStep(layer, f, i);
|
|
321
|
+
case "errorbar":
|
|
322
|
+
return renderErrorbar(layer, f, i);
|
|
323
|
+
case "quiver":
|
|
324
|
+
return renderQuiver(layer, f, i);
|
|
325
|
+
default:
|
|
326
|
+
return renderLine(layer, f, i);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/tikz.ts
|
|
331
|
+
function texEsc(s) {
|
|
332
|
+
return s.replace(/[\\&%$#_{}~^]/g, (c) => {
|
|
333
|
+
switch (c) {
|
|
334
|
+
case "\\":
|
|
335
|
+
return "\\textbackslash{}";
|
|
336
|
+
case "~":
|
|
337
|
+
return "\\textasciitilde{}";
|
|
338
|
+
case "^":
|
|
339
|
+
return "\\textasciicircum{}";
|
|
340
|
+
default:
|
|
341
|
+
return `\\${c}`;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function tikzColor(c) {
|
|
346
|
+
if (c === "none") return { color: "none", alpha: 1 };
|
|
347
|
+
if (c.startsWith("#")) {
|
|
348
|
+
const h = c.slice(1);
|
|
349
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
350
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
351
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
352
|
+
const alpha = h.length >= 8 ? Math.round(parseInt(h.slice(6, 8), 16) / 255 * 100) / 100 : 1;
|
|
353
|
+
return { color: `{rgb,255:red,${r};green,${g};blue,${b}}`, alpha };
|
|
354
|
+
}
|
|
355
|
+
return { color: c, alpha: 1 };
|
|
356
|
+
}
|
|
357
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
358
|
+
var ANCHOR = {
|
|
359
|
+
start: "base west",
|
|
360
|
+
middle: "base",
|
|
361
|
+
end: "base east"
|
|
362
|
+
};
|
|
363
|
+
function primTikZ(p, X, Y, s) {
|
|
364
|
+
const pt = (x, y) => `(${X(x)},${Y(y)})`;
|
|
365
|
+
const path = (pts) => pts.map(([x, y]) => pt(x, y)).join(" -- ");
|
|
366
|
+
switch (p.k) {
|
|
367
|
+
case "line": {
|
|
368
|
+
const { color } = tikzColor(p.stroke);
|
|
369
|
+
const op = p.opacity !== void 0 && p.opacity < 1 ? `,draw opacity=${round(p.opacity)}` : "";
|
|
370
|
+
return `\\draw[color=${color},line width=${round(p.w * s)}pt${op}] ${pt(p.x1, p.y1)} -- ${pt(p.x2, p.y2)};`;
|
|
371
|
+
}
|
|
372
|
+
case "polyline": {
|
|
373
|
+
const { color } = tikzColor(p.stroke);
|
|
374
|
+
return `\\draw[color=${color},line width=${round(p.w * s)}pt] ${path(p.pts)};`;
|
|
375
|
+
}
|
|
376
|
+
case "polygon": {
|
|
377
|
+
const f = tikzColor(p.fill);
|
|
378
|
+
const st = tikzColor(p.stroke);
|
|
379
|
+
const drawOpt = p.stroke === "none" ? "draw=none" : `draw=${st.color}`;
|
|
380
|
+
const op = f.alpha < 1 ? `,fill opacity=${f.alpha}` : "";
|
|
381
|
+
return `\\filldraw[fill=${f.color},${drawOpt}${op}] ${path(p.pts)} -- cycle;`;
|
|
382
|
+
}
|
|
383
|
+
case "rect": {
|
|
384
|
+
const f = tikzColor(p.fill);
|
|
385
|
+
const op = f.alpha < 1 ? `[fill opacity=${f.alpha}]` : "";
|
|
386
|
+
return `\\fill${op}[color=${f.color}] ${pt(p.x, p.y)} rectangle ${pt(p.x + p.w, p.y + p.h)};`;
|
|
387
|
+
}
|
|
388
|
+
case "circle": {
|
|
389
|
+
const f = tikzColor(p.fill);
|
|
390
|
+
const a = (p.opacity ?? 1) * f.alpha;
|
|
391
|
+
const op = a < 1 ? `,fill opacity=${round(a)}` : "";
|
|
392
|
+
return `\\filldraw[fill=${f.color},draw=none${op}] ${pt(p.cx, p.cy)} circle (${round(p.r * s)}pt);`;
|
|
393
|
+
}
|
|
394
|
+
case "text": {
|
|
395
|
+
const { color } = tikzColor(p.fill);
|
|
396
|
+
const rot = p.rotate !== void 0 ? `,rotate=${p.rotate}` : "";
|
|
397
|
+
return `\\node[anchor=${ANCHOR[p.anchor]},text=${color},font=\\fontsize{${round(p.size * s)}}{${round(p.size * s * 1.2)}}\\selectfont${rot}] at ${pt(p.x, p.y)} {${texEsc(p.s)}};`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function emitTikZ(scene, opts = {}) {
|
|
402
|
+
const s = opts.tikz?.scale ?? 0.75;
|
|
403
|
+
const X = (x) => round(x * s);
|
|
404
|
+
const Y = (y) => round((scene.height - y) * s);
|
|
405
|
+
const bg = tikzColor(scene.bg);
|
|
406
|
+
const body = [
|
|
407
|
+
`\\fill[color=${bg.color}] (${X(0)},${Y(scene.height)}) rectangle (${X(scene.width)},${Y(0)});`,
|
|
408
|
+
...scene.prims.map((p) => primTikZ(p, X, Y, s))
|
|
409
|
+
];
|
|
410
|
+
const pic = `\\begin{tikzpicture}[x=1pt,y=1pt]
|
|
411
|
+
${body.join("\n")}
|
|
412
|
+
\\end{tikzpicture}`;
|
|
413
|
+
if (opts.tikz?.standalone === false) return pic;
|
|
414
|
+
return `\\documentclass[tikz,border=2pt]{standalone}
|
|
415
|
+
\\begin{document}
|
|
416
|
+
${pic}
|
|
417
|
+
\\end{document}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/emit.ts
|
|
421
|
+
function emit(scene, opts = {}) {
|
|
422
|
+
return opts.format === "tikz" ? emitTikZ(scene, opts) : emitSVG(scene);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/frame.ts
|
|
426
|
+
var MARGIN = { top: 34, right: 20, bottom: 46, left: 64 };
|
|
427
|
+
function combinedExtent(layers, pick) {
|
|
428
|
+
const all = [];
|
|
429
|
+
for (const l of layers) all.push(...pick(l));
|
|
430
|
+
return extent(all);
|
|
431
|
+
}
|
|
432
|
+
function noDataScene(width, height, theme) {
|
|
433
|
+
return {
|
|
434
|
+
width,
|
|
435
|
+
height,
|
|
436
|
+
bg: theme.bg,
|
|
437
|
+
prims: [
|
|
438
|
+
{
|
|
439
|
+
k: "text",
|
|
440
|
+
x: width / 2,
|
|
441
|
+
y: height / 2,
|
|
442
|
+
s: "no data",
|
|
443
|
+
fill: theme.muted,
|
|
444
|
+
anchor: "middle",
|
|
445
|
+
size: 13
|
|
446
|
+
}
|
|
447
|
+
]
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function draw2D(layers, opts = {}) {
|
|
451
|
+
const width = opts.width ?? 520;
|
|
452
|
+
const height = opts.height ?? 320;
|
|
453
|
+
const theme = THEMES[opts.theme ?? "light"];
|
|
454
|
+
try {
|
|
455
|
+
const yAll = layers.flatMap((l) => coerce1d(l.y));
|
|
456
|
+
if (yAll.length === 0) return emit(noDataScene(width, height, theme), opts);
|
|
457
|
+
const xdom = combinedExtent(
|
|
458
|
+
layers,
|
|
459
|
+
(l) => l.x ? coerce1d(l.x) : coerce1d(l.y).map((_, i) => i)
|
|
460
|
+
);
|
|
461
|
+
const ydom = combinedExtent(layers, (l) => coerce1d(l.y));
|
|
462
|
+
if (!Number.isFinite(xdom[0]) || !Number.isFinite(ydom[0]))
|
|
463
|
+
return emit(noDataScene(width, height, theme), opts);
|
|
464
|
+
const innerX = [MARGIN.left, width - MARGIN.right];
|
|
465
|
+
const innerY = [height - MARGIN.bottom, MARGIN.top];
|
|
466
|
+
const xScaleFn = opts.x?.scale === "log" ? logScale : linearScale;
|
|
467
|
+
const yScaleFn = opts.y?.scale === "log" ? logScale : linearScale;
|
|
468
|
+
const px = xScaleFn(xdom, innerX);
|
|
469
|
+
const py = yScaleFn(ydom, innerY);
|
|
470
|
+
const frame = {
|
|
471
|
+
px,
|
|
472
|
+
py,
|
|
473
|
+
xdom,
|
|
474
|
+
ydom,
|
|
475
|
+
theme,
|
|
476
|
+
width,
|
|
477
|
+
height,
|
|
478
|
+
color: (i) => theme.series[i % theme.series.length]
|
|
479
|
+
};
|
|
480
|
+
const prims = [];
|
|
481
|
+
const xticks = niceTicks(xdom[0], xdom[1]).filter((t) => t >= xdom[0] && t <= xdom[1]);
|
|
482
|
+
const yticks = niceTicks(ydom[0], ydom[1]).filter((t) => t >= ydom[0] && t <= ydom[1]);
|
|
483
|
+
for (const t of xticks)
|
|
484
|
+
prims.push({
|
|
485
|
+
k: "line",
|
|
486
|
+
x1: px(t),
|
|
487
|
+
y1: innerY[0],
|
|
488
|
+
x2: px(t),
|
|
489
|
+
y2: innerY[1],
|
|
490
|
+
stroke: theme.grid,
|
|
491
|
+
w: 1
|
|
492
|
+
});
|
|
493
|
+
for (const t of yticks)
|
|
494
|
+
prims.push({
|
|
495
|
+
k: "line",
|
|
496
|
+
x1: innerX[0],
|
|
497
|
+
y1: py(t),
|
|
498
|
+
x2: innerX[1],
|
|
499
|
+
y2: py(t),
|
|
500
|
+
stroke: theme.grid,
|
|
501
|
+
w: 1
|
|
502
|
+
});
|
|
503
|
+
prims.push({
|
|
504
|
+
k: "line",
|
|
505
|
+
x1: MARGIN.left,
|
|
506
|
+
y1: height - MARGIN.bottom,
|
|
507
|
+
x2: width - MARGIN.right,
|
|
508
|
+
y2: height - MARGIN.bottom,
|
|
509
|
+
stroke: theme.axis,
|
|
510
|
+
w: 1
|
|
511
|
+
});
|
|
512
|
+
prims.push({
|
|
513
|
+
k: "line",
|
|
514
|
+
x1: MARGIN.left,
|
|
515
|
+
y1: MARGIN.top,
|
|
516
|
+
x2: MARGIN.left,
|
|
517
|
+
y2: height - MARGIN.bottom,
|
|
518
|
+
stroke: theme.axis,
|
|
519
|
+
w: 1
|
|
520
|
+
});
|
|
521
|
+
for (const t of xticks)
|
|
522
|
+
prims.push({
|
|
523
|
+
k: "text",
|
|
524
|
+
x: px(t),
|
|
525
|
+
y: height - MARGIN.bottom + 16,
|
|
526
|
+
s: fmt(t),
|
|
527
|
+
fill: theme.muted,
|
|
528
|
+
anchor: "middle",
|
|
529
|
+
size: 11
|
|
530
|
+
});
|
|
531
|
+
for (const t of yticks)
|
|
532
|
+
prims.push({
|
|
533
|
+
k: "text",
|
|
534
|
+
x: MARGIN.left - 8,
|
|
535
|
+
y: py(t) + 4,
|
|
536
|
+
s: fmt(t),
|
|
537
|
+
fill: theme.muted,
|
|
538
|
+
anchor: "end",
|
|
539
|
+
size: 11
|
|
540
|
+
});
|
|
541
|
+
if (opts.title)
|
|
542
|
+
prims.push({
|
|
543
|
+
k: "text",
|
|
544
|
+
x: width / 2,
|
|
545
|
+
y: 20,
|
|
546
|
+
s: opts.title,
|
|
547
|
+
fill: theme.fg,
|
|
548
|
+
anchor: "middle",
|
|
549
|
+
size: 14
|
|
550
|
+
});
|
|
551
|
+
if (opts.xLabel)
|
|
552
|
+
prims.push({
|
|
553
|
+
k: "text",
|
|
554
|
+
x: MARGIN.left + (width - MARGIN.left - MARGIN.right) / 2,
|
|
555
|
+
y: height - 8,
|
|
556
|
+
s: opts.xLabel,
|
|
557
|
+
fill: theme.fg,
|
|
558
|
+
anchor: "middle",
|
|
559
|
+
size: 12
|
|
560
|
+
});
|
|
561
|
+
if (opts.yLabel)
|
|
562
|
+
prims.push({
|
|
563
|
+
k: "text",
|
|
564
|
+
x: 16,
|
|
565
|
+
y: MARGIN.top + (height - MARGIN.top - MARGIN.bottom) / 2,
|
|
566
|
+
s: opts.yLabel,
|
|
567
|
+
fill: theme.fg,
|
|
568
|
+
anchor: "middle",
|
|
569
|
+
size: 12,
|
|
570
|
+
rotate: -90
|
|
571
|
+
});
|
|
572
|
+
layers.forEach((l, i) => prims.push(...renderLayer(l, frame, i)));
|
|
573
|
+
if (opts.legend && layers.some((l) => l.label)) {
|
|
574
|
+
layers.forEach((l, i) => {
|
|
575
|
+
if (!l.label) return;
|
|
576
|
+
const ly = MARGIN.top + 4 + i * 16;
|
|
577
|
+
const c = l.color ?? frame.color(i);
|
|
578
|
+
prims.push({ k: "rect", x: width - MARGIN.right - 120, y: ly - 8, w: 10, h: 10, fill: c });
|
|
579
|
+
prims.push({
|
|
580
|
+
k: "text",
|
|
581
|
+
x: width - MARGIN.right - 106,
|
|
582
|
+
y: ly + 1,
|
|
583
|
+
s: l.label,
|
|
584
|
+
fill: theme.fg,
|
|
585
|
+
anchor: "start",
|
|
586
|
+
size: 11
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return emit({ width, height, bg: theme.bg, prims }, opts);
|
|
591
|
+
} catch {
|
|
592
|
+
return emit(noDataScene(width, height, theme), opts);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/marks2d.ts
|
|
597
|
+
function line2(x, y, opts) {
|
|
598
|
+
return draw2D([{ type: "line", x, y }], opts);
|
|
599
|
+
}
|
|
600
|
+
function scatter(x, y, opts) {
|
|
601
|
+
return draw2D([{ type: "scatter", x, y }], opts);
|
|
602
|
+
}
|
|
603
|
+
function bar(x, y, opts) {
|
|
604
|
+
return draw2D([{ type: "bar", x, y }], opts);
|
|
605
|
+
}
|
|
606
|
+
function area(x, y, opts) {
|
|
607
|
+
return draw2D([{ type: "area", x, y }], opts);
|
|
608
|
+
}
|
|
609
|
+
function step(x, y, opts) {
|
|
610
|
+
return draw2D([{ type: "step", x, y }], opts);
|
|
611
|
+
}
|
|
612
|
+
function errorbar(x, y, yerr, opts) {
|
|
613
|
+
return draw2D([{ type: "errorbar", x, y, yerr }], opts);
|
|
614
|
+
}
|
|
615
|
+
function quiver(x, y, u, v, opts) {
|
|
616
|
+
return draw2D([{ type: "quiver", x, y, u, v }], opts);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/overlay.ts
|
|
620
|
+
function overlay(layers, opts) {
|
|
621
|
+
return draw2D(layers, opts);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/palette.ts
|
|
625
|
+
var STOPS = [
|
|
626
|
+
[68, 1, 84],
|
|
627
|
+
[72, 40, 120],
|
|
628
|
+
[62, 74, 137],
|
|
629
|
+
[49, 104, 142],
|
|
630
|
+
[38, 130, 142],
|
|
631
|
+
[31, 158, 137],
|
|
632
|
+
[53, 183, 121],
|
|
633
|
+
[110, 206, 88],
|
|
634
|
+
[253, 231, 37]
|
|
635
|
+
];
|
|
636
|
+
var hex = (n) => Math.round(n).toString(16).padStart(2, "0");
|
|
637
|
+
function viridis(t) {
|
|
638
|
+
const c = Math.max(0, Math.min(1, t));
|
|
639
|
+
const s = c * (STOPS.length - 1);
|
|
640
|
+
const i = Math.min(STOPS.length - 2, Math.floor(s));
|
|
641
|
+
const f = s - i;
|
|
642
|
+
const a = STOPS[i];
|
|
643
|
+
const b = STOPS[i + 1];
|
|
644
|
+
const r = a[0] + (b[0] - a[0]) * f;
|
|
645
|
+
const g = a[1] + (b[1] - a[1]) * f;
|
|
646
|
+
const bl = a[2] + (b[2] - a[2]) * f;
|
|
647
|
+
return `#${hex(r)}${hex(g)}${hex(bl)}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/contour.ts
|
|
651
|
+
var MARGIN2 = { top: 34, right: 22, bottom: 30, left: 40 };
|
|
652
|
+
function contour(z, opts = {}) {
|
|
653
|
+
const width = opts.width ?? 520;
|
|
654
|
+
const height = opts.height ?? 320;
|
|
655
|
+
const theme = THEMES[opts.theme ?? "light"];
|
|
656
|
+
const grid = coerce2d(z);
|
|
657
|
+
const rows = grid.length;
|
|
658
|
+
const cols = rows ? grid[0].length : 0;
|
|
659
|
+
if (rows < 2 || cols < 2) {
|
|
660
|
+
const scene = {
|
|
661
|
+
width,
|
|
662
|
+
height,
|
|
663
|
+
bg: theme.bg,
|
|
664
|
+
prims: [
|
|
665
|
+
{
|
|
666
|
+
k: "text",
|
|
667
|
+
x: width / 2,
|
|
668
|
+
y: height / 2,
|
|
669
|
+
s: "no data",
|
|
670
|
+
fill: theme.muted,
|
|
671
|
+
anchor: "middle",
|
|
672
|
+
size: 13
|
|
673
|
+
}
|
|
674
|
+
]
|
|
675
|
+
};
|
|
676
|
+
return emit(scene, opts);
|
|
677
|
+
}
|
|
678
|
+
let lo = Infinity;
|
|
679
|
+
let hi = -Infinity;
|
|
680
|
+
for (const row of grid)
|
|
681
|
+
for (const v of row)
|
|
682
|
+
if (Number.isFinite(v)) {
|
|
683
|
+
if (v < lo) lo = v;
|
|
684
|
+
if (v > hi) hi = v;
|
|
685
|
+
}
|
|
686
|
+
const nLevels = opts.levels ?? 10;
|
|
687
|
+
const px = linearScale([0, cols - 1], [MARGIN2.left, width - MARGIN2.right]);
|
|
688
|
+
const py = linearScale([0, rows - 1], [height - MARGIN2.bottom, MARGIN2.top]);
|
|
689
|
+
const prims = [];
|
|
690
|
+
const span = hi > lo ? hi - lo : 1;
|
|
691
|
+
for (let li = 1; li <= nLevels; li++) {
|
|
692
|
+
const level = lo + span * li / (nLevels + 1);
|
|
693
|
+
const color = viridis(li / (nLevels + 1));
|
|
694
|
+
const interp = (va, vb) => (level - va) / (vb - va);
|
|
695
|
+
for (let r = 0; r < rows - 1; r++) {
|
|
696
|
+
for (let c = 0; c < cols - 1; c++) {
|
|
697
|
+
const tl = grid[r][c];
|
|
698
|
+
const tr = grid[r][c + 1];
|
|
699
|
+
const br = grid[r + 1][c + 1];
|
|
700
|
+
const bl = grid[r + 1][c];
|
|
701
|
+
if (![tl, tr, br, bl].every(Number.isFinite)) continue;
|
|
702
|
+
const cross = [];
|
|
703
|
+
if (tl < level !== tr < level) cross.push([c + interp(tl, tr), r]);
|
|
704
|
+
if (tr < level !== br < level) cross.push([c + 1, r + interp(tr, br)]);
|
|
705
|
+
if (bl < level !== br < level) cross.push([c + interp(bl, br), r + 1]);
|
|
706
|
+
if (tl < level !== bl < level) cross.push([c, r + interp(tl, bl)]);
|
|
707
|
+
for (let k = 0; k + 1 < cross.length; k += 2) {
|
|
708
|
+
const [ax, ay] = cross[k];
|
|
709
|
+
const [bx, by] = cross[k + 1];
|
|
710
|
+
prims.push({
|
|
711
|
+
k: "line",
|
|
712
|
+
x1: px(ax),
|
|
713
|
+
y1: py(ay),
|
|
714
|
+
x2: px(bx),
|
|
715
|
+
y2: py(by),
|
|
716
|
+
stroke: color,
|
|
717
|
+
w: 1.5
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (opts.title)
|
|
724
|
+
prims.push({
|
|
725
|
+
k: "text",
|
|
726
|
+
x: width / 2,
|
|
727
|
+
y: 20,
|
|
728
|
+
s: opts.title,
|
|
729
|
+
fill: theme.fg,
|
|
730
|
+
anchor: "middle",
|
|
731
|
+
size: 14
|
|
732
|
+
});
|
|
733
|
+
return emit({ width, height, bg: theme.bg, prims }, opts);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/three/project.ts
|
|
737
|
+
function project(p, cam) {
|
|
738
|
+
const az = cam.azim * Math.PI / 180;
|
|
739
|
+
const el = cam.elev * Math.PI / 180;
|
|
740
|
+
const [x, y, z] = p;
|
|
741
|
+
const xa = x * Math.cos(az) - y * Math.sin(az);
|
|
742
|
+
const ya = x * Math.sin(az) + y * Math.cos(az);
|
|
743
|
+
const sx = xa;
|
|
744
|
+
const sy = z * Math.cos(el) - ya * Math.sin(el);
|
|
745
|
+
const depth = z * Math.sin(el) + ya * Math.cos(el);
|
|
746
|
+
return [sx, sy, depth];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/three/surface.ts
|
|
750
|
+
var MARGIN3 = 40;
|
|
751
|
+
function surface(z, opts = {}) {
|
|
752
|
+
const width = opts.width ?? 520;
|
|
753
|
+
const height = opts.height ?? 320;
|
|
754
|
+
const theme = THEMES[opts.theme ?? "light"];
|
|
755
|
+
const grid = coerce2d(z);
|
|
756
|
+
const rows = grid.length;
|
|
757
|
+
const cols = rows ? grid[0].length : 0;
|
|
758
|
+
if (rows < 2 || cols < 2) {
|
|
759
|
+
const scene = {
|
|
760
|
+
width,
|
|
761
|
+
height,
|
|
762
|
+
bg: theme.bg,
|
|
763
|
+
prims: [
|
|
764
|
+
{
|
|
765
|
+
k: "text",
|
|
766
|
+
x: width / 2,
|
|
767
|
+
y: height / 2,
|
|
768
|
+
s: "no data",
|
|
769
|
+
fill: theme.muted,
|
|
770
|
+
anchor: "middle",
|
|
771
|
+
size: 13
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
};
|
|
775
|
+
return emit(scene, opts);
|
|
776
|
+
}
|
|
777
|
+
const cam = { azim: opts.azim ?? 45, elev: opts.elev ?? 25 };
|
|
778
|
+
let zlo = Infinity;
|
|
779
|
+
let zhi = -Infinity;
|
|
780
|
+
for (const row of grid)
|
|
781
|
+
for (const v of row)
|
|
782
|
+
if (Number.isFinite(v)) {
|
|
783
|
+
if (v < zlo) zlo = v;
|
|
784
|
+
if (v > zhi) zhi = v;
|
|
785
|
+
}
|
|
786
|
+
const zspan = zhi > zlo ? zhi - zlo : 1;
|
|
787
|
+
const proj = [];
|
|
788
|
+
let sxlo = Infinity, sxhi = -Infinity, sylo = Infinity, syhi = -Infinity;
|
|
789
|
+
for (let r = 0; r < rows; r++) {
|
|
790
|
+
const prow = [];
|
|
791
|
+
for (let c = 0; c < cols; c++) {
|
|
792
|
+
const wx = c / (cols - 1) * 2 - 1;
|
|
793
|
+
const wy = r / (rows - 1) * 2 - 1;
|
|
794
|
+
const wz = (grid[r][c] - zlo) / zspan * 2 - 1;
|
|
795
|
+
const p = project([wx, wy, wz], cam);
|
|
796
|
+
prow.push(p);
|
|
797
|
+
if (p[0] < sxlo) sxlo = p[0];
|
|
798
|
+
if (p[0] > sxhi) sxhi = p[0];
|
|
799
|
+
if (p[1] < sylo) sylo = p[1];
|
|
800
|
+
if (p[1] > syhi) syhi = p[1];
|
|
801
|
+
}
|
|
802
|
+
proj.push(prow);
|
|
803
|
+
}
|
|
804
|
+
const sxSpan = sxhi > sxlo ? sxhi - sxlo : 1;
|
|
805
|
+
const sySpan = syhi > sylo ? syhi - sylo : 1;
|
|
806
|
+
const toScreen = (p) => [
|
|
807
|
+
MARGIN3 + (p[0] - sxlo) / sxSpan * (width - 2 * MARGIN3),
|
|
808
|
+
height - MARGIN3 - (p[1] - sylo) / sySpan * (height - 2 * MARGIN3)
|
|
809
|
+
];
|
|
810
|
+
const kind = opts.kind ?? "filled";
|
|
811
|
+
const prims = [];
|
|
812
|
+
if (kind === "wireframe") {
|
|
813
|
+
for (let r = 0; r < rows; r++)
|
|
814
|
+
prims.push({ k: "polyline", pts: proj[r].map(toScreen), stroke: theme.series[0], w: 1 });
|
|
815
|
+
for (let c = 0; c < cols; c++)
|
|
816
|
+
prims.push({
|
|
817
|
+
k: "polyline",
|
|
818
|
+
pts: proj.map((row) => toScreen(row[c])),
|
|
819
|
+
stroke: theme.series[0],
|
|
820
|
+
w: 1
|
|
821
|
+
});
|
|
822
|
+
} else {
|
|
823
|
+
const quads = [];
|
|
824
|
+
for (let r = 0; r < rows - 1; r++) {
|
|
825
|
+
for (let c = 0; c < cols - 1; c++) {
|
|
826
|
+
const corners = [proj[r][c], proj[r][c + 1], proj[r + 1][c + 1], proj[r + 1][c]];
|
|
827
|
+
const depth = (corners[0][2] + corners[1][2] + corners[2][2] + corners[3][2]) / 4;
|
|
828
|
+
const meanZ = (grid[r][c] + grid[r][c + 1] + grid[r + 1][c + 1] + grid[r + 1][c]) / 4;
|
|
829
|
+
quads.push({ depth, pts: corners.map(toScreen), shade: (meanZ - zlo) / zspan });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
quads.sort((a, b) => b.depth - a.depth);
|
|
833
|
+
for (const q of quads)
|
|
834
|
+
prims.push({ k: "polygon", pts: q.pts, fill: viridis(q.shade), stroke: theme.grid });
|
|
835
|
+
}
|
|
836
|
+
if (opts.title)
|
|
837
|
+
prims.push({
|
|
838
|
+
k: "text",
|
|
839
|
+
x: width / 2,
|
|
840
|
+
y: 20,
|
|
841
|
+
s: opts.title,
|
|
842
|
+
fill: theme.fg,
|
|
843
|
+
anchor: "middle",
|
|
844
|
+
size: 14
|
|
845
|
+
});
|
|
846
|
+
return emit({ width, height, bg: theme.bg, prims }, opts);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/plot.ts
|
|
850
|
+
function isLayerArray(v) {
|
|
851
|
+
return Array.isArray(v) && v.length > 0 && typeof v[0] === "object" && v[0] !== null && "type" in v[0];
|
|
852
|
+
}
|
|
853
|
+
function freeVars(source) {
|
|
854
|
+
const names = /* @__PURE__ */ new Set();
|
|
855
|
+
const funcs = /* @__PURE__ */ new Set();
|
|
856
|
+
try {
|
|
857
|
+
const node = parse(source);
|
|
858
|
+
if (typeof node.traverse === "function") {
|
|
859
|
+
node.traverse((n) => {
|
|
860
|
+
const nn = n;
|
|
861
|
+
if (nn.type === "SymbolNode" && nn.name) names.add(nn.name);
|
|
862
|
+
if (nn.type === "FunctionNode" && nn.name) funcs.add(nn.name);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
}
|
|
867
|
+
funcs.forEach((f) => names.delete(f));
|
|
868
|
+
["pi", "e", "tau"].forEach((c) => names.delete(c));
|
|
869
|
+
return [...names];
|
|
870
|
+
}
|
|
871
|
+
function plotExpr(source, opts) {
|
|
872
|
+
const { kind, ...rest } = opts;
|
|
873
|
+
const from = opts.from ?? -10;
|
|
874
|
+
const to = opts.to ?? 10;
|
|
875
|
+
const n = opts.samples ?? 200;
|
|
876
|
+
const scope = opts.scope ?? {};
|
|
877
|
+
const rangeFn = range;
|
|
878
|
+
const grid = rangeFn(from, to, (to - from) / (n - 1), true).toArray?.() ?? Array.from({ length: n }, (_, i) => from + (to - from) * i / (n - 1));
|
|
879
|
+
const vars = freeVars(source);
|
|
880
|
+
if (vars.length <= 1) {
|
|
881
|
+
const v = vars[0] ?? "x";
|
|
882
|
+
const ys = grid.map((val) => {
|
|
883
|
+
try {
|
|
884
|
+
return Number(evaluate(source, { ...scope, [v]: val }));
|
|
885
|
+
} catch {
|
|
886
|
+
return NaN;
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return line2(grid, ys, { xLabel: v, title: source, ...rest });
|
|
890
|
+
}
|
|
891
|
+
const [vx, vy] = vars;
|
|
892
|
+
const g2 = grid.map(
|
|
893
|
+
(yv) => grid.map((xv) => {
|
|
894
|
+
try {
|
|
895
|
+
return Number(evaluate(source, { ...scope, [vx]: xv, [vy]: yv }));
|
|
896
|
+
} catch {
|
|
897
|
+
return NaN;
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
return kind === "3d" || kind === "surface" ? surface(g2, { title: source, ...rest }) : contour(g2, { title: source, ...rest });
|
|
902
|
+
}
|
|
903
|
+
function plot(a, b, c) {
|
|
904
|
+
if (typeof a === "string") return plotExpr(a, b ?? {});
|
|
905
|
+
if (isLayerArray(a)) return overlay(a, b ?? {});
|
|
906
|
+
if (Array.isArray(b) || b instanceof Float64Array) return line2(a, b, c);
|
|
907
|
+
return line2(void 0, a, b ?? {});
|
|
908
|
+
}
|
|
909
|
+
function toTikZ(a, b, c) {
|
|
910
|
+
if (typeof a === "string") return plot(a, { ...b ?? {}, format: "tikz" });
|
|
911
|
+
if (Array.isArray(a) && a.length > 0 && typeof a[0] === "object" && a[0] !== null && "type" in a[0])
|
|
912
|
+
return plot(a, { ...b ?? {}, format: "tikz" });
|
|
913
|
+
if (Array.isArray(b) || b instanceof Float64Array)
|
|
914
|
+
return plot(a, b, { ...c ?? {}, format: "tikz" });
|
|
915
|
+
return plot(a, { ...b ?? {}, format: "tikz" });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/histogram.ts
|
|
919
|
+
import { histogram as histBins } from "@danielsimonjr/mathts-functions";
|
|
920
|
+
function histogram(data, opts = {}) {
|
|
921
|
+
const nums = coerce1d(data);
|
|
922
|
+
if (nums.length === 0) return draw2D([], opts);
|
|
923
|
+
const { counts, edges } = histBins(nums, opts.bins ?? 10);
|
|
924
|
+
const centers = counts.map((_, i) => (edges[i] + edges[i + 1]) / 2);
|
|
925
|
+
return draw2D([{ type: "bar", x: centers, y: counts, color: opts.palette?.[0] }], {
|
|
926
|
+
yLabel: "count",
|
|
927
|
+
...opts
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/heatmap.ts
|
|
932
|
+
var MARGIN4 = { top: 34, right: 22, bottom: 30, left: 40 };
|
|
933
|
+
function heatmap(z, opts = {}) {
|
|
934
|
+
const width = opts.width ?? 520;
|
|
935
|
+
const height = opts.height ?? 320;
|
|
936
|
+
const theme = THEMES[opts.theme ?? "light"];
|
|
937
|
+
const grid = coerce2d(z);
|
|
938
|
+
const rows = grid.length;
|
|
939
|
+
const cols = rows ? grid[0].length : 0;
|
|
940
|
+
if (rows === 0 || cols === 0) {
|
|
941
|
+
const scene = {
|
|
942
|
+
width,
|
|
943
|
+
height,
|
|
944
|
+
bg: theme.bg,
|
|
945
|
+
prims: [
|
|
946
|
+
{
|
|
947
|
+
k: "text",
|
|
948
|
+
x: width / 2,
|
|
949
|
+
y: height / 2,
|
|
950
|
+
s: "no data",
|
|
951
|
+
fill: theme.muted,
|
|
952
|
+
anchor: "middle",
|
|
953
|
+
size: 13
|
|
954
|
+
}
|
|
955
|
+
]
|
|
956
|
+
};
|
|
957
|
+
return emit(scene, opts);
|
|
958
|
+
}
|
|
959
|
+
let lo = Infinity;
|
|
960
|
+
let hi = -Infinity;
|
|
961
|
+
for (const row of grid)
|
|
962
|
+
for (const v of row)
|
|
963
|
+
if (Number.isFinite(v)) {
|
|
964
|
+
if (v < lo) lo = v;
|
|
965
|
+
if (v > hi) hi = v;
|
|
966
|
+
}
|
|
967
|
+
const span = hi > lo ? hi - lo : 1;
|
|
968
|
+
const cw = (width - MARGIN4.left - MARGIN4.right) / cols;
|
|
969
|
+
const ch = (height - MARGIN4.top - MARGIN4.bottom) / rows;
|
|
970
|
+
const prims = [];
|
|
971
|
+
for (let r = 0; r < rows; r++) {
|
|
972
|
+
for (let c = 0; c < cols; c++) {
|
|
973
|
+
const v = grid[r][c];
|
|
974
|
+
const color = Number.isFinite(v) ? viridis((v - lo) / span) : theme.grid;
|
|
975
|
+
prims.push({
|
|
976
|
+
k: "rect",
|
|
977
|
+
x: MARGIN4.left + c * cw,
|
|
978
|
+
y: MARGIN4.top + r * ch,
|
|
979
|
+
w: cw + 0.5,
|
|
980
|
+
h: ch + 0.5,
|
|
981
|
+
fill: color
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (opts.title)
|
|
986
|
+
prims.push({
|
|
987
|
+
k: "text",
|
|
988
|
+
x: width / 2,
|
|
989
|
+
y: 20,
|
|
990
|
+
s: opts.title,
|
|
991
|
+
fill: theme.fg,
|
|
992
|
+
anchor: "middle",
|
|
993
|
+
size: 14
|
|
994
|
+
});
|
|
995
|
+
return emit({ width, height, bg: theme.bg, prims }, opts);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/three/points3d.ts
|
|
999
|
+
var MARGIN5 = 40;
|
|
1000
|
+
function minOf(a) {
|
|
1001
|
+
let m = Infinity;
|
|
1002
|
+
for (const v of a) if (v < m) m = v;
|
|
1003
|
+
return m;
|
|
1004
|
+
}
|
|
1005
|
+
function maxOf(a) {
|
|
1006
|
+
let m = -Infinity;
|
|
1007
|
+
for (const v of a) if (v > m) m = v;
|
|
1008
|
+
return m;
|
|
1009
|
+
}
|
|
1010
|
+
function projectAll(x, y, z, cam) {
|
|
1011
|
+
const xs = coerce1d(x);
|
|
1012
|
+
const ys = coerce1d(y);
|
|
1013
|
+
const zs = coerce1d(z);
|
|
1014
|
+
const n = Math.min(xs.length, ys.length, zs.length);
|
|
1015
|
+
const nx = (a) => {
|
|
1016
|
+
const lo = minOf(a);
|
|
1017
|
+
const hi = maxOf(a);
|
|
1018
|
+
const s = hi > lo ? hi - lo : 1;
|
|
1019
|
+
return (v) => (v - lo) / s * 2 - 1;
|
|
1020
|
+
};
|
|
1021
|
+
const fx = nx(xs.slice(0, n));
|
|
1022
|
+
const fy = nx(ys.slice(0, n));
|
|
1023
|
+
const fz = nx(zs.slice(0, n));
|
|
1024
|
+
const screen = [];
|
|
1025
|
+
for (let i = 0; i < n; i++) screen.push(project([fx(xs[i]), fy(ys[i]), fz(zs[i])], cam));
|
|
1026
|
+
return { screen };
|
|
1027
|
+
}
|
|
1028
|
+
function render(x, y, z, opts, mode) {
|
|
1029
|
+
const width = opts.width ?? 520;
|
|
1030
|
+
const height = opts.height ?? 320;
|
|
1031
|
+
const theme = THEMES[opts.theme ?? "light"];
|
|
1032
|
+
const cam = { azim: opts.azim ?? 45, elev: opts.elev ?? 25 };
|
|
1033
|
+
const { screen } = projectAll(x, y, z, cam);
|
|
1034
|
+
if (screen.length === 0) {
|
|
1035
|
+
const scene = {
|
|
1036
|
+
width,
|
|
1037
|
+
height,
|
|
1038
|
+
bg: theme.bg,
|
|
1039
|
+
prims: [
|
|
1040
|
+
{
|
|
1041
|
+
k: "text",
|
|
1042
|
+
x: width / 2,
|
|
1043
|
+
y: height / 2,
|
|
1044
|
+
s: "no data",
|
|
1045
|
+
fill: theme.muted,
|
|
1046
|
+
anchor: "middle",
|
|
1047
|
+
size: 13
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
};
|
|
1051
|
+
return emit(scene, opts);
|
|
1052
|
+
}
|
|
1053
|
+
let sxlo = Infinity, sxhi = -Infinity, sylo = Infinity, syhi = -Infinity;
|
|
1054
|
+
for (const p of screen) {
|
|
1055
|
+
if (p[0] < sxlo) sxlo = p[0];
|
|
1056
|
+
if (p[0] > sxhi) sxhi = p[0];
|
|
1057
|
+
if (p[1] < sylo) sylo = p[1];
|
|
1058
|
+
if (p[1] > syhi) syhi = p[1];
|
|
1059
|
+
}
|
|
1060
|
+
const sxSpan = sxhi > sxlo ? sxhi - sxlo : 1;
|
|
1061
|
+
const sySpan = syhi > sylo ? syhi - sylo : 1;
|
|
1062
|
+
const toScreen = (p) => [
|
|
1063
|
+
MARGIN5 + (p[0] - sxlo) / sxSpan * (width - 2 * MARGIN5),
|
|
1064
|
+
height - MARGIN5 - (p[1] - sylo) / sySpan * (height - 2 * MARGIN5)
|
|
1065
|
+
];
|
|
1066
|
+
const color = theme.series[0];
|
|
1067
|
+
const prims = [];
|
|
1068
|
+
if (mode === "line") {
|
|
1069
|
+
const depths = screen.map((p) => p[2]);
|
|
1070
|
+
const dlo = minOf(depths);
|
|
1071
|
+
const dhi = maxOf(depths);
|
|
1072
|
+
const dspan = dhi > dlo ? dhi - dlo : 1;
|
|
1073
|
+
const segs = [];
|
|
1074
|
+
for (let i = 0; i + 1 < screen.length; i++) {
|
|
1075
|
+
const a = screen[i];
|
|
1076
|
+
const b = screen[i + 1];
|
|
1077
|
+
segs.push({ a, b, depth: (a[2] + b[2]) / 2 });
|
|
1078
|
+
}
|
|
1079
|
+
segs.sort((s1, s2) => s2.depth - s1.depth);
|
|
1080
|
+
for (const sg of segs) {
|
|
1081
|
+
const near = 1 - (sg.depth - dlo) / dspan;
|
|
1082
|
+
const opacity = 0.4 + 0.6 * near;
|
|
1083
|
+
const [x1, y1] = toScreen(sg.a);
|
|
1084
|
+
const [x2, y2] = toScreen(sg.b);
|
|
1085
|
+
prims.push({ k: "line", x1, y1, x2, y2, stroke: color, w: 2, opacity });
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
const depths = screen.map((p) => p[2]);
|
|
1089
|
+
const dlo = minOf(depths);
|
|
1090
|
+
const dhi = maxOf(depths);
|
|
1091
|
+
const dspan = dhi > dlo ? dhi - dlo : 1;
|
|
1092
|
+
const sorted = screen.slice().sort((a, b) => b[2] - a[2]);
|
|
1093
|
+
for (const p of sorted) {
|
|
1094
|
+
const [sx, sy] = toScreen(p);
|
|
1095
|
+
const near = 1 - (p[2] - dlo) / dspan;
|
|
1096
|
+
const opacity = 0.4 + 0.6 * near;
|
|
1097
|
+
prims.push({ k: "circle", cx: sx, cy: sy, r: 3, fill: color, opacity });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (opts.title)
|
|
1101
|
+
prims.push({
|
|
1102
|
+
k: "text",
|
|
1103
|
+
x: width / 2,
|
|
1104
|
+
y: 20,
|
|
1105
|
+
s: opts.title,
|
|
1106
|
+
fill: theme.fg,
|
|
1107
|
+
anchor: "middle",
|
|
1108
|
+
size: 14
|
|
1109
|
+
});
|
|
1110
|
+
return emit({ width, height, bg: theme.bg, prims }, opts);
|
|
1111
|
+
}
|
|
1112
|
+
function scatter3d(x, y, z, opts = {}) {
|
|
1113
|
+
return render(x, y, z, opts, "points");
|
|
1114
|
+
}
|
|
1115
|
+
function curve3d(x, y, z, opts = {}) {
|
|
1116
|
+
return render(x, y, z, opts, "line");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/index.ts
|
|
1120
|
+
var VERSION = "0.2.0";
|
|
1121
|
+
export {
|
|
1122
|
+
VERSION,
|
|
1123
|
+
area,
|
|
1124
|
+
bar,
|
|
1125
|
+
contour,
|
|
1126
|
+
curve3d,
|
|
1127
|
+
errorbar,
|
|
1128
|
+
heatmap,
|
|
1129
|
+
histogram,
|
|
1130
|
+
line2 as line,
|
|
1131
|
+
overlay,
|
|
1132
|
+
plot,
|
|
1133
|
+
quiver,
|
|
1134
|
+
scatter,
|
|
1135
|
+
scatter3d,
|
|
1136
|
+
step,
|
|
1137
|
+
surface,
|
|
1138
|
+
toTikZ,
|
|
1139
|
+
viridis
|
|
1140
|
+
};
|