@photonviz/core 0.3.1 → 0.3.2
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 +14 -5
- package/dist/index.d.ts +696 -2
- package/dist/index.js +1818 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3300,7 +3300,7 @@ var LineLayer = class {
|
|
|
3300
3300
|
|
|
3301
3301
|
// src/geo/earcut.ts
|
|
3302
3302
|
var Z_ORDER_THRESHOLD = 80;
|
|
3303
|
-
var
|
|
3303
|
+
var Node2 = class {
|
|
3304
3304
|
constructor(i, x, y) {
|
|
3305
3305
|
this.i = i;
|
|
3306
3306
|
this.x = x;
|
|
@@ -3603,8 +3603,8 @@ function sectorContainsSector(m, p) {
|
|
|
3603
3603
|
return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
|
|
3604
3604
|
}
|
|
3605
3605
|
function splitPolygon(a, b) {
|
|
3606
|
-
const a2 = new
|
|
3607
|
-
const b2 = new
|
|
3606
|
+
const a2 = new Node2(a.i, a.x, a.y);
|
|
3607
|
+
const b2 = new Node2(b.i, b.x, b.y);
|
|
3608
3608
|
const an = a.next;
|
|
3609
3609
|
const bp = b.prev;
|
|
3610
3610
|
a.next = b;
|
|
@@ -3618,7 +3618,7 @@ function splitPolygon(a, b) {
|
|
|
3618
3618
|
return b2;
|
|
3619
3619
|
}
|
|
3620
3620
|
function insertNode(i, x, y, last) {
|
|
3621
|
-
const p = new
|
|
3621
|
+
const p = new Node2(i, x, y);
|
|
3622
3622
|
if (!last) {
|
|
3623
3623
|
p.prev = p;
|
|
3624
3624
|
p.next = p;
|
|
@@ -4969,6 +4969,28 @@ function drawMarker(ctx, px, py, color) {
|
|
|
4969
4969
|
ctx.restore();
|
|
4970
4970
|
}
|
|
4971
4971
|
|
|
4972
|
+
// src/render/export.ts
|
|
4973
|
+
function canvasToBlob(canvas, type = "image/png", quality) {
|
|
4974
|
+
return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
4975
|
+
}
|
|
4976
|
+
async function downloadCanvas(canvas, filename = "chart.png", type = "image/png", quality) {
|
|
4977
|
+
const blob = await canvasToBlob(canvas, type, quality);
|
|
4978
|
+
if (!blob) return;
|
|
4979
|
+
const url = URL.createObjectURL(blob);
|
|
4980
|
+
const a = document.createElement("a");
|
|
4981
|
+
a.href = url;
|
|
4982
|
+
a.download = filename;
|
|
4983
|
+
a.click();
|
|
4984
|
+
URL.revokeObjectURL(url);
|
|
4985
|
+
}
|
|
4986
|
+
async function copyCanvasToClipboard(canvas) {
|
|
4987
|
+
const blob = await canvasToBlob(canvas, "image/png");
|
|
4988
|
+
if (!blob) throw new Error("Failed to render image");
|
|
4989
|
+
const CI = globalThis.ClipboardItem;
|
|
4990
|
+
if (!navigator.clipboard?.write || !CI) throw new Error("Clipboard image write is not supported here");
|
|
4991
|
+
await navigator.clipboard.write([new CI({ "image/png": blob })]);
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4972
4994
|
// src/scales/scale.ts
|
|
4973
4995
|
var LinearScale = class {
|
|
4974
4996
|
constructor(domain = [0, 1]) {
|
|
@@ -5155,18 +5177,27 @@ var OrdinalTimeScale = class {
|
|
|
5155
5177
|
const i0 = Math.max(0, Math.ceil(this.domain[0] - 1e-9));
|
|
5156
5178
|
const i1 = Math.min(n - 1, Math.floor(this.domain[1] + 1e-9));
|
|
5157
5179
|
if (i1 < i0) return [];
|
|
5180
|
+
const DAY2 = 864e5;
|
|
5158
5181
|
const dayKey = (d) => d.getFullYear() * 1e4 + d.getMonth() * 100 + d.getDate();
|
|
5182
|
+
const weekKey = (d) => {
|
|
5183
|
+
const m = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
5184
|
+
const dow = (m.getDay() + 6) % 7;
|
|
5185
|
+
return Math.floor((m.getTime() - dow * DAY2) / DAY2);
|
|
5186
|
+
};
|
|
5187
|
+
const dayMonth = (d) => `${d.getDate()} ${MONTHS[d.getMonth()]}`;
|
|
5188
|
+
const monthLabel = (d) => d.getMonth() === 0 ? `${d.getFullYear()}` : MONTHS[d.getMonth()];
|
|
5159
5189
|
const levels = [
|
|
5160
5190
|
{ key: (d) => d.getFullYear(), label: (d) => `${d.getFullYear()}` },
|
|
5161
|
-
{
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
},
|
|
5165
|
-
{ key:
|
|
5166
|
-
{ key: (d) => dayKey(d) *
|
|
5167
|
-
{ key: (d) => (dayKey(d) *
|
|
5191
|
+
{ key: (d) => d.getFullYear() * 4 + Math.floor(d.getMonth() / 3), label: monthLabel },
|
|
5192
|
+
{ key: (d) => d.getFullYear() * 12 + d.getMonth(), label: monthLabel },
|
|
5193
|
+
{ key: weekKey, label: dayMonth },
|
|
5194
|
+
{ key: dayKey, label: dayMonth },
|
|
5195
|
+
{ key: (d) => dayKey(d) * 4 + Math.floor(d.getHours() / 6), label: hhmm },
|
|
5196
|
+
{ key: (d) => dayKey(d) * 24 + d.getHours(), label: hhmm },
|
|
5197
|
+
{ key: (d) => (dayKey(d) * 24 + d.getHours()) * 4 + Math.floor(d.getMinutes() / 15), label: hhmm },
|
|
5198
|
+
{ key: (d) => (dayKey(d) * 24 + d.getHours()) * 60 + d.getMinutes(), label: hhmm }
|
|
5168
5199
|
];
|
|
5169
|
-
const
|
|
5200
|
+
const periodStarts = (lvl) => {
|
|
5170
5201
|
const out = [];
|
|
5171
5202
|
let prev = i0 > 0 ? lvl.key(new Date(this.times[i0 - 1])) : NaN;
|
|
5172
5203
|
for (let i = i0; i <= i1; i++) {
|
|
@@ -5178,29 +5209,30 @@ var OrdinalTimeScale = class {
|
|
|
5178
5209
|
}
|
|
5179
5210
|
return out;
|
|
5180
5211
|
};
|
|
5181
|
-
let
|
|
5212
|
+
let chosen = null;
|
|
5213
|
+
let bestScore = Infinity;
|
|
5182
5214
|
for (const lvl of levels) {
|
|
5183
|
-
const b =
|
|
5215
|
+
const b = periodStarts(lvl);
|
|
5184
5216
|
if (b.length === 0) continue;
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
idxs = best.b;
|
|
5192
|
-
label = best.lvl.label;
|
|
5193
|
-
if (idxs.length > target * 1.5) {
|
|
5194
|
-
const stride = Math.ceil(idxs.length / target);
|
|
5195
|
-
idxs = idxs.filter((_, k) => k % stride === 0);
|
|
5217
|
+
const over = b.length > target * 1.5 ? (b.length - target * 1.5) * 3 : 0;
|
|
5218
|
+
const sparse = b.length < 2 ? 100 : 0;
|
|
5219
|
+
const score = Math.abs(b.length - target) + over + sparse;
|
|
5220
|
+
if (score < bestScore) {
|
|
5221
|
+
bestScore = score;
|
|
5222
|
+
chosen = { lvl, b };
|
|
5196
5223
|
}
|
|
5197
|
-
}
|
|
5198
|
-
|
|
5224
|
+
}
|
|
5225
|
+
if (!chosen) {
|
|
5226
|
+
const out = [];
|
|
5199
5227
|
const step = Math.max(1, Math.floor((i1 - i0) / Math.max(1, target - 1)));
|
|
5200
|
-
for (let i = i0; i <= i1; i += step)
|
|
5201
|
-
|
|
5228
|
+
for (let i = i0; i <= i1; i += step) {
|
|
5229
|
+
const d = new Date(this.times[i]);
|
|
5230
|
+
out.push({ value: i, label: `${dayMonth(d)} ${hhmm(d)}` });
|
|
5231
|
+
}
|
|
5232
|
+
return out;
|
|
5202
5233
|
}
|
|
5203
|
-
|
|
5234
|
+
const c = chosen;
|
|
5235
|
+
return c.b.map((i) => ({ value: i, label: c.lvl.label(new Date(this.times[i])) }));
|
|
5204
5236
|
}
|
|
5205
5237
|
formatTick(value) {
|
|
5206
5238
|
const d = new Date(this.timeAt(value));
|
|
@@ -5249,7 +5281,14 @@ var ICONS = {
|
|
|
5249
5281
|
pan: `<path d="M8 1.5v13M1.5 8h13M8 1.5 6 3.5M8 1.5l2 2M8 14.5l-2-2M8 14.5l2-2M1.5 8l2-2M1.5 8l2 2M14.5 8l-2-2M14.5 8l-2 2" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
5250
5282
|
box: `<rect x="2.5" y="2.5" width="11" height="11" rx="1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 2"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/>`,
|
|
5251
5283
|
boxX: `<path d="M1.5 8h13M3.5 5.5 1.5 8l2 2.5M12.5 5.5 14.5 8l-2 2.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="4.5" y="3" width="7" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 1.8"/>`,
|
|
5252
|
-
boxY: `<path d="M8 1.5v13M5.5 3.5 8 1.5l2.5 2M5.5 12.5 8 14.5l2.5-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="3" y="4.5" width="10" height="7" rx="1" fill="none" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 1.8"
|
|
5284
|
+
boxY: `<path d="M8 1.5v13M5.5 3.5 8 1.5l2.5 2M5.5 12.5 8 14.5l2.5-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="3" y="4.5" width="10" height="7" rx="1" fill="none" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 1.8"/>`,
|
|
5285
|
+
download: `<path d="M8 1.8v8.4M4.8 7l3.2 3.2L11.2 7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 11.5v1.7A1 1 0 0 0 3.5 14.2h9a1 1 0 0 0 1-1V11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
5286
|
+
trendline: `<path d="M2 13 14 3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="2" cy="13" r="1.6" fill="currentColor"/><circle cx="14" cy="3" r="1.6" fill="currentColor"/>`,
|
|
5287
|
+
hline: `<path d="M1.5 8h13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="1.6" fill="currentColor"/>`,
|
|
5288
|
+
ray: `<path d="M2 12 13 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M13 4 10 4.4M13 4l-.4 3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="2" cy="12" r="1.6" fill="currentColor"/>`,
|
|
5289
|
+
fib: `<path d="M2 3h12M2 6.2h12M2 9.4h12M2 12.6h12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>`,
|
|
5290
|
+
rect: `<rect x="2.5" y="3.5" width="11" height="9" rx="1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 2"/>`,
|
|
5291
|
+
trash: `<path d="M3 4.5h10M6.5 4.5V3h3v1.5M4.5 4.5l.6 8.5a1 1 0 0 0 1 .9h3.8a1 1 0 0 0 1-.9l.6-8.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
5253
5292
|
};
|
|
5254
5293
|
function createToolbar(container, host, dark) {
|
|
5255
5294
|
const t = dark ? darkToolbar : lightToolbar;
|
|
@@ -5275,6 +5314,9 @@ function createToolbar(container, host, dark) {
|
|
|
5275
5314
|
{ key: "box-x", title: "Zoom X only", icon: ICONS.boxX, mode: "box-x" },
|
|
5276
5315
|
{ key: "box-y", title: "Zoom Y only", icon: ICONS.boxY, mode: "box-y" }
|
|
5277
5316
|
];
|
|
5317
|
+
if (host.download) {
|
|
5318
|
+
buttons.push({ key: "download", title: "Download PNG", icon: ICONS.download, action: () => host.download() });
|
|
5319
|
+
}
|
|
5278
5320
|
const modeButtons = /* @__PURE__ */ new Map();
|
|
5279
5321
|
for (const b of buttons) {
|
|
5280
5322
|
const btn = document.createElement("button");
|
|
@@ -5319,6 +5361,67 @@ function createToolbar(container, host, dark) {
|
|
|
5319
5361
|
};
|
|
5320
5362
|
setActive(host.getMode());
|
|
5321
5363
|
host.onModeChange(setActive);
|
|
5364
|
+
if (host.drawTools) {
|
|
5365
|
+
const dt = host.drawTools;
|
|
5366
|
+
const mkBtn = (icon, title) => {
|
|
5367
|
+
const btn = document.createElement("button");
|
|
5368
|
+
btn.type = "button";
|
|
5369
|
+
btn.title = title;
|
|
5370
|
+
btn.setAttribute("aria-label", title);
|
|
5371
|
+
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">${icon}</svg>`;
|
|
5372
|
+
Object.assign(btn.style, {
|
|
5373
|
+
display: "inline-flex",
|
|
5374
|
+
alignItems: "center",
|
|
5375
|
+
justifyContent: "center",
|
|
5376
|
+
width: "26px",
|
|
5377
|
+
height: "26px",
|
|
5378
|
+
padding: "0",
|
|
5379
|
+
border: "none",
|
|
5380
|
+
borderRadius: "6px",
|
|
5381
|
+
background: "transparent",
|
|
5382
|
+
color: t.fg,
|
|
5383
|
+
cursor: "pointer",
|
|
5384
|
+
transition: "background 0.12s, color 0.12s"
|
|
5385
|
+
});
|
|
5386
|
+
btn.addEventListener("mouseenter", () => {
|
|
5387
|
+
if (btn.dataset.active !== "true") btn.style.background = t.hoverBg;
|
|
5388
|
+
});
|
|
5389
|
+
btn.addEventListener("mouseleave", () => {
|
|
5390
|
+
if (btn.dataset.active !== "true") btn.style.background = "transparent";
|
|
5391
|
+
});
|
|
5392
|
+
return btn;
|
|
5393
|
+
};
|
|
5394
|
+
const sep = document.createElement("div");
|
|
5395
|
+
Object.assign(sep.style, { width: "1px", height: "18px", background: t.border, margin: "0 3px", alignSelf: "center" });
|
|
5396
|
+
bar.appendChild(sep);
|
|
5397
|
+
const drawList = [
|
|
5398
|
+
{ key: "trendline", title: "Trendline", icon: ICONS.trendline },
|
|
5399
|
+
{ key: "hline", title: "Horizontal line", icon: ICONS.hline },
|
|
5400
|
+
{ key: "ray", title: "Ray", icon: ICONS.ray },
|
|
5401
|
+
{ key: "fib", title: "Fibonacci retracement", icon: ICONS.fib },
|
|
5402
|
+
{ key: "rect", title: "Rectangle", icon: ICONS.rect }
|
|
5403
|
+
];
|
|
5404
|
+
const drawBtns = /* @__PURE__ */ new Map();
|
|
5405
|
+
for (const d of drawList) {
|
|
5406
|
+
const btn = mkBtn(d.icon, d.title);
|
|
5407
|
+
btn.addEventListener("click", () => dt.set(dt.get() === d.key ? null : d.key));
|
|
5408
|
+
drawBtns.set(d.key, btn);
|
|
5409
|
+
bar.appendChild(btn);
|
|
5410
|
+
}
|
|
5411
|
+
const clearBtn = mkBtn(ICONS.trash, "Clear drawings");
|
|
5412
|
+
clearBtn.addEventListener("click", () => dt.clear());
|
|
5413
|
+
bar.appendChild(clearBtn);
|
|
5414
|
+
const setDrawActive = (tool) => {
|
|
5415
|
+
for (const [k, b] of drawBtns) {
|
|
5416
|
+
const active = k === tool;
|
|
5417
|
+
b.dataset.active = String(active);
|
|
5418
|
+
b.style.background = active ? t.activeBg : "transparent";
|
|
5419
|
+
b.style.color = active ? t.activeFg : t.fg;
|
|
5420
|
+
}
|
|
5421
|
+
};
|
|
5422
|
+
setDrawActive(dt.get());
|
|
5423
|
+
dt.onChange(setDrawActive);
|
|
5424
|
+
}
|
|
5322
5425
|
container.appendChild(bar);
|
|
5323
5426
|
return {
|
|
5324
5427
|
element: bar,
|
|
@@ -5330,6 +5433,18 @@ function createToolbar(container, host, dark) {
|
|
|
5330
5433
|
var DEFAULT_MARGIN = { top: 16, right: 16, bottom: 40, left: 56 };
|
|
5331
5434
|
var Y_AXIS_GAP = 52;
|
|
5332
5435
|
var TITLE_RESERVE = 28;
|
|
5436
|
+
function hexToRgba(hex, alpha) {
|
|
5437
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
5438
|
+
if (!m) return hex;
|
|
5439
|
+
const n = parseInt(m[1], 16);
|
|
5440
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
|
|
5441
|
+
}
|
|
5442
|
+
function segDist(px, py, ax, ay, bx, by) {
|
|
5443
|
+
const dx = bx - ax, dy = by - ay;
|
|
5444
|
+
const len2 = dx * dx + dy * dy;
|
|
5445
|
+
const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2));
|
|
5446
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
5447
|
+
}
|
|
5333
5448
|
function isPickable(layer) {
|
|
5334
5449
|
return typeof layer.pick === "function";
|
|
5335
5450
|
}
|
|
@@ -5374,6 +5489,15 @@ var Plot = class {
|
|
|
5374
5489
|
this.lastEmittedX = null;
|
|
5375
5490
|
this.lastEmittedCursor = NaN;
|
|
5376
5491
|
this.linkedCursorX = null;
|
|
5492
|
+
// Interactive drawing tools (see setDrawTool / drawingTools option).
|
|
5493
|
+
this.drawTool = null;
|
|
5494
|
+
this.drawings = [];
|
|
5495
|
+
this.pendingDrawing = null;
|
|
5496
|
+
this.drawToolCbs = [];
|
|
5497
|
+
// Selection / editing of existing drawings.
|
|
5498
|
+
this.hoverDrawing = -1;
|
|
5499
|
+
this.selectedDrawing = -1;
|
|
5500
|
+
this.drawMenu = null;
|
|
5377
5501
|
/** A point clicked to pin its details, until another click clears it. */
|
|
5378
5502
|
this.selected = null;
|
|
5379
5503
|
this.annotations = [];
|
|
@@ -5381,6 +5505,8 @@ var Plot = class {
|
|
|
5381
5505
|
if (getComputedStyle(container).position === "static") {
|
|
5382
5506
|
container.style.position = "relative";
|
|
5383
5507
|
}
|
|
5508
|
+
this.ariaLabel = options.ariaLabel;
|
|
5509
|
+
container.setAttribute("role", "img");
|
|
5384
5510
|
this.gridCanvas = this.makeCanvas(0);
|
|
5385
5511
|
this.dataCanvas = this.makeCanvas(1);
|
|
5386
5512
|
this.axisCanvas = this.makeCanvas(2);
|
|
@@ -5413,6 +5539,7 @@ var Plot = class {
|
|
|
5413
5539
|
this.bgFill = options.background;
|
|
5414
5540
|
this.borderFill = options.border;
|
|
5415
5541
|
this.title = typeof options.title === "string" ? { text: options.title } : options.title ?? null;
|
|
5542
|
+
this.updateAria();
|
|
5416
5543
|
this.legend = options.legend === true ? {} : options.legend || null;
|
|
5417
5544
|
this.mode = options.mode ?? "pan";
|
|
5418
5545
|
this.hoverEnabled = options.hover !== false;
|
|
@@ -5493,7 +5620,14 @@ var Plot = class {
|
|
|
5493
5620
|
setMode: (m) => this.setMode(m),
|
|
5494
5621
|
getMode: () => this.mode,
|
|
5495
5622
|
home: () => this.home(),
|
|
5496
|
-
onModeChange: (cb) => this.modeChangeCbs.push(cb)
|
|
5623
|
+
onModeChange: (cb) => this.modeChangeCbs.push(cb),
|
|
5624
|
+
download: () => void this.downloadImage(),
|
|
5625
|
+
drawTools: options.drawingTools ? {
|
|
5626
|
+
set: (t) => this.setDrawTool(t),
|
|
5627
|
+
get: () => this.drawTool,
|
|
5628
|
+
clear: () => this.clearDrawings(),
|
|
5629
|
+
onChange: (cb) => this.onDrawToolChange((t) => cb(t))
|
|
5630
|
+
} : void 0
|
|
5497
5631
|
},
|
|
5498
5632
|
this.isDark
|
|
5499
5633
|
);
|
|
@@ -5561,9 +5695,28 @@ var Plot = class {
|
|
|
5561
5695
|
}
|
|
5562
5696
|
this.layers.push(layer);
|
|
5563
5697
|
this.autoscale();
|
|
5698
|
+
this.updateAria();
|
|
5564
5699
|
this.requestRender();
|
|
5565
5700
|
return layer;
|
|
5566
5701
|
}
|
|
5702
|
+
// --- Accessibility --------------------------------------------------------
|
|
5703
|
+
/** Set the chart's accessible label (`aria-label`). Pass `undefined` to auto-summarize. */
|
|
5704
|
+
setAriaLabel(text) {
|
|
5705
|
+
this.ariaLabel = text;
|
|
5706
|
+
this.updateAria();
|
|
5707
|
+
}
|
|
5708
|
+
/** A one-line text summary of the chart (title + series), for a11y / tooltips. */
|
|
5709
|
+
describe() {
|
|
5710
|
+
const names = this.legendEntries().map((e) => e.name).filter((n) => !/^(line|scatter|bar|area)-\d+$/.test(n));
|
|
5711
|
+
const title = this.title?.text ? `${this.title.text}. ` : "";
|
|
5712
|
+
const nl = this.layers.length;
|
|
5713
|
+
if (nl === 0) return `${title}Empty chart`.trim();
|
|
5714
|
+
const series = names.length ? ` \u2014 ${names.join(", ")}` : "";
|
|
5715
|
+
return `${title}Chart with ${nl} series${series}`.trim();
|
|
5716
|
+
}
|
|
5717
|
+
updateAria() {
|
|
5718
|
+
this.container.setAttribute("aria-label", this.ariaLabel ?? this.describe());
|
|
5719
|
+
}
|
|
5567
5720
|
/**
|
|
5568
5721
|
* Register a layer built outside core (e.g. `@photonviz/map`). Use with
|
|
5569
5722
|
* {@link context} to construct the layer against this plot's WebGL2 context.
|
|
@@ -5770,6 +5923,275 @@ var Plot = class {
|
|
|
5770
5923
|
this.annotations = [];
|
|
5771
5924
|
this.requestRender();
|
|
5772
5925
|
}
|
|
5926
|
+
// --- Interactive drawing ---------------------------------------------------
|
|
5927
|
+
/** Activate a drawing tool (click-drag on the plot to place), or `null` to stop drawing. */
|
|
5928
|
+
setDrawTool(tool) {
|
|
5929
|
+
if (this.drawTool === tool) return;
|
|
5930
|
+
this.drawTool = tool;
|
|
5931
|
+
this.pendingDrawing = null;
|
|
5932
|
+
this.updateCursor();
|
|
5933
|
+
for (const cb of this.drawToolCbs) cb(tool);
|
|
5934
|
+
this.requestRender();
|
|
5935
|
+
}
|
|
5936
|
+
/** The active drawing tool, or `null`. */
|
|
5937
|
+
getDrawTool() {
|
|
5938
|
+
return this.drawTool;
|
|
5939
|
+
}
|
|
5940
|
+
/** Subscribe to draw-tool changes (used by the toolbar). Returns an unsubscribe fn. */
|
|
5941
|
+
onDrawToolChange(cb) {
|
|
5942
|
+
this.drawToolCbs.push(cb);
|
|
5943
|
+
return () => {
|
|
5944
|
+
this.drawToolCbs = this.drawToolCbs.filter((f) => f !== cb);
|
|
5945
|
+
};
|
|
5946
|
+
}
|
|
5947
|
+
/** Add a drawing programmatically (same shapes the tools produce). */
|
|
5948
|
+
addDrawing(a) {
|
|
5949
|
+
this.drawings.push(a);
|
|
5950
|
+
this.requestRender();
|
|
5951
|
+
}
|
|
5952
|
+
/** All user drawings (trendlines, fib levels, …). */
|
|
5953
|
+
getDrawings() {
|
|
5954
|
+
return [...this.drawings];
|
|
5955
|
+
}
|
|
5956
|
+
/** Remove every user drawing. */
|
|
5957
|
+
clearDrawings() {
|
|
5958
|
+
if (this.drawings.length === 0 && !this.pendingDrawing) return;
|
|
5959
|
+
this.drawings = [];
|
|
5960
|
+
this.pendingDrawing = null;
|
|
5961
|
+
this.requestRender();
|
|
5962
|
+
}
|
|
5963
|
+
/** Canvas-space pixel → data coordinates on the x + primary y scale. */
|
|
5964
|
+
dataAtPx(px, py) {
|
|
5965
|
+
const region = plotRegion(this.layout());
|
|
5966
|
+
const nx = (px - region.left) / region.width;
|
|
5967
|
+
const ny = 1 - (py - region.top) / region.height;
|
|
5968
|
+
return { x: this.scaleX.invert(nx), y: this.primaryY().scale.invert(ny) };
|
|
5969
|
+
}
|
|
5970
|
+
/** Build the annotation for a draw tool from its start + current data points. */
|
|
5971
|
+
buildDrawing(tool, s, c) {
|
|
5972
|
+
const col = "#f59e0b";
|
|
5973
|
+
switch (tool) {
|
|
5974
|
+
case "trendline":
|
|
5975
|
+
return { type: "line", x0: s.x, y0: s.y, x1: c.x, y1: c.y, color: col, width: 1.5 };
|
|
5976
|
+
case "ray":
|
|
5977
|
+
return { type: "ray", x0: s.x, y0: s.y, x1: c.x, y1: c.y, color: col, width: 1.5 };
|
|
5978
|
+
case "hline":
|
|
5979
|
+
return { type: "span", dim: "y", value: c.y, color: col, width: 1.5 };
|
|
5980
|
+
case "rect":
|
|
5981
|
+
return { type: "box", x: [s.x, c.x], y: [s.y, c.y], color: "rgba(245,158,11,0.08)", border: col };
|
|
5982
|
+
case "fib":
|
|
5983
|
+
return { type: "fib", x0: s.x, x1: c.x, high: Math.max(s.y, c.y), low: Math.min(s.y, c.y), fill: true, color: "#94a3b8" };
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
drawingYScale(a) {
|
|
5987
|
+
const id = "yAxis" in a ? a.yAxis : void 0;
|
|
5988
|
+
return (this.yAxes.get(id ?? "y") ?? this.primaryY()).scale;
|
|
5989
|
+
}
|
|
5990
|
+
/** Editable handle points (data space) for a drawing; `[]` if it has none. */
|
|
5991
|
+
drawingHandlePts(a) {
|
|
5992
|
+
switch (a.type) {
|
|
5993
|
+
case "line":
|
|
5994
|
+
case "ray":
|
|
5995
|
+
return [{ x: a.x0, y: a.y0 }, { x: a.x1, y: a.y1 }];
|
|
5996
|
+
case "fib":
|
|
5997
|
+
return [{ x: a.x0, y: a.high }, { x: a.x1, y: a.low }];
|
|
5998
|
+
case "box":
|
|
5999
|
+
return [{ x: a.x[0], y: a.y[0] }, { x: a.x[1], y: a.y[1] }];
|
|
6000
|
+
case "span":
|
|
6001
|
+
return a.dim === "y" ? [{ x: (this.scaleX.domain[0] + this.scaleX.domain[1]) / 2, y: a.value }] : [{ x: a.value, y: (this.primaryY().scale.domain[0] + this.primaryY().scale.domain[1]) / 2 }];
|
|
6002
|
+
default:
|
|
6003
|
+
return [];
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
/** Move handle `i` of a drawing to a new data-space point (all editable types). */
|
|
6007
|
+
setDrawingHandle(a, i, x, y) {
|
|
6008
|
+
switch (a.type) {
|
|
6009
|
+
case "line":
|
|
6010
|
+
case "ray":
|
|
6011
|
+
if (i === 0) {
|
|
6012
|
+
a.x0 = x;
|
|
6013
|
+
a.y0 = y;
|
|
6014
|
+
} else {
|
|
6015
|
+
a.x1 = x;
|
|
6016
|
+
a.y1 = y;
|
|
6017
|
+
}
|
|
6018
|
+
break;
|
|
6019
|
+
case "fib":
|
|
6020
|
+
if (i === 0) {
|
|
6021
|
+
a.x0 = x;
|
|
6022
|
+
a.high = y;
|
|
6023
|
+
} else {
|
|
6024
|
+
a.x1 = x;
|
|
6025
|
+
a.low = y;
|
|
6026
|
+
}
|
|
6027
|
+
break;
|
|
6028
|
+
case "box": {
|
|
6029
|
+
const m = a;
|
|
6030
|
+
if (i === 0) {
|
|
6031
|
+
m.x = [x, a.x[1]];
|
|
6032
|
+
m.y = [y, a.y[1]];
|
|
6033
|
+
} else {
|
|
6034
|
+
m.x = [a.x[0], x];
|
|
6035
|
+
m.y = [a.y[0], y];
|
|
6036
|
+
}
|
|
6037
|
+
break;
|
|
6038
|
+
}
|
|
6039
|
+
case "span":
|
|
6040
|
+
a.value = a.dim === "y" ? y : x;
|
|
6041
|
+
break;
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
/** Translate a whole drawing by a data-space delta (drag the body). */
|
|
6045
|
+
translateDrawing(a, dx, dy) {
|
|
6046
|
+
const pts = this.drawingHandlePts(a);
|
|
6047
|
+
for (let i = 0; i < pts.length; i++) this.setDrawingHandle(a, i, pts[i].x + dx, pts[i].y + dy);
|
|
6048
|
+
}
|
|
6049
|
+
/** Cursor→drawing distance in px (segment for line/ray, edges for box/fib, the line for span). */
|
|
6050
|
+
drawingBodyDist(a, px, py, region) {
|
|
6051
|
+
const s = this.drawingYScale(a);
|
|
6052
|
+
const X = (x) => pxX(region, this.scaleX.norm(x));
|
|
6053
|
+
const Y = (y) => pxY(region, s.norm(y));
|
|
6054
|
+
if (a.type === "line" || a.type === "ray") return segDist(px, py, X(a.x0), Y(a.y0), X(a.x1), Y(a.y1));
|
|
6055
|
+
if (a.type === "span") return a.dim === "y" ? Math.abs(py - Y(a.value)) : Math.abs(px - X(a.value));
|
|
6056
|
+
if (a.type === "box" || a.type === "fib") {
|
|
6057
|
+
const [x0, x1] = a.type === "box" ? [X(a.x[0]), X(a.x[1])] : [X(a.x0), X(a.x1)];
|
|
6058
|
+
const [y0, y1] = a.type === "box" ? [Y(a.y[0]), Y(a.y[1])] : [Y(a.high), Y(a.low)];
|
|
6059
|
+
const inX = px >= Math.min(x0, x1) - 4 && px <= Math.max(x0, x1) + 4;
|
|
6060
|
+
const inY = py >= Math.min(y0, y1) - 4 && py <= Math.max(y0, y1) + 4;
|
|
6061
|
+
if (inX && inY) return 0;
|
|
6062
|
+
}
|
|
6063
|
+
return Infinity;
|
|
6064
|
+
}
|
|
6065
|
+
/** Topmost drawing under the cursor and its handle (handle −1 = body, index −1 = none). */
|
|
6066
|
+
hitDrawing(px, py) {
|
|
6067
|
+
if (this.drawings.length === 0) return { index: -1, handle: -1 };
|
|
6068
|
+
const region = plotRegion(this.layout());
|
|
6069
|
+
for (let di = this.drawings.length - 1; di >= 0; di--) {
|
|
6070
|
+
const a = this.drawings[di];
|
|
6071
|
+
const s = this.drawingYScale(a);
|
|
6072
|
+
const pts = this.drawingHandlePts(a);
|
|
6073
|
+
for (let hi = 0; hi < pts.length; hi++) {
|
|
6074
|
+
const hx = pxX(region, this.scaleX.norm(pts[hi].x)), hy = pxY(region, s.norm(pts[hi].y));
|
|
6075
|
+
if (Math.hypot(hx - px, hy - py) <= 8) return { index: di, handle: hi };
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
for (let di = this.drawings.length - 1; di >= 0; di--) {
|
|
6079
|
+
if (this.drawingBodyDist(this.drawings[di], px, py, region) <= 6) return { index: di, handle: -1 };
|
|
6080
|
+
}
|
|
6081
|
+
return { index: -1, handle: -1 };
|
|
6082
|
+
}
|
|
6083
|
+
removeDrawing(index) {
|
|
6084
|
+
if (index < 0 || index >= this.drawings.length) return;
|
|
6085
|
+
this.drawings.splice(index, 1);
|
|
6086
|
+
if (this.selectedDrawing === index) this.selectedDrawing = -1;
|
|
6087
|
+
else if (this.selectedDrawing > index) this.selectedDrawing--;
|
|
6088
|
+
this.hoverDrawing = -1;
|
|
6089
|
+
this.hideDrawMenu();
|
|
6090
|
+
this.requestRender();
|
|
6091
|
+
}
|
|
6092
|
+
hideDrawMenu() {
|
|
6093
|
+
if (this.drawMenu) {
|
|
6094
|
+
this.drawMenu.remove();
|
|
6095
|
+
this.drawMenu = null;
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
/** Right-click context menu for a drawing: rename, recolor, delete. */
|
|
6099
|
+
showDrawMenu(clientX, clientY, index) {
|
|
6100
|
+
this.hideDrawMenu();
|
|
6101
|
+
const a = this.drawings[index];
|
|
6102
|
+
if (!a) return;
|
|
6103
|
+
this.selectedDrawing = index;
|
|
6104
|
+
const menu = document.createElement("div");
|
|
6105
|
+
Object.assign(menu.style, {
|
|
6106
|
+
position: "fixed",
|
|
6107
|
+
left: `${clientX}px`,
|
|
6108
|
+
top: `${clientY}px`,
|
|
6109
|
+
zIndex: "1000",
|
|
6110
|
+
minWidth: "150px",
|
|
6111
|
+
padding: "4px",
|
|
6112
|
+
borderRadius: "8px",
|
|
6113
|
+
background: this.isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.99)",
|
|
6114
|
+
color: this.isDark ? "#e2e8f0" : "#1e293b",
|
|
6115
|
+
border: `1px solid ${this.isDark ? "rgba(148,163,184,0.3)" : "rgba(100,116,139,0.3)"}`,
|
|
6116
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.35)",
|
|
6117
|
+
font: "13px system-ui, sans-serif",
|
|
6118
|
+
userSelect: "none"
|
|
6119
|
+
});
|
|
6120
|
+
const item = (label, onClick) => {
|
|
6121
|
+
const row = document.createElement("div");
|
|
6122
|
+
row.textContent = label;
|
|
6123
|
+
Object.assign(row.style, { padding: "6px 10px", borderRadius: "5px", cursor: "pointer" });
|
|
6124
|
+
row.addEventListener("mouseenter", () => {
|
|
6125
|
+
row.style.background = this.isDark ? "rgba(148,163,184,0.15)" : "rgba(100,116,139,0.12)";
|
|
6126
|
+
});
|
|
6127
|
+
row.addEventListener("mouseleave", () => {
|
|
6128
|
+
row.style.background = "transparent";
|
|
6129
|
+
});
|
|
6130
|
+
row.addEventListener("mousedown", (e) => {
|
|
6131
|
+
e.preventDefault();
|
|
6132
|
+
e.stopPropagation();
|
|
6133
|
+
onClick();
|
|
6134
|
+
});
|
|
6135
|
+
return row;
|
|
6136
|
+
};
|
|
6137
|
+
menu.appendChild(item("\u270E Rename\u2026", () => {
|
|
6138
|
+
this.hideDrawMenu();
|
|
6139
|
+
this.renameDrawing(index);
|
|
6140
|
+
}));
|
|
6141
|
+
const colorRow = document.createElement("div");
|
|
6142
|
+
Object.assign(colorRow.style, { display: "flex", gap: "6px", padding: "6px 10px" });
|
|
6143
|
+
for (const c of ["#f59e0b", "#60a5fa", "#34d399", "#f472b6", "#e2e8f0"]) {
|
|
6144
|
+
const sw = document.createElement("span");
|
|
6145
|
+
Object.assign(sw.style, { width: "16px", height: "16px", borderRadius: "4px", background: c, cursor: "pointer", border: "1px solid rgba(0,0,0,0.2)" });
|
|
6146
|
+
sw.addEventListener("mousedown", (e) => {
|
|
6147
|
+
e.preventDefault();
|
|
6148
|
+
e.stopPropagation();
|
|
6149
|
+
this.recolorDrawing(a, c);
|
|
6150
|
+
this.hideDrawMenu();
|
|
6151
|
+
this.requestRender();
|
|
6152
|
+
});
|
|
6153
|
+
colorRow.appendChild(sw);
|
|
6154
|
+
}
|
|
6155
|
+
menu.appendChild(colorRow);
|
|
6156
|
+
const del = item("\u{1F5D1} Delete", () => this.removeDrawing(index));
|
|
6157
|
+
del.style.color = "#f87171";
|
|
6158
|
+
menu.appendChild(del);
|
|
6159
|
+
(document.fullscreenElement ?? document.body).appendChild(menu);
|
|
6160
|
+
this.drawMenu = menu;
|
|
6161
|
+
const dismiss = (ev) => {
|
|
6162
|
+
if (ev && ev.type === "pointerdown" && ev.target instanceof Node && menu.contains(ev.target)) return;
|
|
6163
|
+
window.removeEventListener("pointerdown", dismiss, true);
|
|
6164
|
+
window.removeEventListener("blur", dismiss);
|
|
6165
|
+
window.removeEventListener("resize", dismiss);
|
|
6166
|
+
document.removeEventListener("fullscreenchange", dismiss);
|
|
6167
|
+
document.removeEventListener("scroll", dismiss, true);
|
|
6168
|
+
this.hideDrawMenu();
|
|
6169
|
+
};
|
|
6170
|
+
setTimeout(() => window.addEventListener("pointerdown", dismiss, true), 0);
|
|
6171
|
+
window.addEventListener("blur", dismiss);
|
|
6172
|
+
window.addEventListener("resize", dismiss);
|
|
6173
|
+
document.addEventListener("fullscreenchange", dismiss);
|
|
6174
|
+
document.addEventListener("scroll", dismiss, true);
|
|
6175
|
+
this.requestRender();
|
|
6176
|
+
}
|
|
6177
|
+
/** Recolor a drawing: a box keeps its translucent fill + opaque border; others take the color directly. */
|
|
6178
|
+
recolorDrawing(a, color) {
|
|
6179
|
+
if (a.type === "box") {
|
|
6180
|
+
a.border = color;
|
|
6181
|
+
a.color = hexToRgba(color, 0.12);
|
|
6182
|
+
} else {
|
|
6183
|
+
a.color = color;
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
/** Prompt for a label on a drawing (double-click / context menu). */
|
|
6187
|
+
renameDrawing(index) {
|
|
6188
|
+
const a = this.drawings[index];
|
|
6189
|
+
if (!a || a.type !== "line" && a.type !== "ray" && a.type !== "box" && a.type !== "fib") return;
|
|
6190
|
+
const next = window.prompt("Label", a.label ?? "");
|
|
6191
|
+
if (next === null) return;
|
|
6192
|
+
a.label = next || void 0;
|
|
6193
|
+
this.requestRender();
|
|
6194
|
+
}
|
|
5773
6195
|
/** Compute an STFT of `signal` and render it as a heatmap (time × frequency). */
|
|
5774
6196
|
addHeatmapSpectrogram(signal, opts = {}) {
|
|
5775
6197
|
const s = spectrogram(signal, opts);
|
|
@@ -5921,6 +6343,45 @@ var Plot = class {
|
|
|
5921
6343
|
this.autoscale();
|
|
5922
6344
|
this.requestRender();
|
|
5923
6345
|
}
|
|
6346
|
+
// --- Export ---------------------------------------------------------------
|
|
6347
|
+
/**
|
|
6348
|
+
* Composite this plot's layers (grid → data → axes/labels) into a single 2D
|
|
6349
|
+
* canvas at device resolution. `background` fills behind the plot (default the
|
|
6350
|
+
* theme's page color; pass `"transparent"` to keep alpha).
|
|
6351
|
+
*/
|
|
6352
|
+
compositeCanvas(background) {
|
|
6353
|
+
this.render();
|
|
6354
|
+
const w = this.dataCanvas.width, h = this.dataCanvas.height;
|
|
6355
|
+
const out = document.createElement("canvas");
|
|
6356
|
+
out.width = w;
|
|
6357
|
+
out.height = h;
|
|
6358
|
+
const ctx = out.getContext("2d");
|
|
6359
|
+
const bg = background ?? (this.isDark ? "#0b1220" : "#ffffff");
|
|
6360
|
+
if (bg !== "transparent") {
|
|
6361
|
+
ctx.fillStyle = bg;
|
|
6362
|
+
ctx.fillRect(0, 0, w, h);
|
|
6363
|
+
}
|
|
6364
|
+
ctx.drawImage(this.gridCanvas, 0, 0);
|
|
6365
|
+
ctx.drawImage(this.dataCanvas, 0, 0);
|
|
6366
|
+
ctx.drawImage(this.axisCanvas, 0, 0);
|
|
6367
|
+
return out;
|
|
6368
|
+
}
|
|
6369
|
+
/** Export the current view as a data URL (default PNG). */
|
|
6370
|
+
toDataURL(type = "image/png", opts = {}) {
|
|
6371
|
+
return this.compositeCanvas(opts.background).toDataURL(type, opts.quality);
|
|
6372
|
+
}
|
|
6373
|
+
/** Export the current view as a `Blob` (default PNG). */
|
|
6374
|
+
toBlob(type = "image/png", opts = {}) {
|
|
6375
|
+
return canvasToBlob(this.compositeCanvas(opts.background), type, opts.quality);
|
|
6376
|
+
}
|
|
6377
|
+
/** Download the current view as an image (PNG by default). */
|
|
6378
|
+
downloadImage(filename = "chart.png", type = "image/png", opts = {}) {
|
|
6379
|
+
return downloadCanvas(this.compositeCanvas(opts.background), filename, type, opts.quality);
|
|
6380
|
+
}
|
|
6381
|
+
/** Copy the current view to the clipboard as a PNG. Throws if the browser can't. */
|
|
6382
|
+
copyToClipboard(opts = {}) {
|
|
6383
|
+
return copyCanvasToClipboard(this.compositeCanvas(opts.background));
|
|
6384
|
+
}
|
|
5924
6385
|
/** Re-fit auto axes to the data: x over all series, each y axis over its own series. */
|
|
5925
6386
|
autoscale() {
|
|
5926
6387
|
if (this.autoX) {
|
|
@@ -5951,6 +6412,7 @@ var Plot = class {
|
|
|
5951
6412
|
destroy() {
|
|
5952
6413
|
this.resizeObserver.disconnect();
|
|
5953
6414
|
this.toolbarHandle?.destroy();
|
|
6415
|
+
this.hideDrawMenu();
|
|
5954
6416
|
this.selectionDiv.remove();
|
|
5955
6417
|
this.tooltip.remove();
|
|
5956
6418
|
this.infoBox.remove();
|
|
@@ -6073,7 +6535,7 @@ var Plot = class {
|
|
|
6073
6535
|
});
|
|
6074
6536
|
}
|
|
6075
6537
|
if (this.title) drawTitle(this.axisCtx, region, this.title, this.isDark);
|
|
6076
|
-
if (this.annotations.length) this.renderAnnotations(region);
|
|
6538
|
+
if (this.annotations.length || this.drawings.length || this.pendingDrawing) this.renderAnnotations(region);
|
|
6077
6539
|
if (this.crosshair && this.pressPx) {
|
|
6078
6540
|
drawCrosshairXY(this.axisCtx, region, this.pressPx.x, this.pressPx.y, this.theme);
|
|
6079
6541
|
}
|
|
@@ -6161,7 +6623,8 @@ var Plot = class {
|
|
|
6161
6623
|
ctx.beginPath();
|
|
6162
6624
|
ctx.rect(left, top, right - left, bottom - top);
|
|
6163
6625
|
ctx.clip();
|
|
6164
|
-
|
|
6626
|
+
const items = this.pendingDrawing ? [...this.annotations, ...this.drawings, this.pendingDrawing] : this.drawings.length ? [...this.annotations, ...this.drawings] : this.annotations;
|
|
6627
|
+
for (const a of items) {
|
|
6165
6628
|
ctx.setLineDash([]);
|
|
6166
6629
|
if (a.type === "span") {
|
|
6167
6630
|
ctx.strokeStyle = a.color ?? this.theme.axis;
|
|
@@ -6203,13 +6666,91 @@ var Plot = class {
|
|
|
6203
6666
|
ctx.lineWidth = 1;
|
|
6204
6667
|
ctx.strokeRect(rx + 0.5, ry + 0.5, rw, rh);
|
|
6205
6668
|
}
|
|
6206
|
-
|
|
6669
|
+
if (a.label) {
|
|
6670
|
+
ctx.fillStyle = a.border ?? a.color ?? this.theme.text;
|
|
6671
|
+
ctx.font = this.theme.font;
|
|
6672
|
+
ctx.textAlign = "left";
|
|
6673
|
+
ctx.textBaseline = "bottom";
|
|
6674
|
+
ctx.fillText(a.label, rx + 4, ry - 3);
|
|
6675
|
+
}
|
|
6676
|
+
} else if (a.type === "label") {
|
|
6207
6677
|
const s = yScaleOf(a.yAxis);
|
|
6208
6678
|
ctx.fillStyle = a.color ?? this.theme.text;
|
|
6209
6679
|
ctx.font = a.font ?? this.theme.font;
|
|
6210
6680
|
ctx.textAlign = a.align ?? "left";
|
|
6211
6681
|
ctx.textBaseline = "middle";
|
|
6212
6682
|
ctx.fillText(a.text, px(a.x), py(s, a.y));
|
|
6683
|
+
} else if (a.type === "line" || a.type === "ray") {
|
|
6684
|
+
const s = yScaleOf(a.yAxis);
|
|
6685
|
+
ctx.strokeStyle = a.color ?? this.theme.axis;
|
|
6686
|
+
ctx.lineWidth = a.width ?? 1.5;
|
|
6687
|
+
if (a.dash) ctx.setLineDash(a.dash);
|
|
6688
|
+
const X0 = px(a.x0), Y0 = py(s, a.y0);
|
|
6689
|
+
let X1 = px(a.x1), Y1 = py(s, a.y1);
|
|
6690
|
+
if (a.type === "ray") {
|
|
6691
|
+
const dx = X1 - X0, dy = Y1 - Y0, len = Math.hypot(dx, dy) || 1, f = 8e3 / len;
|
|
6692
|
+
X1 = X0 + dx * f;
|
|
6693
|
+
Y1 = Y0 + dy * f;
|
|
6694
|
+
}
|
|
6695
|
+
ctx.beginPath();
|
|
6696
|
+
ctx.moveTo(X0, Y0);
|
|
6697
|
+
ctx.lineTo(X1, Y1);
|
|
6698
|
+
ctx.stroke();
|
|
6699
|
+
if (a.label) {
|
|
6700
|
+
ctx.setLineDash([]);
|
|
6701
|
+
ctx.fillStyle = a.color ?? this.theme.text;
|
|
6702
|
+
ctx.font = this.theme.font;
|
|
6703
|
+
ctx.textAlign = "left";
|
|
6704
|
+
ctx.textBaseline = "bottom";
|
|
6705
|
+
ctx.fillText(a.label, px(a.x1) + 7, py(s, a.y1) - 4);
|
|
6706
|
+
}
|
|
6707
|
+
} else {
|
|
6708
|
+
const s = yScaleOf(a.yAxis);
|
|
6709
|
+
const ratios = a.ratios ?? [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
6710
|
+
const span = a.high - a.low;
|
|
6711
|
+
const fx0 = Math.min(px(a.x0), px(a.x1)), fx1 = Math.max(px(a.x0), px(a.x1));
|
|
6712
|
+
const color = a.color ?? this.theme.axis;
|
|
6713
|
+
const ys = ratios.map((r) => py(s, a.high - span * r));
|
|
6714
|
+
if (a.fill) {
|
|
6715
|
+
for (let i = 0; i < ys.length - 1; i++) {
|
|
6716
|
+
ctx.fillStyle = `rgba(96,165,250,${i % 2 === 0 ? 0.06 : 0.12})`;
|
|
6717
|
+
ctx.fillRect(fx0, Math.min(ys[i], ys[i + 1]), fx1 - fx0, Math.abs(ys[i + 1] - ys[i]));
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
ctx.setLineDash([]);
|
|
6721
|
+
ctx.strokeStyle = color;
|
|
6722
|
+
ctx.lineWidth = 1;
|
|
6723
|
+
ctx.fillStyle = color;
|
|
6724
|
+
ctx.font = this.theme.font;
|
|
6725
|
+
ctx.textAlign = "left";
|
|
6726
|
+
ctx.textBaseline = "bottom";
|
|
6727
|
+
for (let i = 0; i < ratios.length; i++) {
|
|
6728
|
+
const y = Math.round(ys[i]) + 0.5;
|
|
6729
|
+
ctx.beginPath();
|
|
6730
|
+
ctx.moveTo(fx0, y);
|
|
6731
|
+
ctx.lineTo(fx1, y);
|
|
6732
|
+
ctx.stroke();
|
|
6733
|
+
ctx.fillText(`${(ratios[i] * 100).toFixed(1)}% \xB7 ${(a.high - span * ratios[i]).toFixed(2)}`, fx0 + 4, y - 2);
|
|
6734
|
+
}
|
|
6735
|
+
if (a.label) ctx.fillText(a.label, fx0 + 4, Math.min(...ys) - 4);
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
const active = this.selectedDrawing >= 0 ? this.selectedDrawing : this.hoverDrawing;
|
|
6739
|
+
if (active >= 0 && active < this.drawings.length) {
|
|
6740
|
+
const a = this.drawings[active];
|
|
6741
|
+
const s = yScaleOf(a.yAxis);
|
|
6742
|
+
const col = a.border ?? a.color ?? this.theme.axis;
|
|
6743
|
+
const selected = this.selectedDrawing === active;
|
|
6744
|
+
ctx.setLineDash([]);
|
|
6745
|
+
for (const pt of this.drawingHandlePts(a)) {
|
|
6746
|
+
const hx = px(pt.x), hy = py(s, pt.y);
|
|
6747
|
+
ctx.beginPath();
|
|
6748
|
+
ctx.arc(hx, hy, selected ? 5 : 4, 0, Math.PI * 2);
|
|
6749
|
+
ctx.fillStyle = col;
|
|
6750
|
+
ctx.fill();
|
|
6751
|
+
ctx.lineWidth = 1.6;
|
|
6752
|
+
ctx.strokeStyle = this.isDark ? "#0b1220" : "#ffffff";
|
|
6753
|
+
ctx.stroke();
|
|
6213
6754
|
}
|
|
6214
6755
|
}
|
|
6215
6756
|
ctx.restore();
|
|
@@ -6258,7 +6799,7 @@ var Plot = class {
|
|
|
6258
6799
|
tip.appendChild(el);
|
|
6259
6800
|
}
|
|
6260
6801
|
} else {
|
|
6261
|
-
const xfmt = this.axisX.config.format ??
|
|
6802
|
+
const xfmt = this.axisX.config.format ?? ((v) => this.scaleX.formatTick(v));
|
|
6262
6803
|
const header = document.createElement("div");
|
|
6263
6804
|
header.style.opacity = "0.7";
|
|
6264
6805
|
header.style.marginBottom = "3px";
|
|
@@ -6298,7 +6839,7 @@ var Plot = class {
|
|
|
6298
6839
|
}
|
|
6299
6840
|
// ---- Interaction ----------------------------------------------------------
|
|
6300
6841
|
updateCursor() {
|
|
6301
|
-
this.axisCanvas.style.cursor = this.mode === "pan" ? "grab" : "crosshair";
|
|
6842
|
+
this.axisCanvas.style.cursor = this.drawTool ? "crosshair" : this.mode === "pan" ? "grab" : "crosshair";
|
|
6302
6843
|
}
|
|
6303
6844
|
axisLock() {
|
|
6304
6845
|
if (this.mode === "box-x") return { x: true, y: false };
|
|
@@ -6349,6 +6890,7 @@ var Plot = class {
|
|
|
6349
6890
|
attachInteraction() {
|
|
6350
6891
|
const el = this.axisCanvas;
|
|
6351
6892
|
el.style.touchAction = "none";
|
|
6893
|
+
el.style.outline = "none";
|
|
6352
6894
|
el.addEventListener(
|
|
6353
6895
|
"wheel",
|
|
6354
6896
|
(e) => {
|
|
@@ -6364,6 +6906,10 @@ var Plot = class {
|
|
|
6364
6906
|
);
|
|
6365
6907
|
let panning = false;
|
|
6366
6908
|
let selecting = false;
|
|
6909
|
+
let drawing = false;
|
|
6910
|
+
let drawStart = { x: 0, y: 0 };
|
|
6911
|
+
let handleDrag = null;
|
|
6912
|
+
let bodyDrag = null;
|
|
6367
6913
|
let axisDrag = null;
|
|
6368
6914
|
let lastX = 0;
|
|
6369
6915
|
let lastY = 0;
|
|
@@ -6372,6 +6918,7 @@ var Plot = class {
|
|
|
6372
6918
|
let downX = 0;
|
|
6373
6919
|
let downY = 0;
|
|
6374
6920
|
el.addEventListener("pointerdown", (e) => {
|
|
6921
|
+
if (e.button !== 0) return;
|
|
6375
6922
|
el.setPointerCapture(e.pointerId);
|
|
6376
6923
|
this.hoverPx = null;
|
|
6377
6924
|
const rect = el.getBoundingClientRect();
|
|
@@ -6380,19 +6927,34 @@ var Plot = class {
|
|
|
6380
6927
|
downX = px;
|
|
6381
6928
|
downY = py;
|
|
6382
6929
|
const zone = this.zoneAt(px, py);
|
|
6930
|
+
const drawHit = !this.drawTool && zone.type === "plot" && this.drawings.length ? this.hitDrawing(px, py) : { index: -1, handle: -1 };
|
|
6383
6931
|
if (zone.type === "x") {
|
|
6384
6932
|
axisDrag = "x";
|
|
6385
6933
|
lastX = e.clientX;
|
|
6386
6934
|
} else if (zone.type === "y") {
|
|
6387
6935
|
axisDrag = { y: zone.id };
|
|
6388
6936
|
lastY = e.clientY;
|
|
6937
|
+
} else if (this.drawTool) {
|
|
6938
|
+
drawing = true;
|
|
6939
|
+
drawStart = this.dataAtPx(px, py);
|
|
6940
|
+
this.pendingDrawing = this.buildDrawing(this.drawTool, drawStart, drawStart);
|
|
6941
|
+
this.requestRender();
|
|
6942
|
+
} else if (drawHit.index >= 0) {
|
|
6943
|
+
this.selectedDrawing = drawHit.index;
|
|
6944
|
+
el.focus({ preventScroll: true });
|
|
6945
|
+
if (drawHit.handle >= 0) handleDrag = drawHit;
|
|
6946
|
+
else bodyDrag = { index: drawHit.index, last: this.dataAtPx(px, py) };
|
|
6947
|
+
el.style.cursor = "grabbing";
|
|
6948
|
+
this.requestRender();
|
|
6389
6949
|
} else if (this.mode === "pan") {
|
|
6950
|
+
this.selectedDrawing = -1;
|
|
6390
6951
|
panning = true;
|
|
6391
6952
|
lastX = e.clientX;
|
|
6392
6953
|
lastY = e.clientY;
|
|
6393
6954
|
el.style.cursor = "grabbing";
|
|
6394
6955
|
if (this.crosshair) this.setPress(px, py);
|
|
6395
6956
|
} else {
|
|
6957
|
+
this.selectedDrawing = -1;
|
|
6396
6958
|
selecting = true;
|
|
6397
6959
|
startX = px;
|
|
6398
6960
|
startY = py;
|
|
@@ -6411,12 +6973,25 @@ var Plot = class {
|
|
|
6411
6973
|
this.panY(axisDrag.y, e.clientY - lastY, region);
|
|
6412
6974
|
lastY = e.clientY;
|
|
6413
6975
|
this.requestRender();
|
|
6976
|
+
} else if (handleDrag) {
|
|
6977
|
+
const c = this.dataAtPx(e.clientX - rect.left, e.clientY - rect.top);
|
|
6978
|
+
this.setDrawingHandle(this.drawings[handleDrag.index], handleDrag.handle, c.x, c.y);
|
|
6979
|
+
this.requestRender();
|
|
6980
|
+
} else if (bodyDrag) {
|
|
6981
|
+
const c = this.dataAtPx(e.clientX - rect.left, e.clientY - rect.top);
|
|
6982
|
+
this.translateDrawing(this.drawings[bodyDrag.index], c.x - bodyDrag.last.x, c.y - bodyDrag.last.y);
|
|
6983
|
+
bodyDrag.last = c;
|
|
6984
|
+
this.requestRender();
|
|
6414
6985
|
} else if (panning) {
|
|
6415
6986
|
this.panX(e.clientX - lastX, region);
|
|
6416
6987
|
this.panY(null, e.clientY - lastY, region);
|
|
6417
6988
|
lastX = e.clientX;
|
|
6418
6989
|
lastY = e.clientY;
|
|
6419
6990
|
this.requestRender();
|
|
6991
|
+
} else if (drawing && this.drawTool) {
|
|
6992
|
+
const c = this.dataAtPx(e.clientX - rect.left, e.clientY - rect.top);
|
|
6993
|
+
this.pendingDrawing = this.buildDrawing(this.drawTool, drawStart, c);
|
|
6994
|
+
this.requestRender();
|
|
6420
6995
|
} else if (selecting) {
|
|
6421
6996
|
this.drawSelection(startX, startY, e.clientX - rect.left, e.clientY - rect.top);
|
|
6422
6997
|
} else {
|
|
@@ -6430,12 +7005,32 @@ var Plot = class {
|
|
|
6430
7005
|
el.style.cursor = "ns-resize";
|
|
6431
7006
|
this.setHover(null);
|
|
6432
7007
|
} else {
|
|
6433
|
-
this.
|
|
6434
|
-
if (
|
|
7008
|
+
const hit = !this.drawTool && this.drawings.length ? this.hitDrawing(px, py) : { index: -1, handle: -1 };
|
|
7009
|
+
if (hit.index >= 0) {
|
|
7010
|
+
if (this.hoverDrawing !== hit.index) {
|
|
7011
|
+
this.hoverDrawing = hit.index;
|
|
7012
|
+
this.requestRender();
|
|
7013
|
+
}
|
|
7014
|
+
el.style.cursor = hit.handle >= 0 ? "grab" : "pointer";
|
|
7015
|
+
this.setHover(null);
|
|
7016
|
+
} else {
|
|
7017
|
+
if (this.hoverDrawing !== -1) {
|
|
7018
|
+
this.hoverDrawing = -1;
|
|
7019
|
+
this.requestRender();
|
|
7020
|
+
}
|
|
7021
|
+
this.updateCursor();
|
|
7022
|
+
if (this.hoverEnabled) this.setHover({ x: px, y: py });
|
|
7023
|
+
}
|
|
6435
7024
|
}
|
|
6436
7025
|
}
|
|
6437
7026
|
});
|
|
6438
|
-
el.addEventListener("pointerleave", () =>
|
|
7027
|
+
el.addEventListener("pointerleave", () => {
|
|
7028
|
+
this.setHover(null);
|
|
7029
|
+
if (this.hoverDrawing !== -1) {
|
|
7030
|
+
this.hoverDrawing = -1;
|
|
7031
|
+
this.requestRender();
|
|
7032
|
+
}
|
|
7033
|
+
});
|
|
6439
7034
|
const end = (e) => {
|
|
6440
7035
|
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
|
|
6441
7036
|
if (this.pressPx) {
|
|
@@ -6448,6 +7043,20 @@ var Plot = class {
|
|
|
6448
7043
|
const isClick = e.type === "pointerup" && !axisDrag && Math.hypot(upPx - downX, upPy - downY) < 4;
|
|
6449
7044
|
if (axisDrag) {
|
|
6450
7045
|
axisDrag = null;
|
|
7046
|
+
} else if (handleDrag) {
|
|
7047
|
+
handleDrag = null;
|
|
7048
|
+
this.updateCursor();
|
|
7049
|
+
this.requestRender();
|
|
7050
|
+
} else if (bodyDrag) {
|
|
7051
|
+
bodyDrag = null;
|
|
7052
|
+
this.updateCursor();
|
|
7053
|
+
this.requestRender();
|
|
7054
|
+
} else if (drawing) {
|
|
7055
|
+
drawing = false;
|
|
7056
|
+
const moved = Math.hypot(upPx - downX, upPy - downY);
|
|
7057
|
+
if (this.pendingDrawing && (this.drawTool === "hline" || moved >= 3)) this.drawings.push(this.pendingDrawing);
|
|
7058
|
+
this.pendingDrawing = null;
|
|
7059
|
+
this.requestRender();
|
|
6451
7060
|
} else if (panning) {
|
|
6452
7061
|
panning = false;
|
|
6453
7062
|
this.updateCursor();
|
|
@@ -6456,10 +7065,34 @@ var Plot = class {
|
|
|
6456
7065
|
if (!isClick) this.applySelection(startX, startY, upPx, upPy);
|
|
6457
7066
|
this.selectionDiv.style.display = "none";
|
|
6458
7067
|
}
|
|
6459
|
-
if (isClick) this.handleClick(upPx, upPy);
|
|
7068
|
+
if (isClick && this.drawTool === null && this.selectedDrawing < 0) this.handleClick(upPx, upPy);
|
|
6460
7069
|
};
|
|
6461
7070
|
el.addEventListener("pointerup", end);
|
|
6462
7071
|
el.addEventListener("pointercancel", end);
|
|
7072
|
+
el.addEventListener("dblclick", (e) => {
|
|
7073
|
+
const rect = el.getBoundingClientRect();
|
|
7074
|
+
const hit = this.hitDrawing(e.clientX - rect.left, e.clientY - rect.top);
|
|
7075
|
+
if (hit.index >= 0) {
|
|
7076
|
+
e.preventDefault();
|
|
7077
|
+
this.selectedDrawing = hit.index;
|
|
7078
|
+
this.renameDrawing(hit.index);
|
|
7079
|
+
}
|
|
7080
|
+
});
|
|
7081
|
+
el.addEventListener("contextmenu", (e) => {
|
|
7082
|
+
const rect = el.getBoundingClientRect();
|
|
7083
|
+
const hit = this.hitDrawing(e.clientX - rect.left, e.clientY - rect.top);
|
|
7084
|
+
if (hit.index >= 0) {
|
|
7085
|
+
e.preventDefault();
|
|
7086
|
+
this.showDrawMenu(e.clientX, e.clientY, hit.index);
|
|
7087
|
+
}
|
|
7088
|
+
});
|
|
7089
|
+
el.tabIndex = el.tabIndex >= 0 ? el.tabIndex : 0;
|
|
7090
|
+
el.addEventListener("keydown", (e) => {
|
|
7091
|
+
if ((e.key === "Delete" || e.key === "Backspace") && this.selectedDrawing >= 0) {
|
|
7092
|
+
e.preventDefault();
|
|
7093
|
+
this.removeDrawing(this.selectedDrawing);
|
|
7094
|
+
}
|
|
7095
|
+
});
|
|
6463
7096
|
}
|
|
6464
7097
|
setHover(px) {
|
|
6465
7098
|
const had = this.hoverPx !== null;
|
|
@@ -6890,6 +7523,41 @@ var PolarPlot = class {
|
|
|
6890
7523
|
this.retransform();
|
|
6891
7524
|
this.requestRender();
|
|
6892
7525
|
}
|
|
7526
|
+
// --- Export ---------------------------------------------------------------
|
|
7527
|
+
/** Composite grid → data → overlay into one canvas at device resolution. */
|
|
7528
|
+
compositeCanvas(background) {
|
|
7529
|
+
this.render();
|
|
7530
|
+
const w = this.dataCanvas.width, h = this.dataCanvas.height;
|
|
7531
|
+
const out = document.createElement("canvas");
|
|
7532
|
+
out.width = w;
|
|
7533
|
+
out.height = h;
|
|
7534
|
+
const ctx = out.getContext("2d");
|
|
7535
|
+
const bg = background ?? (this.isDark ? "#0b1220" : "#ffffff");
|
|
7536
|
+
if (bg !== "transparent") {
|
|
7537
|
+
ctx.fillStyle = bg;
|
|
7538
|
+
ctx.fillRect(0, 0, w, h);
|
|
7539
|
+
}
|
|
7540
|
+
ctx.drawImage(this.gridCanvas, 0, 0);
|
|
7541
|
+
ctx.drawImage(this.dataCanvas, 0, 0);
|
|
7542
|
+
ctx.drawImage(this.overlayCanvas, 0, 0);
|
|
7543
|
+
return out;
|
|
7544
|
+
}
|
|
7545
|
+
/** Export the current view as a data URL (default PNG). */
|
|
7546
|
+
toDataURL(type = "image/png", opts = {}) {
|
|
7547
|
+
return this.compositeCanvas(opts.background).toDataURL(type, opts.quality);
|
|
7548
|
+
}
|
|
7549
|
+
/** Export the current view as a `Blob` (default PNG). */
|
|
7550
|
+
toBlob(type = "image/png", opts = {}) {
|
|
7551
|
+
return canvasToBlob(this.compositeCanvas(opts.background), type, opts.quality);
|
|
7552
|
+
}
|
|
7553
|
+
/** Download the current view as an image (PNG by default). */
|
|
7554
|
+
downloadImage(filename = "chart.png", type = "image/png", opts = {}) {
|
|
7555
|
+
return downloadCanvas(this.compositeCanvas(opts.background), filename, type, opts.quality);
|
|
7556
|
+
}
|
|
7557
|
+
/** Copy the current view to the clipboard as a PNG. */
|
|
7558
|
+
copyToClipboard(opts = {}) {
|
|
7559
|
+
return copyCanvasToClipboard(this.compositeCanvas(opts.background));
|
|
7560
|
+
}
|
|
6893
7561
|
destroy() {
|
|
6894
7562
|
this.resizeObserver.disconnect();
|
|
6895
7563
|
for (const e of this.entries) e.layer.dispose();
|
|
@@ -9366,6 +10034,7 @@ var Plot3D = class {
|
|
|
9366
10034
|
this.ambient = 0.35;
|
|
9367
10035
|
this.controlsEl = null;
|
|
9368
10036
|
this.resetBtn = null;
|
|
10037
|
+
this.downloadBtn = null;
|
|
9369
10038
|
/** Screen position (device px) of the picked point, for the highlight ring. */
|
|
9370
10039
|
this.hoverHit = null;
|
|
9371
10040
|
this.container = container;
|
|
@@ -9461,6 +10130,31 @@ var Plot3D = class {
|
|
|
9461
10130
|
container.appendChild(btn);
|
|
9462
10131
|
this.resetBtn = btn;
|
|
9463
10132
|
}
|
|
10133
|
+
if (options.downloadButton !== false) {
|
|
10134
|
+
const dl = document.createElement("button");
|
|
10135
|
+
dl.type = "button";
|
|
10136
|
+
dl.title = "Download PNG";
|
|
10137
|
+
dl.textContent = "\u2913";
|
|
10138
|
+
Object.assign(dl.style, {
|
|
10139
|
+
position: "absolute",
|
|
10140
|
+
bottom: "8px",
|
|
10141
|
+
left: options.resetButton !== false ? "40px" : "8px",
|
|
10142
|
+
zIndex: "6",
|
|
10143
|
+
width: "26px",
|
|
10144
|
+
height: "26px",
|
|
10145
|
+
padding: "0",
|
|
10146
|
+
cursor: "pointer",
|
|
10147
|
+
borderRadius: "6px",
|
|
10148
|
+
font: "16px system-ui, sans-serif",
|
|
10149
|
+
lineHeight: "24px",
|
|
10150
|
+
background: "rgba(15,23,42,0.8)",
|
|
10151
|
+
color: "#cbd5e1",
|
|
10152
|
+
border: "1px solid rgba(148,163,184,0.25)"
|
|
10153
|
+
});
|
|
10154
|
+
dl.addEventListener("click", () => void this.downloadImage());
|
|
10155
|
+
container.appendChild(dl);
|
|
10156
|
+
this.downloadBtn = dl;
|
|
10157
|
+
}
|
|
9464
10158
|
this.lineProgram = createProgram(this.gl, LINE_VERT, LINE_FRAG);
|
|
9465
10159
|
this.lineUniforms = uniformLocations(this.gl, this.lineProgram, ["uVP", "uColor"]);
|
|
9466
10160
|
this.boxVao = this.gl.createVertexArray();
|
|
@@ -9671,6 +10365,38 @@ var Plot3D = class {
|
|
|
9671
10365
|
this.recompute();
|
|
9672
10366
|
this.requestRender();
|
|
9673
10367
|
}
|
|
10368
|
+
// --- Export ---------------------------------------------------------------
|
|
10369
|
+
/** Copy the current 3D frame (already composited on one canvas) into a fresh canvas. */
|
|
10370
|
+
compositeCanvas(background) {
|
|
10371
|
+
this.render();
|
|
10372
|
+
const w = this.canvas.width, h = this.canvas.height;
|
|
10373
|
+
const out = document.createElement("canvas");
|
|
10374
|
+
out.width = w;
|
|
10375
|
+
out.height = h;
|
|
10376
|
+
const ctx = out.getContext("2d");
|
|
10377
|
+
if (background && background !== "transparent") {
|
|
10378
|
+
ctx.fillStyle = background;
|
|
10379
|
+
ctx.fillRect(0, 0, w, h);
|
|
10380
|
+
}
|
|
10381
|
+
ctx.drawImage(this.canvas, 0, 0);
|
|
10382
|
+
return out;
|
|
10383
|
+
}
|
|
10384
|
+
/** Export the current view as a data URL (default PNG). */
|
|
10385
|
+
toDataURL(type = "image/png", opts = {}) {
|
|
10386
|
+
return this.compositeCanvas(opts.background).toDataURL(type, opts.quality);
|
|
10387
|
+
}
|
|
10388
|
+
/** Export the current view as a `Blob` (default PNG). */
|
|
10389
|
+
toBlob(type = "image/png", opts = {}) {
|
|
10390
|
+
return canvasToBlob(this.compositeCanvas(opts.background), type, opts.quality);
|
|
10391
|
+
}
|
|
10392
|
+
/** Download the current view as an image (PNG by default). */
|
|
10393
|
+
downloadImage(filename = "chart.png", type = "image/png", opts = {}) {
|
|
10394
|
+
return downloadCanvas(this.compositeCanvas(opts.background), filename, type, opts.quality);
|
|
10395
|
+
}
|
|
10396
|
+
/** Copy the current view to the clipboard as a PNG. */
|
|
10397
|
+
copyToClipboard(opts = {}) {
|
|
10398
|
+
return copyCanvasToClipboard(this.compositeCanvas(opts.background));
|
|
10399
|
+
}
|
|
9674
10400
|
destroy() {
|
|
9675
10401
|
this.resizeObserver.disconnect();
|
|
9676
10402
|
this.autoRotateSpeed = 0;
|
|
@@ -9687,6 +10413,7 @@ var Plot3D = class {
|
|
|
9687
10413
|
this.colorbarDiv.remove();
|
|
9688
10414
|
this.tooltip.remove();
|
|
9689
10415
|
this.resetBtn?.remove();
|
|
10416
|
+
this.downloadBtn?.remove();
|
|
9690
10417
|
this.container.removeChild(this.canvas);
|
|
9691
10418
|
}
|
|
9692
10419
|
recompute() {
|
|
@@ -10202,6 +10929,137 @@ function firstFinite(values) {
|
|
|
10202
10929
|
for (let i = 0; i < values.length; i++) if (!Number.isNaN(values[i])) return i;
|
|
10203
10930
|
return -1;
|
|
10204
10931
|
}
|
|
10932
|
+
function rollingHigh(v, period) {
|
|
10933
|
+
const n = v.length, out = new Float64Array(n).fill(NaN);
|
|
10934
|
+
for (let i = period - 1; i < n; i++) {
|
|
10935
|
+
let m = -Infinity;
|
|
10936
|
+
for (let k = 0; k < period; k++) m = Math.max(m, v[i - k]);
|
|
10937
|
+
out[i] = m;
|
|
10938
|
+
}
|
|
10939
|
+
return out;
|
|
10940
|
+
}
|
|
10941
|
+
function rollingLow(v, period) {
|
|
10942
|
+
const n = v.length, out = new Float64Array(n).fill(NaN);
|
|
10943
|
+
for (let i = period - 1; i < n; i++) {
|
|
10944
|
+
let m = Infinity;
|
|
10945
|
+
for (let k = 0; k < period; k++) m = Math.min(m, v[i - k]);
|
|
10946
|
+
out[i] = m;
|
|
10947
|
+
}
|
|
10948
|
+
return out;
|
|
10949
|
+
}
|
|
10950
|
+
function stochastic(high, low, close, kPeriod = 14, dPeriod = 3) {
|
|
10951
|
+
const n = Math.min(high.length, low.length, close.length);
|
|
10952
|
+
const hh = rollingHigh(high, kPeriod), ll = rollingLow(low, kPeriod);
|
|
10953
|
+
const k = new Float64Array(n).fill(NaN);
|
|
10954
|
+
for (let i = 0; i < n; i++) {
|
|
10955
|
+
if (!Number.isNaN(hh[i])) {
|
|
10956
|
+
const rng = hh[i] - ll[i];
|
|
10957
|
+
k[i] = rng === 0 ? 50 : 100 * (close[i] - ll[i]) / rng;
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
return { k, d: sma(k, dPeriod) };
|
|
10961
|
+
}
|
|
10962
|
+
function keltner(high, low, close, period = 20, mult = 2, atrPeriod = 10) {
|
|
10963
|
+
const middle = ema(close, period);
|
|
10964
|
+
const a = atr(high, low, close, atrPeriod);
|
|
10965
|
+
const n = close.length, upper = new Float64Array(n).fill(NaN), lower = new Float64Array(n).fill(NaN);
|
|
10966
|
+
for (let i = 0; i < n; i++) if (!Number.isNaN(middle[i]) && !Number.isNaN(a[i])) {
|
|
10967
|
+
upper[i] = middle[i] + mult * a[i];
|
|
10968
|
+
lower[i] = middle[i] - mult * a[i];
|
|
10969
|
+
}
|
|
10970
|
+
return { middle, upper, lower };
|
|
10971
|
+
}
|
|
10972
|
+
function obv(close, volume) {
|
|
10973
|
+
const n = Math.min(close.length, volume.length), out = new Float64Array(n);
|
|
10974
|
+
for (let i = 1; i < n; i++) {
|
|
10975
|
+
const d = close[i] - close[i - 1];
|
|
10976
|
+
out[i] = out[i - 1] + (d > 0 ? volume[i] : d < 0 ? -volume[i] : 0);
|
|
10977
|
+
}
|
|
10978
|
+
return out;
|
|
10979
|
+
}
|
|
10980
|
+
function ichimoku(high, low, convPeriod = 9, basePeriod = 26, spanBPeriod = 52) {
|
|
10981
|
+
const n = Math.min(high.length, low.length);
|
|
10982
|
+
const mid = (p) => {
|
|
10983
|
+
const hi = rollingHigh(high, p), lo = rollingLow(low, p), o = new Float64Array(n).fill(NaN);
|
|
10984
|
+
for (let i = 0; i < n; i++) if (!Number.isNaN(hi[i])) o[i] = (hi[i] + lo[i]) / 2;
|
|
10985
|
+
return o;
|
|
10986
|
+
};
|
|
10987
|
+
const conversion = mid(convPeriod), base = mid(basePeriod), spanB = mid(spanBPeriod);
|
|
10988
|
+
const spanA = new Float64Array(n).fill(NaN);
|
|
10989
|
+
for (let i = 0; i < n; i++) if (!Number.isNaN(conversion[i]) && !Number.isNaN(base[i])) spanA[i] = (conversion[i] + base[i]) / 2;
|
|
10990
|
+
return { conversion, base, spanA, spanB };
|
|
10991
|
+
}
|
|
10992
|
+
function adx(high, low, close, period = 14) {
|
|
10993
|
+
const n = Math.min(high.length, low.length, close.length);
|
|
10994
|
+
const plusDI = new Float64Array(n).fill(NaN), minusDI = new Float64Array(n).fill(NaN), out = new Float64Array(n).fill(NaN);
|
|
10995
|
+
if (n <= period) return { adx: out, plusDI, minusDI };
|
|
10996
|
+
const tr = new Float64Array(n), pdm = new Float64Array(n), mdm = new Float64Array(n);
|
|
10997
|
+
for (let i = 1; i < n; i++) {
|
|
10998
|
+
const up = high[i] - high[i - 1], down = low[i - 1] - low[i];
|
|
10999
|
+
pdm[i] = up > down && up > 0 ? up : 0;
|
|
11000
|
+
mdm[i] = down > up && down > 0 ? down : 0;
|
|
11001
|
+
const pc = close[i - 1];
|
|
11002
|
+
tr[i] = Math.max(high[i] - low[i], Math.abs(high[i] - pc), Math.abs(low[i] - pc));
|
|
11003
|
+
}
|
|
11004
|
+
let sTR = 0, sP = 0, sM = 0;
|
|
11005
|
+
for (let i = 1; i <= period; i++) {
|
|
11006
|
+
sTR += tr[i];
|
|
11007
|
+
sP += pdm[i];
|
|
11008
|
+
sM += mdm[i];
|
|
11009
|
+
}
|
|
11010
|
+
const dx = new Float64Array(n).fill(NaN);
|
|
11011
|
+
const diAt = (i) => {
|
|
11012
|
+
const p = sTR === 0 ? 0 : 100 * sP / sTR, m = sTR === 0 ? 0 : 100 * sM / sTR;
|
|
11013
|
+
plusDI[i] = p;
|
|
11014
|
+
minusDI[i] = m;
|
|
11015
|
+
const sum = p + m;
|
|
11016
|
+
dx[i] = sum === 0 ? 0 : 100 * Math.abs(p - m) / sum;
|
|
11017
|
+
};
|
|
11018
|
+
diAt(period);
|
|
11019
|
+
for (let i = period + 1; i < n; i++) {
|
|
11020
|
+
sTR = sTR - sTR / period + tr[i];
|
|
11021
|
+
sP = sP - sP / period + pdm[i];
|
|
11022
|
+
sM = sM - sM / period + mdm[i];
|
|
11023
|
+
diAt(i);
|
|
11024
|
+
}
|
|
11025
|
+
const start = period, adxStart = start + period;
|
|
11026
|
+
if (adxStart < n) {
|
|
11027
|
+
let acc = 0;
|
|
11028
|
+
for (let i = start + 1; i <= adxStart; i++) acc += dx[i];
|
|
11029
|
+
let prev = acc / period;
|
|
11030
|
+
out[adxStart] = prev;
|
|
11031
|
+
for (let i = adxStart + 1; i < n; i++) {
|
|
11032
|
+
prev = (prev * (period - 1) + dx[i]) / period;
|
|
11033
|
+
out[i] = prev;
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
return { adx: out, plusDI, minusDI };
|
|
11037
|
+
}
|
|
11038
|
+
function superTrend(high, low, close, period = 10, mult = 3) {
|
|
11039
|
+
const n = Math.min(high.length, low.length, close.length);
|
|
11040
|
+
const a = atr(high, low, close, period);
|
|
11041
|
+
const trend = new Float64Array(n).fill(NaN), direction = new Float64Array(n).fill(NaN);
|
|
11042
|
+
const finalUpper = new Float64Array(n), finalLower = new Float64Array(n);
|
|
11043
|
+
let dir = 1;
|
|
11044
|
+
for (let i = 0; i < n; i++) {
|
|
11045
|
+
if (Number.isNaN(a[i])) continue;
|
|
11046
|
+
const hl2 = (high[i] + low[i]) / 2;
|
|
11047
|
+
const bU = hl2 + mult * a[i], bL = hl2 - mult * a[i];
|
|
11048
|
+
const prevValid = i > 0 && !Number.isNaN(a[i - 1]);
|
|
11049
|
+
finalUpper[i] = prevValid && (bU < finalUpper[i - 1] || close[i - 1] > finalUpper[i - 1]) ? bU : prevValid ? finalUpper[i - 1] : bU;
|
|
11050
|
+
finalLower[i] = prevValid && (bL > finalLower[i - 1] || close[i - 1] < finalLower[i - 1]) ? bL : prevValid ? finalLower[i - 1] : bL;
|
|
11051
|
+
if (!prevValid) dir = close[i] >= hl2 ? 1 : -1;
|
|
11052
|
+
else if (dir === 1 && close[i] < finalLower[i]) dir = -1;
|
|
11053
|
+
else if (dir === -1 && close[i] > finalUpper[i]) dir = 1;
|
|
11054
|
+
direction[i] = dir;
|
|
11055
|
+
trend[i] = dir === 1 ? finalLower[i] : finalUpper[i];
|
|
11056
|
+
}
|
|
11057
|
+
return { trend, direction };
|
|
11058
|
+
}
|
|
11059
|
+
function fibRetracements(high, low, ratios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]) {
|
|
11060
|
+
const span = high - low;
|
|
11061
|
+
return ratios.map((r) => ({ ratio: r, price: high - span * r }));
|
|
11062
|
+
}
|
|
10205
11063
|
|
|
10206
11064
|
// src/finance/transforms.ts
|
|
10207
11065
|
function heikinAshi(d) {
|
|
@@ -10437,17 +11295,908 @@ function addDepth(plot, opts) {
|
|
|
10437
11295
|
})
|
|
10438
11296
|
};
|
|
10439
11297
|
}
|
|
11298
|
+
|
|
11299
|
+
// src/data/csv.ts
|
|
11300
|
+
function tokenize(text, delim) {
|
|
11301
|
+
const rows = [];
|
|
11302
|
+
let row = [];
|
|
11303
|
+
let field = "";
|
|
11304
|
+
let inQuotes = false;
|
|
11305
|
+
for (let i = 0; i < text.length; i++) {
|
|
11306
|
+
const c = text[i];
|
|
11307
|
+
if (inQuotes) {
|
|
11308
|
+
if (c === '"') {
|
|
11309
|
+
if (text[i + 1] === '"') {
|
|
11310
|
+
field += '"';
|
|
11311
|
+
i++;
|
|
11312
|
+
} else inQuotes = false;
|
|
11313
|
+
} else field += c;
|
|
11314
|
+
continue;
|
|
11315
|
+
}
|
|
11316
|
+
if (c === '"') {
|
|
11317
|
+
inQuotes = true;
|
|
11318
|
+
} else if (c === delim) {
|
|
11319
|
+
row.push(field);
|
|
11320
|
+
field = "";
|
|
11321
|
+
} else if (c === "\n" || c === "\r") {
|
|
11322
|
+
if (c === "\r" && text[i + 1] === "\n") i++;
|
|
11323
|
+
row.push(field);
|
|
11324
|
+
rows.push(row);
|
|
11325
|
+
row = [];
|
|
11326
|
+
field = "";
|
|
11327
|
+
} else field += c;
|
|
11328
|
+
}
|
|
11329
|
+
if (field.length > 0 || row.length > 0) {
|
|
11330
|
+
row.push(field);
|
|
11331
|
+
rows.push(row);
|
|
11332
|
+
}
|
|
11333
|
+
return rows;
|
|
11334
|
+
}
|
|
11335
|
+
function parseCSV(text, opts = {}) {
|
|
11336
|
+
const delim = opts.delimiter ?? ",";
|
|
11337
|
+
const useHeader = opts.header !== false;
|
|
11338
|
+
let all = tokenize(text, delim);
|
|
11339
|
+
if (opts.skipEmpty !== false) all = all.filter((r) => !(r.length === 1 && r[0].trim() === ""));
|
|
11340
|
+
let headers;
|
|
11341
|
+
let dataRows;
|
|
11342
|
+
if (useHeader && all.length) {
|
|
11343
|
+
headers = all[0].map((h) => h.trim());
|
|
11344
|
+
dataRows = all.slice(1);
|
|
11345
|
+
} else {
|
|
11346
|
+
const n = all[0]?.length ?? 0;
|
|
11347
|
+
headers = Array.from({ length: n }, (_, i) => `col${i}`);
|
|
11348
|
+
dataRows = all;
|
|
11349
|
+
}
|
|
11350
|
+
const idx = (name) => typeof name === "number" ? name : headers.indexOf(name);
|
|
11351
|
+
return {
|
|
11352
|
+
headers,
|
|
11353
|
+
rows: dataRows,
|
|
11354
|
+
get length() {
|
|
11355
|
+
return dataRows.length;
|
|
11356
|
+
},
|
|
11357
|
+
column(name) {
|
|
11358
|
+
const i = idx(name);
|
|
11359
|
+
return dataRows.map((r) => r[i] ?? "");
|
|
11360
|
+
},
|
|
11361
|
+
numeric(name) {
|
|
11362
|
+
const i = idx(name);
|
|
11363
|
+
const out = new Float64Array(dataRows.length);
|
|
11364
|
+
for (let k = 0; k < dataRows.length; k++) out[k] = Number.parseFloat(dataRows[k][i] ?? "");
|
|
11365
|
+
return out;
|
|
11366
|
+
}
|
|
11367
|
+
};
|
|
11368
|
+
}
|
|
11369
|
+
|
|
11370
|
+
// src/data/downsample.ts
|
|
11371
|
+
function lttb(x, y, threshold) {
|
|
11372
|
+
const n = Math.min(x.length, y.length);
|
|
11373
|
+
if (threshold >= n || threshold <= 2) {
|
|
11374
|
+
const rx = new Float64Array(n), ry = new Float64Array(n);
|
|
11375
|
+
for (let i = 0; i < n; i++) {
|
|
11376
|
+
rx[i] = x[i];
|
|
11377
|
+
ry[i] = y[i];
|
|
11378
|
+
}
|
|
11379
|
+
return { x: rx, y: ry };
|
|
11380
|
+
}
|
|
11381
|
+
const sx = new Float64Array(threshold), sy = new Float64Array(threshold);
|
|
11382
|
+
const bucket = (n - 2) / (threshold - 2);
|
|
11383
|
+
let a = 0;
|
|
11384
|
+
sx[0] = x[0];
|
|
11385
|
+
sy[0] = y[0];
|
|
11386
|
+
let out = 1;
|
|
11387
|
+
for (let i = 0; i < threshold - 2; i++) {
|
|
11388
|
+
let avgX = 0, avgY = 0;
|
|
11389
|
+
let rs = Math.floor((i + 1) * bucket) + 1;
|
|
11390
|
+
const re = Math.min(Math.floor((i + 2) * bucket) + 1, n);
|
|
11391
|
+
const rlen = re - rs || 1;
|
|
11392
|
+
for (let k = rs; k < re; k++) {
|
|
11393
|
+
avgX += x[k];
|
|
11394
|
+
avgY += y[k];
|
|
11395
|
+
}
|
|
11396
|
+
avgX /= rlen;
|
|
11397
|
+
avgY /= rlen;
|
|
11398
|
+
let cs = Math.floor(i * bucket) + 1;
|
|
11399
|
+
const ce = Math.floor((i + 1) * bucket) + 1;
|
|
11400
|
+
const ax = x[a], ay = y[a];
|
|
11401
|
+
let maxArea = -1, chosen = cs, cx = x[cs], cy = y[cs];
|
|
11402
|
+
for (; cs < ce; cs++) {
|
|
11403
|
+
const area2 = Math.abs((ax - avgX) * (y[cs] - ay) - (ax - x[cs]) * (avgY - ay));
|
|
11404
|
+
if (area2 > maxArea) {
|
|
11405
|
+
maxArea = area2;
|
|
11406
|
+
chosen = cs;
|
|
11407
|
+
cx = x[cs];
|
|
11408
|
+
cy = y[cs];
|
|
11409
|
+
}
|
|
11410
|
+
}
|
|
11411
|
+
sx[out] = cx;
|
|
11412
|
+
sy[out] = cy;
|
|
11413
|
+
out++;
|
|
11414
|
+
a = chosen;
|
|
11415
|
+
}
|
|
11416
|
+
sx[out] = x[n - 1];
|
|
11417
|
+
sy[out] = y[n - 1];
|
|
11418
|
+
return { x: sx, y: sy };
|
|
11419
|
+
}
|
|
11420
|
+
|
|
11421
|
+
// src/charts/treemap.ts
|
|
11422
|
+
var DEFAULT_EXTENT = { x: [0, 1], y: [0, 1] };
|
|
11423
|
+
var TREEMAP_PALETTE = [
|
|
11424
|
+
"#4e79a7",
|
|
11425
|
+
"#f28e2b",
|
|
11426
|
+
"#e15759",
|
|
11427
|
+
"#76b7b2",
|
|
11428
|
+
"#59a14f",
|
|
11429
|
+
"#edc948",
|
|
11430
|
+
"#b07aa1",
|
|
11431
|
+
"#ff9da7",
|
|
11432
|
+
"#9c755f",
|
|
11433
|
+
"#bab0ac"
|
|
11434
|
+
];
|
|
11435
|
+
function worst(row, w, sum) {
|
|
11436
|
+
if (row.length === 0 || w === 0) return Infinity;
|
|
11437
|
+
let max = -Infinity, min = Infinity;
|
|
11438
|
+
for (const a of row) {
|
|
11439
|
+
if (a > max) max = a;
|
|
11440
|
+
if (a < min) min = a;
|
|
11441
|
+
}
|
|
11442
|
+
const s2 = sum * sum, w2 = w * w;
|
|
11443
|
+
return Math.max(w2 * max / s2, s2 / (w2 * min));
|
|
11444
|
+
}
|
|
11445
|
+
function treemapLayout(items, extent = DEFAULT_EXTENT) {
|
|
11446
|
+
const clean = items.filter((it) => it.value > 0);
|
|
11447
|
+
if (clean.length === 0) return [];
|
|
11448
|
+
const order = clean.map((_, i) => i).sort((a, b) => clean[b].value - clean[a].value);
|
|
11449
|
+
const total = clean.reduce((s, it) => s + it.value, 0);
|
|
11450
|
+
let [x0, x1] = extent.x;
|
|
11451
|
+
let [y0, y1] = extent.y;
|
|
11452
|
+
const totalArea = Math.abs(x1 - x0) * Math.abs(y1 - y0);
|
|
11453
|
+
const areas = order.map((i) => clean[i].value / total * totalArea);
|
|
11454
|
+
const cells = [];
|
|
11455
|
+
let pos = 0;
|
|
11456
|
+
const layoutRow = (row, startPos) => {
|
|
11457
|
+
const rowSum = row.reduce((s, a) => s + a, 0);
|
|
11458
|
+
const w = Math.min(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
11459
|
+
if (rowSum === 0 || w === 0) return;
|
|
11460
|
+
const horizontal = Math.abs(y1 - y0) < Math.abs(x1 - x0);
|
|
11461
|
+
if (horizontal) {
|
|
11462
|
+
const rowW = rowSum / Math.abs(y1 - y0);
|
|
11463
|
+
let cy = y0;
|
|
11464
|
+
for (let k = 0; k < row.length; k++) {
|
|
11465
|
+
const idx = order[startPos + k];
|
|
11466
|
+
const h = row[k] / rowSum * Math.abs(y1 - y0);
|
|
11467
|
+
cells.push({
|
|
11468
|
+
label: clean[idx].label,
|
|
11469
|
+
value: clean[idx].value,
|
|
11470
|
+
color: clean[idx].color,
|
|
11471
|
+
x0,
|
|
11472
|
+
y0: cy,
|
|
11473
|
+
x1: x0 + rowW,
|
|
11474
|
+
y1: cy + h
|
|
11475
|
+
});
|
|
11476
|
+
cy += h;
|
|
11477
|
+
}
|
|
11478
|
+
x0 += rowW;
|
|
11479
|
+
} else {
|
|
11480
|
+
const rowH = rowSum / Math.abs(x1 - x0);
|
|
11481
|
+
let cx = x0;
|
|
11482
|
+
for (let k = 0; k < row.length; k++) {
|
|
11483
|
+
const idx = order[startPos + k];
|
|
11484
|
+
const wdt = row[k] / rowSum * Math.abs(x1 - x0);
|
|
11485
|
+
cells.push({
|
|
11486
|
+
label: clean[idx].label,
|
|
11487
|
+
value: clean[idx].value,
|
|
11488
|
+
color: clean[idx].color,
|
|
11489
|
+
x0: cx,
|
|
11490
|
+
y0,
|
|
11491
|
+
x1: cx + wdt,
|
|
11492
|
+
y1: y0 + rowH
|
|
11493
|
+
});
|
|
11494
|
+
cx += wdt;
|
|
11495
|
+
}
|
|
11496
|
+
y0 += rowH;
|
|
11497
|
+
}
|
|
11498
|
+
};
|
|
11499
|
+
while (pos < areas.length) {
|
|
11500
|
+
const w = Math.min(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
11501
|
+
const row = [];
|
|
11502
|
+
let rowSum = 0;
|
|
11503
|
+
let start = pos;
|
|
11504
|
+
while (pos < areas.length) {
|
|
11505
|
+
const a = areas[pos];
|
|
11506
|
+
const withNew = worst([...row, a], w, rowSum + a);
|
|
11507
|
+
const current = worst(row, w, rowSum);
|
|
11508
|
+
if (row.length > 0 && withNew > current) break;
|
|
11509
|
+
row.push(a);
|
|
11510
|
+
rowSum += a;
|
|
11511
|
+
pos++;
|
|
11512
|
+
}
|
|
11513
|
+
layoutRow(row, start);
|
|
11514
|
+
}
|
|
11515
|
+
return cells;
|
|
11516
|
+
}
|
|
11517
|
+
var LABEL_MIN_FRAC = 0.04;
|
|
11518
|
+
function addTreemap(plot, opts) {
|
|
11519
|
+
const extent = opts.extent ?? DEFAULT_EXTENT;
|
|
11520
|
+
const palette = opts.colors && opts.colors.length ? opts.colors : TREEMAP_PALETTE;
|
|
11521
|
+
const cells = treemapLayout(opts.items, extent);
|
|
11522
|
+
const patches = cells.map((c, i) => ({
|
|
11523
|
+
x: [c.x0, c.x1, c.x1, c.x0],
|
|
11524
|
+
y: [c.y0, c.y0, c.y1, c.y1],
|
|
11525
|
+
color: c.color ?? palette[i % palette.length]
|
|
11526
|
+
}));
|
|
11527
|
+
const layer = plot.addPatches({
|
|
11528
|
+
patches,
|
|
11529
|
+
opacity: opts.opacity,
|
|
11530
|
+
name: opts.name,
|
|
11531
|
+
renderType: opts.renderType
|
|
11532
|
+
});
|
|
11533
|
+
if (opts.labels !== false) {
|
|
11534
|
+
const minW = Math.abs(extent.x[1] - extent.x[0]) * LABEL_MIN_FRAC;
|
|
11535
|
+
const minH = Math.abs(extent.y[1] - extent.y[0]) * LABEL_MIN_FRAC;
|
|
11536
|
+
for (const c of cells) {
|
|
11537
|
+
if (Math.abs(c.x1 - c.x0) < minW || Math.abs(c.y1 - c.y0) < minH) continue;
|
|
11538
|
+
plot.addAnnotation({
|
|
11539
|
+
type: "label",
|
|
11540
|
+
x: (c.x0 + c.x1) / 2,
|
|
11541
|
+
y: (c.y0 + c.y1) / 2,
|
|
11542
|
+
text: c.label,
|
|
11543
|
+
align: "center"
|
|
11544
|
+
});
|
|
11545
|
+
}
|
|
11546
|
+
}
|
|
11547
|
+
return layer;
|
|
11548
|
+
}
|
|
11549
|
+
|
|
11550
|
+
// src/charts/funnel.ts
|
|
11551
|
+
var FUNNEL_PALETTE = [
|
|
11552
|
+
"#4e79a7",
|
|
11553
|
+
"#f28e2b",
|
|
11554
|
+
"#e15759",
|
|
11555
|
+
"#76b7b2",
|
|
11556
|
+
"#59a14f",
|
|
11557
|
+
"#edc948",
|
|
11558
|
+
"#b07aa1",
|
|
11559
|
+
"#ff9da7",
|
|
11560
|
+
"#9c755f",
|
|
11561
|
+
"#bab0ac"
|
|
11562
|
+
];
|
|
11563
|
+
function funnelLayout(items, opts = {}) {
|
|
11564
|
+
const n = items.length;
|
|
11565
|
+
if (n === 0) return [];
|
|
11566
|
+
const fullW = opts.width ?? 1;
|
|
11567
|
+
const fullH = opts.height ?? 1;
|
|
11568
|
+
const neck = opts.neck ?? 0.4;
|
|
11569
|
+
const maxV = items.reduce((m, it) => Math.max(m, it.value), 0) || 1;
|
|
11570
|
+
const halfW = (i) => Math.max(0, items[i].value) / maxV * fullW * 0.5;
|
|
11571
|
+
const stageH = fullH / n;
|
|
11572
|
+
const stages = [];
|
|
11573
|
+
for (let i = 0; i < n; i++) {
|
|
11574
|
+
const top = fullH - i * stageH;
|
|
11575
|
+
const bot = fullH - (i + 1) * stageH;
|
|
11576
|
+
const wTop = halfW(i);
|
|
11577
|
+
const wBot = i + 1 < n ? halfW(i + 1) : wTop * neck;
|
|
11578
|
+
stages.push({
|
|
11579
|
+
label: items[i].label,
|
|
11580
|
+
value: items[i].value,
|
|
11581
|
+
color: items[i].color,
|
|
11582
|
+
// CW ring: top-left, top-right, bottom-right, bottom-left.
|
|
11583
|
+
poly: {
|
|
11584
|
+
x: [-wTop, wTop, wBot, -wBot],
|
|
11585
|
+
y: [top, top, bot, bot]
|
|
11586
|
+
}
|
|
11587
|
+
});
|
|
11588
|
+
}
|
|
11589
|
+
return stages;
|
|
11590
|
+
}
|
|
11591
|
+
function addFunnel(plot, opts) {
|
|
11592
|
+
const palette = opts.colors && opts.colors.length ? opts.colors : FUNNEL_PALETTE;
|
|
11593
|
+
const stages = funnelLayout(opts.items, opts);
|
|
11594
|
+
const patches = stages.map((s, i) => ({
|
|
11595
|
+
x: s.poly.x,
|
|
11596
|
+
y: s.poly.y,
|
|
11597
|
+
color: s.color ?? palette[i % palette.length]
|
|
11598
|
+
}));
|
|
11599
|
+
const layer = plot.addPatches({
|
|
11600
|
+
patches,
|
|
11601
|
+
opacity: opts.opacity,
|
|
11602
|
+
name: opts.name,
|
|
11603
|
+
renderType: opts.renderType
|
|
11604
|
+
});
|
|
11605
|
+
if (opts.labels !== false) {
|
|
11606
|
+
for (const s of stages) {
|
|
11607
|
+
const cy = (s.poly.y[0] + s.poly.y[2]) / 2;
|
|
11608
|
+
plot.addAnnotation({ type: "label", x: 0, y: cy, text: s.label, align: "center" });
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
return layer;
|
|
11612
|
+
}
|
|
11613
|
+
|
|
11614
|
+
// src/charts/sunburst.ts
|
|
11615
|
+
var DEFAULT_COLORS2 = [
|
|
11616
|
+
"#3b82f6",
|
|
11617
|
+
"#f472b6",
|
|
11618
|
+
"#22d3ee",
|
|
11619
|
+
"#a3e635",
|
|
11620
|
+
"#fbbf24",
|
|
11621
|
+
"#a78bfa",
|
|
11622
|
+
"#34d399",
|
|
11623
|
+
"#fb7185",
|
|
11624
|
+
"#60a5fa",
|
|
11625
|
+
"#f59e0b"
|
|
11626
|
+
];
|
|
11627
|
+
function nodeValue(node) {
|
|
11628
|
+
if (node.children && node.children.length > 0) {
|
|
11629
|
+
let sum = 0;
|
|
11630
|
+
for (const c of node.children) sum += nodeValue(c);
|
|
11631
|
+
return sum;
|
|
11632
|
+
}
|
|
11633
|
+
return Math.max(0, node.value ?? 0);
|
|
11634
|
+
}
|
|
11635
|
+
function sunburstLayout(root, opts = {}) {
|
|
11636
|
+
const ringWidth = opts.ringWidth ?? 1;
|
|
11637
|
+
const center = opts.center ?? 0;
|
|
11638
|
+
const start = opts.startAngle ?? Math.PI / 2;
|
|
11639
|
+
const out = [];
|
|
11640
|
+
const recurse = (node, depth2, a0, a1) => {
|
|
11641
|
+
const r0 = center + depth2 * ringWidth;
|
|
11642
|
+
out.push({ name: node.name, depth: depth2, a0, a1, r0, r1: r0 + ringWidth, color: node.color });
|
|
11643
|
+
if (!node.children || node.children.length === 0) return;
|
|
11644
|
+
const total = nodeValue(node) || 1;
|
|
11645
|
+
let a = a0;
|
|
11646
|
+
for (const child of node.children) {
|
|
11647
|
+
const span = (a1 - a0) * nodeValue(child) / total;
|
|
11648
|
+
recurse(child, depth2 + 1, a, a + span);
|
|
11649
|
+
a += span;
|
|
11650
|
+
}
|
|
11651
|
+
};
|
|
11652
|
+
recurse(root, 0, start, start + Math.PI * 2);
|
|
11653
|
+
return out;
|
|
11654
|
+
}
|
|
11655
|
+
function arcRing(a0, a1, r0, r1, color) {
|
|
11656
|
+
const step = Math.PI / 90;
|
|
11657
|
+
const segs = Math.max(1, Math.ceil(Math.abs(a1 - a0) / step));
|
|
11658
|
+
const x = [];
|
|
11659
|
+
const y = [];
|
|
11660
|
+
for (let s = 0; s <= segs; s++) {
|
|
11661
|
+
const t = a0 + (a1 - a0) * s / segs;
|
|
11662
|
+
x.push(r1 * Math.cos(t));
|
|
11663
|
+
y.push(r1 * Math.sin(t));
|
|
11664
|
+
}
|
|
11665
|
+
for (let s = segs; s >= 0; s--) {
|
|
11666
|
+
const t = a0 + (a1 - a0) * s / segs;
|
|
11667
|
+
x.push(r0 * Math.cos(t));
|
|
11668
|
+
y.push(r0 * Math.sin(t));
|
|
11669
|
+
}
|
|
11670
|
+
return { x, y, color };
|
|
11671
|
+
}
|
|
11672
|
+
function addSunburst(plot, opts) {
|
|
11673
|
+
const palette = opts.colors && opts.colors.length > 0 ? opts.colors : DEFAULT_COLORS2;
|
|
11674
|
+
const arcs = sunburstLayout(opts.root, { ringWidth: opts.ringWidth });
|
|
11675
|
+
const patches = arcs.map((arc, i) => {
|
|
11676
|
+
if (arc.a1 - arc.a0 <= 0) return { x: [], y: [] };
|
|
11677
|
+
const color = arc.color ?? palette[i % palette.length];
|
|
11678
|
+
return arcRing(arc.a0, arc.a1, arc.r0, arc.r1, color);
|
|
11679
|
+
});
|
|
11680
|
+
return plot.addPatches({
|
|
11681
|
+
patches,
|
|
11682
|
+
opacity: opts.opacity,
|
|
11683
|
+
name: opts.name,
|
|
11684
|
+
renderType: opts.renderType
|
|
11685
|
+
});
|
|
11686
|
+
}
|
|
11687
|
+
|
|
11688
|
+
// src/charts/gauge.ts
|
|
11689
|
+
var DEG = Math.PI / 180;
|
|
11690
|
+
function arcRing2(a0, a1, r0, r1) {
|
|
11691
|
+
const step = Math.PI / 90;
|
|
11692
|
+
const segs = Math.max(1, Math.ceil(Math.abs(a1 - a0) / step));
|
|
11693
|
+
const x = [];
|
|
11694
|
+
const y = [];
|
|
11695
|
+
for (let s = 0; s <= segs; s++) {
|
|
11696
|
+
const t = a0 + (a1 - a0) * s / segs;
|
|
11697
|
+
x.push(r1 * Math.cos(t));
|
|
11698
|
+
y.push(r1 * Math.sin(t));
|
|
11699
|
+
}
|
|
11700
|
+
for (let s = segs; s >= 0; s--) {
|
|
11701
|
+
const t = a0 + (a1 - a0) * s / segs;
|
|
11702
|
+
x.push(r0 * Math.cos(t));
|
|
11703
|
+
y.push(r0 * Math.sin(t));
|
|
11704
|
+
}
|
|
11705
|
+
return { x, y };
|
|
11706
|
+
}
|
|
11707
|
+
function gaugeLayout(opts) {
|
|
11708
|
+
const min = opts.min ?? 0;
|
|
11709
|
+
const max = opts.max ?? 100;
|
|
11710
|
+
const a0 = (opts.startAngle ?? 200) * DEG;
|
|
11711
|
+
const a1 = (opts.endAngle ?? -20) * DEG;
|
|
11712
|
+
const rOut = opts.radius ?? 1;
|
|
11713
|
+
const rIn = opts.innerRadius ?? 0.7;
|
|
11714
|
+
const span = max - min || 1;
|
|
11715
|
+
const t = Math.min(1, Math.max(0, (opts.value - min) / span));
|
|
11716
|
+
const aVal = a0 + (a1 - a0) * t;
|
|
11717
|
+
const len = rOut * 0.95;
|
|
11718
|
+
const hw = (rOut - rIn) * 0.12;
|
|
11719
|
+
const px = Math.cos(aVal + Math.PI / 2), py = Math.sin(aVal + Math.PI / 2);
|
|
11720
|
+
const needle = {
|
|
11721
|
+
x: [len * Math.cos(aVal), hw * px, -hw * px],
|
|
11722
|
+
y: [len * Math.sin(aVal), hw * py, -hw * py]
|
|
11723
|
+
};
|
|
11724
|
+
return {
|
|
11725
|
+
bg: arcRing2(a0, a1, rIn, rOut),
|
|
11726
|
+
value: arcRing2(a0, aVal, rIn, rOut),
|
|
11727
|
+
needle
|
|
11728
|
+
};
|
|
11729
|
+
}
|
|
11730
|
+
function thresholdColor(value, thresholds, fallback) {
|
|
11731
|
+
if (!thresholds || thresholds.length === 0) return fallback;
|
|
11732
|
+
let color = fallback;
|
|
11733
|
+
let best = -Infinity;
|
|
11734
|
+
for (const th of thresholds) {
|
|
11735
|
+
if (value >= th.value && th.value >= best) {
|
|
11736
|
+
best = th.value;
|
|
11737
|
+
color = th.color;
|
|
11738
|
+
}
|
|
11739
|
+
}
|
|
11740
|
+
return color;
|
|
11741
|
+
}
|
|
11742
|
+
function addGauge(plot, opts) {
|
|
11743
|
+
const track = opts.trackColor ?? "#e5e7eb";
|
|
11744
|
+
const valColor = thresholdColor(opts.value, opts.thresholds, opts.color ?? "#3b82f6");
|
|
11745
|
+
const needleColor = opts.needleColor ?? "#334155";
|
|
11746
|
+
const geo = gaugeLayout({
|
|
11747
|
+
value: opts.value,
|
|
11748
|
+
min: opts.min,
|
|
11749
|
+
max: opts.max,
|
|
11750
|
+
startAngle: opts.startAngle,
|
|
11751
|
+
endAngle: opts.endAngle
|
|
11752
|
+
});
|
|
11753
|
+
const patches = [
|
|
11754
|
+
{ x: geo.bg.x, y: geo.bg.y, color: track },
|
|
11755
|
+
{ x: geo.value.x, y: geo.value.y, color: valColor },
|
|
11756
|
+
{ x: geo.needle.x, y: geo.needle.y, color: needleColor }
|
|
11757
|
+
];
|
|
11758
|
+
if (opts.label !== false) {
|
|
11759
|
+
plot.addAnnotation({
|
|
11760
|
+
type: "label",
|
|
11761
|
+
x: 0,
|
|
11762
|
+
y: -0.15,
|
|
11763
|
+
text: opts.label ?? String(opts.value),
|
|
11764
|
+
align: "center"
|
|
11765
|
+
});
|
|
11766
|
+
}
|
|
11767
|
+
return plot.addPatches({ patches, name: opts.name, renderType: opts.renderType });
|
|
11768
|
+
}
|
|
11769
|
+
|
|
11770
|
+
// src/charts/sankey.ts
|
|
11771
|
+
var DEFAULT_COLORS3 = [
|
|
11772
|
+
"#3b82f6",
|
|
11773
|
+
"#f472b6",
|
|
11774
|
+
"#22d3ee",
|
|
11775
|
+
"#a3e635",
|
|
11776
|
+
"#fbbf24",
|
|
11777
|
+
"#a78bfa",
|
|
11778
|
+
"#34d399",
|
|
11779
|
+
"#fb7185",
|
|
11780
|
+
"#60a5fa",
|
|
11781
|
+
"#f59e0b"
|
|
11782
|
+
];
|
|
11783
|
+
var RIBBON_SAMPLES = 20;
|
|
11784
|
+
function cubic(t, a, b, c, d) {
|
|
11785
|
+
const u = 1 - t;
|
|
11786
|
+
return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
|
|
11787
|
+
}
|
|
11788
|
+
function sankeyLayout(nodes, links, opts = {}) {
|
|
11789
|
+
const n = nodes.length;
|
|
11790
|
+
const [ex0, ex1] = opts.extent?.x ?? [0, 1];
|
|
11791
|
+
const [ey0, ey1] = opts.extent?.y ?? [0, 1];
|
|
11792
|
+
const nodeWidth = opts.nodeWidth ?? 0.02;
|
|
11793
|
+
const yHeight = ey1 - ey0;
|
|
11794
|
+
const pad = (opts.nodePadding ?? 0.02) * yHeight;
|
|
11795
|
+
if (n === 0) return { nodeRects: [], ribbons: [] };
|
|
11796
|
+
const layer = new Array(n).fill(0);
|
|
11797
|
+
const valid = links.filter((l) => l.source >= 0 && l.source < n && l.target >= 0 && l.target < n);
|
|
11798
|
+
for (let iter = 0; iter < n; iter++) {
|
|
11799
|
+
let changed = false;
|
|
11800
|
+
for (const l of valid) {
|
|
11801
|
+
if (layer[l.target] < layer[l.source] + 1) {
|
|
11802
|
+
layer[l.target] = layer[l.source] + 1;
|
|
11803
|
+
changed = true;
|
|
11804
|
+
}
|
|
11805
|
+
}
|
|
11806
|
+
if (!changed) break;
|
|
11807
|
+
}
|
|
11808
|
+
const numLayers = Math.max(...layer) + 1;
|
|
11809
|
+
const inSum = new Array(n).fill(0);
|
|
11810
|
+
const outSum = new Array(n).fill(0);
|
|
11811
|
+
for (const l of valid) {
|
|
11812
|
+
const v = Math.max(0, l.value);
|
|
11813
|
+
outSum[l.source] += v;
|
|
11814
|
+
inSum[l.target] += v;
|
|
11815
|
+
}
|
|
11816
|
+
const flow = new Array(n);
|
|
11817
|
+
for (let i = 0; i < n; i++) flow[i] = Math.max(inSum[i], outSum[i]);
|
|
11818
|
+
const columns = Array.from({ length: numLayers }, () => []);
|
|
11819
|
+
for (let i = 0; i < n; i++) columns[layer[i]].push(i);
|
|
11820
|
+
let k = Infinity;
|
|
11821
|
+
for (const col of columns) {
|
|
11822
|
+
let sum = 0;
|
|
11823
|
+
for (const i of col) sum += flow[i];
|
|
11824
|
+
const avail = yHeight - (col.length - 1) * pad;
|
|
11825
|
+
if (sum > 0 && avail > 0) k = Math.min(k, avail / sum);
|
|
11826
|
+
}
|
|
11827
|
+
if (!isFinite(k) || k <= 0) k = 0;
|
|
11828
|
+
const nodeRects = [];
|
|
11829
|
+
const rectOf = new Array(n).fill(null);
|
|
11830
|
+
for (let li = 0; li < numLayers; li++) {
|
|
11831
|
+
const col = columns[li];
|
|
11832
|
+
const frac = numLayers > 1 ? li / (numLayers - 1) : 0.5;
|
|
11833
|
+
const xc = ex0 + (ex1 - ex0 - nodeWidth) * frac;
|
|
11834
|
+
const x0 = xc;
|
|
11835
|
+
const x1 = xc + nodeWidth;
|
|
11836
|
+
let colH = (col.length - 1) * pad;
|
|
11837
|
+
for (const i of col) colH += k * flow[i];
|
|
11838
|
+
let depth2 = Math.max(0, (yHeight - colH) / 2);
|
|
11839
|
+
for (const i of col) {
|
|
11840
|
+
const h = k * flow[i];
|
|
11841
|
+
const yTop = ey1 - depth2;
|
|
11842
|
+
const yBot = yTop - h;
|
|
11843
|
+
const r = { i, x0, y0: yBot, x1, y1: yTop };
|
|
11844
|
+
nodeRects.push(r);
|
|
11845
|
+
rectOf[i] = r;
|
|
11846
|
+
depth2 += h + pad;
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
const srcOff = new Array(n).fill(0);
|
|
11850
|
+
const tgtOff = new Array(n).fill(0);
|
|
11851
|
+
const ribbons = [];
|
|
11852
|
+
for (let li = 0; li < links.length; li++) {
|
|
11853
|
+
const l = links[li];
|
|
11854
|
+
if (l.source < 0 || l.source >= n || l.target < 0 || l.target >= n) continue;
|
|
11855
|
+
const sr = rectOf[l.source];
|
|
11856
|
+
const tr = rectOf[l.target];
|
|
11857
|
+
const thick = k * Math.max(0, l.value);
|
|
11858
|
+
const sTop = sr.y1 - srcOff[l.source];
|
|
11859
|
+
srcOff[l.source] += thick;
|
|
11860
|
+
const tTop = tr.y1 - tgtOff[l.target];
|
|
11861
|
+
tgtOff[l.target] += thick;
|
|
11862
|
+
const sBot = sTop - thick;
|
|
11863
|
+
const tBot = tTop - thick;
|
|
11864
|
+
const sx = sr.x1;
|
|
11865
|
+
const tx = tr.x0;
|
|
11866
|
+
const mx = (sx + tx) / 2;
|
|
11867
|
+
const x = [];
|
|
11868
|
+
const y = [];
|
|
11869
|
+
for (let s = 0; s <= RIBBON_SAMPLES; s++) {
|
|
11870
|
+
const t = s / RIBBON_SAMPLES;
|
|
11871
|
+
x.push(cubic(t, sx, mx, mx, tx));
|
|
11872
|
+
y.push(cubic(t, sTop, sTop, tTop, tTop));
|
|
11873
|
+
}
|
|
11874
|
+
for (let s = 0; s <= RIBBON_SAMPLES; s++) {
|
|
11875
|
+
const t = s / RIBBON_SAMPLES;
|
|
11876
|
+
x.push(cubic(t, tx, mx, mx, sx));
|
|
11877
|
+
y.push(cubic(t, tBot, tBot, sBot, sBot));
|
|
11878
|
+
}
|
|
11879
|
+
ribbons.push({ link: li, x, y });
|
|
11880
|
+
}
|
|
11881
|
+
return { nodeRects, ribbons };
|
|
11882
|
+
}
|
|
11883
|
+
function nodeColor(nodes, colors, i) {
|
|
11884
|
+
return nodes[i].color ?? colors?.[i] ?? DEFAULT_COLORS3[i % DEFAULT_COLORS3.length];
|
|
11885
|
+
}
|
|
11886
|
+
var RIBBON_ALPHA = 0.5;
|
|
11887
|
+
function addSankey(plot, opts) {
|
|
11888
|
+
const { nodes, links } = opts;
|
|
11889
|
+
const { nodeRects, ribbons } = sankeyLayout(nodes, links, {
|
|
11890
|
+
extent: opts.extent,
|
|
11891
|
+
nodeWidth: opts.nodeWidth,
|
|
11892
|
+
nodePadding: opts.nodePadding
|
|
11893
|
+
});
|
|
11894
|
+
const patches = [];
|
|
11895
|
+
for (const rb of ribbons) {
|
|
11896
|
+
const src = links[rb.link].source;
|
|
11897
|
+
const base = parseColor(nodeColor(nodes, opts.colors, src));
|
|
11898
|
+
const color = [base[0], base[1], base[2], base[3] * RIBBON_ALPHA];
|
|
11899
|
+
patches.push({ x: rb.x, y: rb.y, color });
|
|
11900
|
+
}
|
|
11901
|
+
for (const r of nodeRects) {
|
|
11902
|
+
patches.push({
|
|
11903
|
+
x: [r.x0, r.x1, r.x1, r.x0],
|
|
11904
|
+
y: [r.y0, r.y0, r.y1, r.y1],
|
|
11905
|
+
color: nodeColor(nodes, opts.colors, r.i)
|
|
11906
|
+
});
|
|
11907
|
+
}
|
|
11908
|
+
const layer = plot.addPatches({
|
|
11909
|
+
patches,
|
|
11910
|
+
opacity: opts.opacity ?? 1,
|
|
11911
|
+
name: opts.name,
|
|
11912
|
+
renderType: opts.renderType
|
|
11913
|
+
});
|
|
11914
|
+
if (opts.labels !== false) {
|
|
11915
|
+
for (const r of nodeRects) {
|
|
11916
|
+
plot.addAnnotation({
|
|
11917
|
+
type: "label",
|
|
11918
|
+
x: (r.x0 + r.x1) / 2,
|
|
11919
|
+
y: (r.y0 + r.y1) / 2,
|
|
11920
|
+
text: nodes[r.i].name,
|
|
11921
|
+
align: "center"
|
|
11922
|
+
});
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
return layer;
|
|
11926
|
+
}
|
|
11927
|
+
|
|
11928
|
+
// src/charts/chord.ts
|
|
11929
|
+
var CHORD_PALETTE = [
|
|
11930
|
+
"#4e79a7",
|
|
11931
|
+
"#f28e2b",
|
|
11932
|
+
"#e15759",
|
|
11933
|
+
"#76b7b2",
|
|
11934
|
+
"#59a14f",
|
|
11935
|
+
"#edc948",
|
|
11936
|
+
"#b07aa1",
|
|
11937
|
+
"#ff9da7",
|
|
11938
|
+
"#9c755f",
|
|
11939
|
+
"#bab0ac"
|
|
11940
|
+
];
|
|
11941
|
+
function onCircle(r, a) {
|
|
11942
|
+
return [r * Math.cos(a), r * Math.sin(a)];
|
|
11943
|
+
}
|
|
11944
|
+
function bezier(p0, p1, p2, t) {
|
|
11945
|
+
const u = 1 - t;
|
|
11946
|
+
const a = u * u, b = 2 * u * t, c = t * t;
|
|
11947
|
+
return [a * p0[0] + b * p1[0] + c * p2[0], a * p0[1] + b * p1[1] + c * p2[1]];
|
|
11948
|
+
}
|
|
11949
|
+
function chordLayout(matrix, opts = {}) {
|
|
11950
|
+
const n = matrix.length;
|
|
11951
|
+
const radius = opts.radius ?? 1;
|
|
11952
|
+
const padAngle = opts.padAngle ?? 0.1 * 2 * Math.PI;
|
|
11953
|
+
const arcWidth = (opts.arcWidth ?? 0.06) * radius;
|
|
11954
|
+
const samples = Math.max(2, opts.samples ?? 24);
|
|
11955
|
+
const inner = radius - arcWidth;
|
|
11956
|
+
const rowSums = matrix.map((row) => row.reduce((s, v) => s + Math.max(0, v || 0), 0));
|
|
11957
|
+
const total = rowSums.reduce((s, v) => s + v, 0);
|
|
11958
|
+
if (n === 0 || total <= 0) return { groupArcs: [], ribbons: [] };
|
|
11959
|
+
const gaps = Math.min(padAngle, 0.9 * 2 * Math.PI);
|
|
11960
|
+
const usable = 2 * Math.PI - gaps;
|
|
11961
|
+
const gap = n > 0 ? gaps / n : 0;
|
|
11962
|
+
const groupStart = new Array(n);
|
|
11963
|
+
const groupEnd = new Array(n);
|
|
11964
|
+
const subStart = matrix.map(() => new Array(n).fill(0));
|
|
11965
|
+
const subEnd = matrix.map(() => new Array(n).fill(0));
|
|
11966
|
+
let angle = 0;
|
|
11967
|
+
for (let i = 0; i < n; i++) {
|
|
11968
|
+
groupStart[i] = angle;
|
|
11969
|
+
let a = angle;
|
|
11970
|
+
for (let j = 0; j < n; j++) {
|
|
11971
|
+
const w = Math.max(0, matrix[i][j] || 0);
|
|
11972
|
+
const span = total > 0 ? w / total * usable : 0;
|
|
11973
|
+
subStart[i][j] = a;
|
|
11974
|
+
subEnd[i][j] = a + span;
|
|
11975
|
+
a += span;
|
|
11976
|
+
}
|
|
11977
|
+
groupEnd[i] = a;
|
|
11978
|
+
angle = a + gap;
|
|
11979
|
+
}
|
|
11980
|
+
const groupArcs = [];
|
|
11981
|
+
for (let i = 0; i < n; i++) {
|
|
11982
|
+
const a0 = groupStart[i], a1 = groupEnd[i];
|
|
11983
|
+
if (a1 <= a0) continue;
|
|
11984
|
+
const x = [], y = [];
|
|
11985
|
+
for (let s = 0; s <= samples; s++) {
|
|
11986
|
+
const [px, py] = onCircle(radius, a0 + (a1 - a0) * s / samples);
|
|
11987
|
+
x.push(px);
|
|
11988
|
+
y.push(py);
|
|
11989
|
+
}
|
|
11990
|
+
for (let s = samples; s >= 0; s--) {
|
|
11991
|
+
const [px, py] = onCircle(inner, a0 + (a1 - a0) * s / samples);
|
|
11992
|
+
x.push(px);
|
|
11993
|
+
y.push(py);
|
|
11994
|
+
}
|
|
11995
|
+
groupArcs.push({ i, x, y });
|
|
11996
|
+
}
|
|
11997
|
+
const center = [0, 0];
|
|
11998
|
+
const ribbons = [];
|
|
11999
|
+
for (let i = 0; i < n; i++) {
|
|
12000
|
+
for (let j = i; j < n; j++) {
|
|
12001
|
+
const wij = Math.max(0, matrix[i][j] || 0);
|
|
12002
|
+
const wji = Math.max(0, matrix[j][i] || 0);
|
|
12003
|
+
if (wij <= 0 && wji <= 0) continue;
|
|
12004
|
+
const iA0 = subStart[i][j], iA1 = subEnd[i][j];
|
|
12005
|
+
const jA0 = subStart[j][i], jA1 = subEnd[j][i];
|
|
12006
|
+
const x = [], y = [];
|
|
12007
|
+
for (let s = 0; s <= samples; s++) {
|
|
12008
|
+
const [px, py] = onCircle(inner, iA0 + (iA1 - iA0) * s / samples);
|
|
12009
|
+
x.push(px);
|
|
12010
|
+
y.push(py);
|
|
12011
|
+
}
|
|
12012
|
+
const iEnd = onCircle(inner, iA1);
|
|
12013
|
+
const jStart = onCircle(inner, jA0);
|
|
12014
|
+
for (let s = 1; s <= samples; s++) {
|
|
12015
|
+
const [px, py] = bezier(iEnd, center, jStart, s / samples);
|
|
12016
|
+
x.push(px);
|
|
12017
|
+
y.push(py);
|
|
12018
|
+
}
|
|
12019
|
+
for (let s = 0; s <= samples; s++) {
|
|
12020
|
+
const [px, py] = onCircle(inner, jA0 + (jA1 - jA0) * s / samples);
|
|
12021
|
+
x.push(px);
|
|
12022
|
+
y.push(py);
|
|
12023
|
+
}
|
|
12024
|
+
const jEnd = onCircle(inner, jA1);
|
|
12025
|
+
const iStart = onCircle(inner, iA0);
|
|
12026
|
+
for (let s = 1; s <= samples; s++) {
|
|
12027
|
+
const [px, py] = bezier(jEnd, center, iStart, s / samples);
|
|
12028
|
+
x.push(px);
|
|
12029
|
+
y.push(py);
|
|
12030
|
+
}
|
|
12031
|
+
ribbons.push({ i, j, x, y });
|
|
12032
|
+
}
|
|
12033
|
+
}
|
|
12034
|
+
return { groupArcs, ribbons };
|
|
12035
|
+
}
|
|
12036
|
+
function addChord(plot, opts) {
|
|
12037
|
+
const radius = opts.radius ?? 1;
|
|
12038
|
+
const palette = opts.colors && opts.colors.length ? opts.colors : CHORD_PALETTE;
|
|
12039
|
+
const opacity = opts.opacity ?? 0.65;
|
|
12040
|
+
const { groupArcs, ribbons } = chordLayout(opts.matrix, { radius });
|
|
12041
|
+
const color = (i) => palette[i % palette.length];
|
|
12042
|
+
const patches = [];
|
|
12043
|
+
for (const rb of ribbons) {
|
|
12044
|
+
patches.push({ x: rb.x, y: rb.y, color: withAlpha(color(rb.i), opacity) });
|
|
12045
|
+
}
|
|
12046
|
+
for (const arc of groupArcs) {
|
|
12047
|
+
patches.push({ x: arc.x, y: arc.y, color: color(arc.i) });
|
|
12048
|
+
}
|
|
12049
|
+
const layer = plot.addPatches({
|
|
12050
|
+
patches,
|
|
12051
|
+
name: opts.name,
|
|
12052
|
+
renderType: opts.renderType
|
|
12053
|
+
});
|
|
12054
|
+
const labels = opts.labels;
|
|
12055
|
+
if (labels && labels.length) {
|
|
12056
|
+
const n = opts.matrix.length;
|
|
12057
|
+
const rowSums = opts.matrix.map((row) => row.reduce((s, v) => s + Math.max(0, v || 0), 0));
|
|
12058
|
+
const total = rowSums.reduce((s, v) => s + v, 0);
|
|
12059
|
+
if (total > 0 && n > 0) {
|
|
12060
|
+
const padAngle = 0.1 * 2 * Math.PI;
|
|
12061
|
+
const gaps = Math.min(padAngle, 0.9 * 2 * Math.PI);
|
|
12062
|
+
const usable = 2 * Math.PI - gaps;
|
|
12063
|
+
const gap = gaps / n;
|
|
12064
|
+
let angle = 0;
|
|
12065
|
+
const labelR = radius * 1.08;
|
|
12066
|
+
for (let i = 0; i < n; i++) {
|
|
12067
|
+
const span = rowSums[i] / total * usable;
|
|
12068
|
+
const mid = angle + span / 2;
|
|
12069
|
+
const text = labels[i];
|
|
12070
|
+
if (text != null) {
|
|
12071
|
+
plot.addAnnotation({
|
|
12072
|
+
type: "label",
|
|
12073
|
+
x: labelR * Math.cos(mid),
|
|
12074
|
+
y: labelR * Math.sin(mid),
|
|
12075
|
+
text,
|
|
12076
|
+
align: "center"
|
|
12077
|
+
});
|
|
12078
|
+
}
|
|
12079
|
+
angle += span + gap;
|
|
12080
|
+
}
|
|
12081
|
+
}
|
|
12082
|
+
}
|
|
12083
|
+
return layer;
|
|
12084
|
+
}
|
|
12085
|
+
function withAlpha(color, alpha) {
|
|
12086
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(color);
|
|
12087
|
+
if (hex) {
|
|
12088
|
+
const v = parseInt(hex[1], 16);
|
|
12089
|
+
return `rgba(${v >> 16 & 255}, ${v >> 8 & 255}, ${v & 255}, ${alpha})`;
|
|
12090
|
+
}
|
|
12091
|
+
return color;
|
|
12092
|
+
}
|
|
12093
|
+
|
|
12094
|
+
// src/charts/parallel.ts
|
|
12095
|
+
var PARALLEL_PALETTE = [
|
|
12096
|
+
"#4e79a7",
|
|
12097
|
+
"#f28e2b",
|
|
12098
|
+
"#e15759",
|
|
12099
|
+
"#76b7b2",
|
|
12100
|
+
"#59a14f",
|
|
12101
|
+
"#edc948",
|
|
12102
|
+
"#b07aa1",
|
|
12103
|
+
"#ff9da7",
|
|
12104
|
+
"#9c755f",
|
|
12105
|
+
"#bab0ac"
|
|
12106
|
+
];
|
|
12107
|
+
function parallelLayout(dimensions, rows) {
|
|
12108
|
+
const d = dimensions.length;
|
|
12109
|
+
const mins = new Array(d).fill(Infinity);
|
|
12110
|
+
const maxs = new Array(d).fill(-Infinity);
|
|
12111
|
+
for (const row of rows) {
|
|
12112
|
+
for (let i = 0; i < d; i++) {
|
|
12113
|
+
const v = row[i];
|
|
12114
|
+
if (v == null || !Number.isFinite(v)) continue;
|
|
12115
|
+
if (v < mins[i]) mins[i] = v;
|
|
12116
|
+
if (v > maxs[i]) maxs[i] = v;
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
for (let i = 0; i < d; i++) {
|
|
12120
|
+
if (!Number.isFinite(mins[i])) {
|
|
12121
|
+
mins[i] = 0;
|
|
12122
|
+
maxs[i] = 1;
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12125
|
+
const axes = dimensions.map((dim, i) => ({ dim, x: i, min: mins[i], max: maxs[i] }));
|
|
12126
|
+
const lines = rows.map((row, r) => {
|
|
12127
|
+
const x = new Array(d);
|
|
12128
|
+
const y = new Array(d);
|
|
12129
|
+
for (let i = 0; i < d; i++) {
|
|
12130
|
+
const span = maxs[i] - mins[i];
|
|
12131
|
+
const v = row[i];
|
|
12132
|
+
const norm2 = v == null || !Number.isFinite(v) || span === 0 ? 0.5 : (v - mins[i]) / span;
|
|
12133
|
+
x[i] = i;
|
|
12134
|
+
y[i] = norm2;
|
|
12135
|
+
}
|
|
12136
|
+
return { row: r, x, y };
|
|
12137
|
+
});
|
|
12138
|
+
return { axes, lines };
|
|
12139
|
+
}
|
|
12140
|
+
function addParallelCoordinates(plot, opts) {
|
|
12141
|
+
const palette = opts.colors && opts.colors.length ? opts.colors : PARALLEL_PALETTE;
|
|
12142
|
+
const width = opts.width ?? 1;
|
|
12143
|
+
const opacity = opts.opacity ?? 0.7;
|
|
12144
|
+
const { axes, lines } = parallelLayout(opts.dimensions, opts.rows);
|
|
12145
|
+
let cbMin = Infinity, cbMax = -Infinity;
|
|
12146
|
+
if (opts.colorBy) {
|
|
12147
|
+
for (const v of opts.colorBy) {
|
|
12148
|
+
if (!Number.isFinite(v)) continue;
|
|
12149
|
+
if (v < cbMin) cbMin = v;
|
|
12150
|
+
if (v > cbMax) cbMax = v;
|
|
12151
|
+
}
|
|
12152
|
+
}
|
|
12153
|
+
const cbSpan = cbMax - cbMin;
|
|
12154
|
+
const rowColor = (r) => {
|
|
12155
|
+
if (opts.colorBy && Number.isFinite(cbMin)) {
|
|
12156
|
+
const v = opts.colorBy[r];
|
|
12157
|
+
const t = v == null || !Number.isFinite(v) || cbSpan === 0 ? 0 : (v - cbMin) / cbSpan;
|
|
12158
|
+
const band = Math.min(palette.length - 1, Math.max(0, Math.floor(t * palette.length)));
|
|
12159
|
+
return palette[band];
|
|
12160
|
+
}
|
|
12161
|
+
return palette[r % palette.length];
|
|
12162
|
+
};
|
|
12163
|
+
const layers = lines.map(
|
|
12164
|
+
(ln, r) => plot.addLine({
|
|
12165
|
+
x: ln.x,
|
|
12166
|
+
y: ln.y,
|
|
12167
|
+
color: withAlpha2(rowColor(r), opacity),
|
|
12168
|
+
width,
|
|
12169
|
+
name: r === 0 ? opts.name : void 0,
|
|
12170
|
+
renderType: opts.renderType
|
|
12171
|
+
})
|
|
12172
|
+
);
|
|
12173
|
+
for (const ax of axes) {
|
|
12174
|
+
plot.addAnnotation({ type: "span", dim: "x", value: ax.x });
|
|
12175
|
+
plot.addAnnotation({ type: "label", x: ax.x, y: 1, text: ax.dim, align: "center" });
|
|
12176
|
+
}
|
|
12177
|
+
return { lines: layers };
|
|
12178
|
+
}
|
|
12179
|
+
function withAlpha2(color, alpha) {
|
|
12180
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(color);
|
|
12181
|
+
if (hex) {
|
|
12182
|
+
const v = parseInt(hex[1], 16);
|
|
12183
|
+
return `rgba(${v >> 16 & 255}, ${v >> 8 & 255}, ${v & 255}, ${alpha})`;
|
|
12184
|
+
}
|
|
12185
|
+
return color;
|
|
12186
|
+
}
|
|
10440
12187
|
export {
|
|
10441
12188
|
AreaLayer,
|
|
10442
12189
|
Axis,
|
|
10443
12190
|
Bar3DLayer,
|
|
10444
12191
|
BarLayer,
|
|
10445
12192
|
BoxLayer,
|
|
12193
|
+
CHORD_PALETTE,
|
|
10446
12194
|
CandlestickLayer,
|
|
10447
12195
|
CategoricalScale,
|
|
10448
12196
|
Contour3DLayer,
|
|
10449
12197
|
ContourLayer,
|
|
10450
12198
|
ErrorBarLayer,
|
|
12199
|
+
FUNNEL_PALETTE,
|
|
10451
12200
|
GraphLayer,
|
|
10452
12201
|
HeatmapLayer,
|
|
10453
12202
|
HexbinLayer,
|
|
@@ -10459,6 +12208,7 @@ export {
|
|
|
10459
12208
|
LogScale,
|
|
10460
12209
|
OhlcLayer,
|
|
10461
12210
|
OrdinalTimeScale,
|
|
12211
|
+
PARALLEL_PALETTE,
|
|
10462
12212
|
PatchesLayer,
|
|
10463
12213
|
PieLayer,
|
|
10464
12214
|
Plot,
|
|
@@ -10472,39 +12222,61 @@ export {
|
|
|
10472
12222
|
SurfaceLayer,
|
|
10473
12223
|
TRANSFORM_GLSL,
|
|
10474
12224
|
TRANSFORM_UNIFORMS,
|
|
12225
|
+
TREEMAP_PALETTE,
|
|
10475
12226
|
TimeScale,
|
|
10476
12227
|
VolumeLayer,
|
|
10477
12228
|
addBollinger,
|
|
12229
|
+
addChord,
|
|
10478
12230
|
addDepth,
|
|
12231
|
+
addFunnel,
|
|
12232
|
+
addGauge,
|
|
10479
12233
|
addHeikinAshi,
|
|
12234
|
+
addParallelCoordinates,
|
|
10480
12235
|
addRenko,
|
|
12236
|
+
addSankey,
|
|
12237
|
+
addSunburst,
|
|
12238
|
+
addTreemap,
|
|
10481
12239
|
addVolumeProfile,
|
|
12240
|
+
adx,
|
|
10482
12241
|
atr,
|
|
10483
12242
|
autoTicks,
|
|
10484
12243
|
bollinger,
|
|
10485
12244
|
boxStats,
|
|
10486
12245
|
bufferUsage,
|
|
12246
|
+
canvasToBlob,
|
|
12247
|
+
chordLayout,
|
|
10487
12248
|
colormap,
|
|
10488
12249
|
colormapLUT,
|
|
12250
|
+
copyCanvasToClipboard,
|
|
10489
12251
|
createProgram,
|
|
10490
12252
|
createToolbar,
|
|
10491
12253
|
darkTheme,
|
|
10492
12254
|
defaultFormat,
|
|
10493
12255
|
depth,
|
|
12256
|
+
downloadCanvas,
|
|
10494
12257
|
earcut,
|
|
10495
12258
|
ema,
|
|
10496
12259
|
fft,
|
|
12260
|
+
fibRetracements,
|
|
10497
12261
|
firstFinite,
|
|
10498
12262
|
forceLayout,
|
|
12263
|
+
funnelLayout,
|
|
12264
|
+
gaugeLayout,
|
|
10499
12265
|
heikinAshi,
|
|
10500
12266
|
histogram,
|
|
12267
|
+
ichimoku,
|
|
10501
12268
|
kde,
|
|
12269
|
+
keltner,
|
|
10502
12270
|
lightTheme,
|
|
10503
12271
|
lineBreak,
|
|
10504
12272
|
linkX,
|
|
12273
|
+
lttb,
|
|
10505
12274
|
macd,
|
|
10506
12275
|
makeScale,
|
|
10507
12276
|
marchingCubes,
|
|
12277
|
+
obv,
|
|
12278
|
+
parallelLayout,
|
|
12279
|
+
parseCSV,
|
|
10508
12280
|
parseColor,
|
|
10509
12281
|
pointAndFigure,
|
|
10510
12282
|
quantileSorted,
|
|
@@ -10513,10 +12285,15 @@ export {
|
|
|
10513
12285
|
resolveTicks,
|
|
10514
12286
|
rollingStd,
|
|
10515
12287
|
rsi,
|
|
12288
|
+
sankeyLayout,
|
|
10516
12289
|
setTransformUniforms,
|
|
10517
12290
|
sma,
|
|
10518
12291
|
spectrogram,
|
|
12292
|
+
stochastic,
|
|
12293
|
+
sunburstLayout,
|
|
12294
|
+
superTrend,
|
|
10519
12295
|
toColorCss,
|
|
12296
|
+
treemapLayout,
|
|
10520
12297
|
trueRange,
|
|
10521
12298
|
uniformLocations,
|
|
10522
12299
|
volumeProfile,
|