@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/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 Node = class {
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 Node(a.i, a.x, a.y);
3607
- const b2 = new Node(b.i, b.x, b.y);
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 Node(i, x, y);
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
- key: (d) => d.getFullYear() * 12 + d.getMonth(),
5163
- label: (d) => d.getMonth() === 0 ? `${d.getFullYear()}` : MONTHS[d.getMonth()]
5164
- },
5165
- { key: dayKey, label: (d) => `${d.getDate()} ${MONTHS[d.getMonth()]}` },
5166
- { key: (d) => dayKey(d) * 100 + d.getHours(), label: hhmm },
5167
- { key: (d) => (dayKey(d) * 100 + d.getHours()) * 100 + d.getMinutes(), label: hhmm }
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 boundaries = (lvl) => {
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 best = null;
5212
+ let chosen = null;
5213
+ let bestScore = Infinity;
5182
5214
  for (const lvl of levels) {
5183
- const b = boundaries(lvl);
5215
+ const b = periodStarts(lvl);
5184
5216
  if (b.length === 0) continue;
5185
- best = { lvl, b };
5186
- if (b.length >= target) break;
5187
- }
5188
- let idxs;
5189
- let label;
5190
- if (best) {
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
- } else {
5198
- idxs = [];
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) idxs.push(i);
5201
- label = (d) => `${d.getDate()} ${MONTHS[d.getMonth()]} ${hhmm(d)}`;
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
- return idxs.map((i) => ({ value: i, label: label(new Date(this.times[i])) }));
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
- for (const a of this.annotations) {
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
- } else {
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 ?? defaultFormat;
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.updateCursor();
6434
- if (this.hoverEnabled) this.setHover({ x: px, y: py });
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", () => this.setHover(null));
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,