@opendata-ai/openchart-vanilla 8.6.4 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import {
4
4
  exportPNG,
5
5
  exportSVG,
6
6
  exportSVGWithFonts
7
- } from "./chunk-6B5OBGIO.js";
7
+ } from "./chunk-I73B3DJI.js";
8
8
  import {
9
9
  buildGradientDefs,
10
10
  buildPatternDefs,
@@ -17,12 +17,12 @@ import {
17
17
  resolveMarkFill,
18
18
  resolvePatternFill,
19
19
  stampAnimationVars
20
- } from "./chunk-MDK6AILK.js";
20
+ } from "./chunk-SXTHPXG6.js";
21
21
  import {
22
22
  SVG_NS,
23
23
  createSVGElement,
24
24
  setAttrs
25
- } from "./chunk-5RX6ZQGZ.js";
25
+ } from "./chunk-SDJHUELU.js";
26
26
  import {
27
27
  AnimationScheduler,
28
28
  SpatialIndex,
@@ -3122,21 +3122,40 @@ function applySecondaryOpacity(els, elapsed, enterDelay, enterDuration) {
3122
3122
  }
3123
3123
  }
3124
3124
 
3125
+ // src/dom-helpers.ts
3126
+ function applySrOnlyStyles(el) {
3127
+ el.style.position = "absolute";
3128
+ el.style.width = "1px";
3129
+ el.style.height = "1px";
3130
+ el.style.padding = "0";
3131
+ el.style.margin = "-1px";
3132
+ el.style.overflow = "clip";
3133
+ el.style.clipPath = "inset(50%)";
3134
+ el.style.whiteSpace = "nowrap";
3135
+ el.style.borderWidth = "0";
3136
+ }
3137
+
3125
3138
  // src/you-draw-it.ts
3126
3139
  var MIN_TOUCH_TARGET = 24;
3140
+ var TRAIL_RESOLUTION = 1e3;
3141
+ var START_SLOP_PX = 16;
3127
3142
  function buildLinearPath(points) {
3128
3143
  if (points.length === 0) return "";
3129
3144
  const sorted = [...points].sort((a, b) => a.x - b.x);
3130
3145
  return sorted.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
3131
3146
  }
3147
+ function round2(n) {
3148
+ return Math.round(n * 100) / 100;
3149
+ }
3132
3150
  function createYouDrawIt(options) {
3133
3151
  const { container, onReveal } = options;
3134
3152
  let destroyed = false;
3135
3153
  let revealed = false;
3136
3154
  let config = null;
3137
3155
  let svgEl = null;
3138
- const guessByX = /* @__PURE__ */ new Map();
3156
+ const trail = /* @__PURE__ */ new Map();
3139
3157
  let cleanupPointerEvents = null;
3158
+ let activePointer = null;
3140
3159
  function buildHatchPattern(id, color) {
3141
3160
  const pattern = document.createElementNS(SVG_NS, "pattern");
3142
3161
  pattern.setAttribute("id", id);
@@ -3219,11 +3238,12 @@ function createYouDrawIt(options) {
3219
3238
  guessPath.setAttribute("pointer-events", "none");
3220
3239
  group.appendChild(guessPath);
3221
3240
  if (!revealed) {
3241
+ const overlayX = Math.max(area.x, fromX - START_SLOP_PX);
3222
3242
  const overlay = createSVGElement("rect");
3223
3243
  setAttrs(overlay, {
3224
- x: fromX,
3244
+ x: overlayX,
3225
3245
  y: area.y,
3226
- width: regionWidth,
3246
+ width: regionWidth + (fromX - overlayX),
3227
3247
  height: area.height,
3228
3248
  fill: "transparent"
3229
3249
  });
@@ -3240,97 +3260,146 @@ function createYouDrawIt(options) {
3240
3260
  }
3241
3261
  svg.appendChild(group);
3242
3262
  }
3263
+ function regionWidthOf(cfg) {
3264
+ return Math.max(0, cfg.area.x + cfg.area.width - cfg.fromX);
3265
+ }
3266
+ function pxToIndex(cfg, px) {
3267
+ const w = regionWidthOf(cfg);
3268
+ if (w === 0) return 0;
3269
+ return (px - cfg.fromX) / w * TRAIL_RESOLUTION;
3270
+ }
3271
+ function indexToPx(cfg, index) {
3272
+ return cfg.fromX + index / TRAIL_RESOLUTION * regionWidthOf(cfg);
3273
+ }
3243
3274
  function redrawGuessPath() {
3244
- if (!svgEl) return;
3275
+ if (!svgEl || !config) return;
3245
3276
  const path = svgEl.querySelector("[data-ydi-guess-path]");
3246
3277
  if (!path) return;
3247
- const points = Array.from(guessByX.entries()).map(([x, y]) => ({ x, y }));
3248
- path.setAttribute("d", buildLinearPath(points));
3249
- }
3250
- function snapToNearestSample(px) {
3251
- if (!config || config.samples.length === 0) return null;
3252
- let nearestPx = config.samples[0].px;
3253
- let best = Math.abs(nearestPx - px);
3254
- for (const s of config.samples) {
3255
- const d = Math.abs(s.px - px);
3256
- if (d < best) {
3257
- best = d;
3258
- nearestPx = s.px;
3259
- }
3260
- }
3261
- return nearestPx;
3278
+ const cfg = config;
3279
+ const keys = Array.from(trail.keys()).sort((a, b) => a - b);
3280
+ const d = keys.map((i, n) => {
3281
+ const cmd = n === 0 || i - keys[n - 1] > 1 ? "M" : "L";
3282
+ return `${cmd}${round2(indexToPx(cfg, i))},${round2(dataToPixelY(trail.get(i)))}`;
3283
+ }).join(" ");
3284
+ path.setAttribute("d", d);
3285
+ syncResetButton();
3286
+ }
3287
+ function clampX(px) {
3288
+ if (!config) return px;
3289
+ return Math.min(config.area.x + config.area.width, Math.max(config.fromX, px));
3262
3290
  }
3263
3291
  function clampY(py) {
3264
3292
  if (!config) return py;
3265
3293
  return Math.min(config.area.y + config.area.height, Math.max(config.area.y, py));
3266
3294
  }
3295
+ function paintSegment(x0, y0, x1, y1) {
3296
+ if (!config) return;
3297
+ const i0 = pxToIndex(config, x0);
3298
+ const i1 = pxToIndex(config, x1);
3299
+ const lo = Math.max(0, Math.ceil(Math.min(i0, i1)));
3300
+ const hi = Math.min(TRAIL_RESOLUTION, Math.floor(Math.max(i0, i1)));
3301
+ const span = i1 - i0;
3302
+ for (let i = lo; i <= hi; i++) {
3303
+ const t = span === 0 ? 1 : (i - i0) / span;
3304
+ trail.set(i, pixelYToData(y0 + t * (y1 - y0)));
3305
+ }
3306
+ const end = Math.min(TRAIL_RESOLUTION, Math.max(0, Math.round(i1)));
3307
+ trail.set(end, pixelYToData(y1));
3308
+ }
3267
3309
  function wirePointerEvents(svg) {
3268
3310
  const overlay = svg.querySelector("[data-ydi-overlay]");
3269
3311
  if (!overlay) return () => {
3270
3312
  };
3271
- let dragging = false;
3272
- const toSvgCoords = (clientX, clientY) => {
3313
+ let last = null;
3314
+ if (activePointer !== null) {
3315
+ try {
3316
+ overlay.setPointerCapture?.(activePointer);
3317
+ } catch {
3318
+ activePointer = null;
3319
+ }
3320
+ }
3321
+ const toSvgPoint = (clientX, clientY) => {
3273
3322
  const svgRect = svg.getBoundingClientRect();
3274
3323
  const viewBox = svg.viewBox?.baseVal;
3275
3324
  const scaleX = viewBox?.width && svgRect.width ? viewBox.width / svgRect.width : 1;
3276
3325
  const scaleY = viewBox?.height && svgRect.height ? viewBox.height / svgRect.height : 1;
3277
3326
  return {
3278
- svgX: (clientX - svgRect.left) * scaleX,
3279
- svgY: (clientY - svgRect.top) * scaleY
3327
+ x: clampX((clientX - svgRect.left) * scaleX),
3328
+ y: clampY((clientY - svgRect.top) * scaleY)
3280
3329
  };
3281
3330
  };
3282
- const paintAt = (clientX, clientY) => {
3283
- const { svgX, svgY } = toSvgCoords(clientX, clientY);
3284
- const snapped = snapToNearestSample(svgX);
3285
- if (snapped === null) return;
3286
- guessByX.set(snapped, clampY(svgY));
3287
- redrawGuessPath();
3288
- };
3289
- const handleMouseDown = (e) => {
3290
- const me = e;
3291
- dragging = true;
3292
- paintAt(me.clientX, me.clientY);
3331
+ const nearAnchor = (p) => {
3332
+ if (!config?.anchor) return false;
3333
+ if (p.x - config.fromX <= START_SLOP_PX) return true;
3334
+ return config.samples.length > 0 && p.x <= config.samples[0].px;
3293
3335
  };
3294
- const handleMouseMove = (e) => {
3295
- if (!dragging) return;
3296
- const me = e;
3297
- paintAt(me.clientX, me.clientY);
3336
+ const endStroke = () => {
3337
+ activePointer = null;
3338
+ last = null;
3298
3339
  };
3299
- const handleMouseUp = () => {
3300
- dragging = false;
3340
+ const handleDown = (e) => {
3341
+ const pe = e;
3342
+ if (pe.button !== 0) return;
3343
+ if (activePointer !== null && activePointer !== pe.pointerId && overlay.hasPointerCapture?.(activePointer)) {
3344
+ return;
3345
+ }
3346
+ if (pe.cancelable) pe.preventDefault();
3347
+ activePointer = pe.pointerId;
3348
+ overlay.setPointerCapture?.(pe.pointerId);
3349
+ live.textContent = "";
3350
+ const p = toSvgPoint(pe.clientX, pe.clientY);
3351
+ const anchor = config?.anchor;
3352
+ if (trail.size === 0 && anchor && nearAnchor(p)) {
3353
+ trail.set(0, pixelYToData(anchor.y));
3354
+ if (p.x > anchor.x) {
3355
+ paintSegment(anchor.x, anchor.y, p.x, p.y);
3356
+ last = p;
3357
+ } else {
3358
+ last = { x: anchor.x, y: anchor.y };
3359
+ }
3360
+ } else {
3361
+ paintSegment(p.x, p.y, p.x, p.y);
3362
+ last = p;
3363
+ }
3364
+ redrawGuessPath();
3301
3365
  };
3302
- const handleTouchStart = (e) => {
3303
- const te = e;
3304
- if (te.touches.length === 0) return;
3305
- if (te.cancelable) te.preventDefault();
3306
- dragging = true;
3307
- paintAt(te.touches[0].clientX, te.touches[0].clientY);
3366
+ const handleMove = (e) => {
3367
+ const pe = e;
3368
+ if (activePointer !== pe.pointerId) return;
3369
+ if (pe.buttons === 0) {
3370
+ endStroke();
3371
+ return;
3372
+ }
3373
+ if (pe.cancelable) pe.preventDefault();
3374
+ const coalesced = pe.getCoalescedEvents?.() ?? [];
3375
+ const events = coalesced.length > 0 ? coalesced : [pe];
3376
+ for (const ev of events) {
3377
+ const p = toSvgPoint(ev.clientX, ev.clientY);
3378
+ const from = last ?? p;
3379
+ paintSegment(from.x, from.y, p.x, p.y);
3380
+ last = p;
3381
+ }
3382
+ redrawGuessPath();
3308
3383
  };
3309
- const handleTouchMove = (e) => {
3310
- if (!dragging) return;
3311
- const te = e;
3312
- if (te.touches.length === 0) return;
3313
- if (te.cancelable) te.preventDefault();
3314
- paintAt(te.touches[0].clientX, te.touches[0].clientY);
3384
+ const handleUp = (e) => {
3385
+ const pe = e;
3386
+ if (activePointer !== pe.pointerId) return;
3387
+ endStroke();
3315
3388
  };
3316
- const handleTouchEnd = () => {
3317
- dragging = false;
3389
+ const handleLostCapture = () => {
3390
+ endStroke();
3318
3391
  };
3319
- overlay.addEventListener("mousedown", handleMouseDown);
3320
- document.addEventListener("mousemove", handleMouseMove);
3321
- document.addEventListener("mouseup", handleMouseUp);
3322
- overlay.addEventListener("touchstart", handleTouchStart, { passive: false });
3323
- overlay.addEventListener("touchmove", handleTouchMove, { passive: false });
3324
- overlay.addEventListener("touchend", handleTouchEnd);
3325
- overlay.addEventListener("touchcancel", handleTouchEnd);
3392
+ overlay.addEventListener("pointerdown", handleDown);
3393
+ overlay.addEventListener("pointermove", handleMove);
3394
+ overlay.addEventListener("pointerup", handleUp);
3395
+ overlay.addEventListener("pointercancel", handleUp);
3396
+ overlay.addEventListener("lostpointercapture", handleLostCapture);
3326
3397
  return () => {
3327
- overlay.removeEventListener("mousedown", handleMouseDown);
3328
- document.removeEventListener("mousemove", handleMouseMove);
3329
- document.removeEventListener("mouseup", handleMouseUp);
3330
- overlay.removeEventListener("touchstart", handleTouchStart);
3331
- overlay.removeEventListener("touchmove", handleTouchMove);
3332
- overlay.removeEventListener("touchend", handleTouchEnd);
3333
- overlay.removeEventListener("touchcancel", handleTouchEnd);
3398
+ overlay.removeEventListener("pointerdown", handleDown);
3399
+ overlay.removeEventListener("pointermove", handleMove);
3400
+ overlay.removeEventListener("pointerup", handleUp);
3401
+ overlay.removeEventListener("pointercancel", handleUp);
3402
+ overlay.removeEventListener("lostpointercapture", handleLostCapture);
3334
3403
  };
3335
3404
  }
3336
3405
  const root = document.createElement("div");
@@ -3339,10 +3408,21 @@ function createYouDrawIt(options) {
3339
3408
  root.style.position = "absolute";
3340
3409
  const prompt = document.createElement("div");
3341
3410
  prompt.className = "oc-ydi-prompt";
3411
+ const resetButton = document.createElement("button");
3412
+ resetButton.type = "button";
3413
+ resetButton.className = "oc-ydi-reset-button";
3414
+ resetButton.hidden = true;
3342
3415
  const revealButton = document.createElement("button");
3343
3416
  revealButton.type = "button";
3344
3417
  revealButton.className = "oc-ydi-reveal-button";
3345
- root.append(prompt, revealButton);
3418
+ const actions = document.createElement("div");
3419
+ actions.className = "oc-ydi-actions";
3420
+ actions.append(resetButton, revealButton);
3421
+ const live = document.createElement("span");
3422
+ live.className = "oc-sr-only";
3423
+ applySrOnlyStyles(live);
3424
+ live.setAttribute("aria-live", "polite");
3425
+ root.append(prompt, actions, live);
3346
3426
  container.style.position = container.style.position || "relative";
3347
3427
  container.appendChild(root);
3348
3428
  function reduceMotion() {
@@ -3360,14 +3440,48 @@ function createYouDrawIt(options) {
3360
3440
  const t = (py - inv.topPixel) / span;
3361
3441
  return inv.topData + t * (inv.bottomData - inv.topData);
3362
3442
  }
3443
+ function dataToPixelY(value) {
3444
+ if (!config) return value;
3445
+ const inv = config.yInvert;
3446
+ if (!inv) {
3447
+ const area = config.area;
3448
+ return area.y + (1 - value) * area.height;
3449
+ }
3450
+ const dataSpan = inv.bottomData - inv.topData;
3451
+ if (dataSpan === 0) return inv.topPixel;
3452
+ const t = (value - inv.topData) / dataSpan;
3453
+ return inv.topPixel + t * (inv.bottomPixel - inv.topPixel);
3454
+ }
3363
3455
  function getGuessData() {
3364
- if (!config) return [];
3365
- const valueByPx = new Map(config.samples.map((s) => [s.px, s.xValue]));
3366
- return Array.from(guessByX.entries()).sort((a, b) => a[0] - b[0]).map(([px, py]) => ({ x: valueByPx.get(px) ?? px, y: pixelYToData(py) }));
3456
+ if (!config || trail.size === 0) return [];
3457
+ const cfg = config;
3458
+ const keys = Array.from(trail.keys()).sort((a, b) => a - b);
3459
+ const first = keys[0];
3460
+ const lastKey = keys[keys.length - 1];
3461
+ const out = [];
3462
+ for (const sample of cfg.samples) {
3463
+ const i = pxToIndex(cfg, sample.px);
3464
+ if (i < first - 0.5 || i > lastKey + 0.5) continue;
3465
+ let hiPos = keys.findIndex((k) => k >= i);
3466
+ if (hiPos === -1) hiPos = keys.length - 1;
3467
+ const loPos = hiPos > 0 && keys[hiPos] > i ? hiPos - 1 : hiPos;
3468
+ const k0 = keys[loPos];
3469
+ const k1 = keys[hiPos];
3470
+ if (k1 - k0 > 1) continue;
3471
+ const v0 = trail.get(k0);
3472
+ const v1 = trail.get(k1);
3473
+ const t = k1 === k0 ? 0 : (i - k0) / (k1 - k0);
3474
+ out.push({ x: sample.xValue, y: v0 + t * (v1 - v0) });
3475
+ }
3476
+ return out;
3477
+ }
3478
+ function syncResetButton() {
3479
+ resetButton.hidden = revealed || trail.size === 0;
3367
3480
  }
3368
3481
  function doReveal() {
3369
3482
  if (destroyed || revealed || !svgEl || !config) return;
3370
3483
  revealed = true;
3484
+ activePointer = null;
3371
3485
  const clipRect = svgEl.querySelector("[data-ydi-clip-rect]");
3372
3486
  if (clipRect) {
3373
3487
  if (reduceMotion()) {
@@ -3386,9 +3500,30 @@ function createYouDrawIt(options) {
3386
3500
  root.classList.add("oc-ydi-revealed");
3387
3501
  prompt.textContent = "";
3388
3502
  revealButton.disabled = true;
3503
+ syncResetButton();
3389
3504
  onReveal?.(getGuessData());
3390
3505
  }
3506
+ function doReset() {
3507
+ if (destroyed) return;
3508
+ revealed = false;
3509
+ activePointer = null;
3510
+ trail.clear();
3511
+ revealButton.disabled = false;
3512
+ root.classList.remove("oc-ydi-revealed");
3513
+ syncResetButton();
3514
+ if (config && svgEl) {
3515
+ render(config, svgEl);
3516
+ prompt.textContent = config.prompt;
3517
+ cleanupPointerEvents?.();
3518
+ cleanupPointerEvents = wirePointerEvents(svgEl);
3519
+ }
3520
+ }
3391
3521
  revealButton.addEventListener("click", doReveal);
3522
+ resetButton.addEventListener("click", () => {
3523
+ doReset();
3524
+ revealButton.focus();
3525
+ live.textContent = "Drawing cleared";
3526
+ });
3392
3527
  return {
3393
3528
  update(cfg, svg) {
3394
3529
  if (destroyed) return;
@@ -3403,6 +3538,9 @@ function createYouDrawIt(options) {
3403
3538
  revealButton.setAttribute("aria-label", cfg.revealLabel);
3404
3539
  revealButton.style.minHeight = `${MIN_TOUCH_TARGET}px`;
3405
3540
  revealButton.disabled = revealed;
3541
+ resetButton.textContent = cfg.resetLabel;
3542
+ resetButton.style.minHeight = `${MIN_TOUCH_TARGET}px`;
3543
+ syncResetButton();
3406
3544
  if (revealed) root.classList.add("oc-ydi-revealed");
3407
3545
  else root.classList.remove("oc-ydi-revealed");
3408
3546
  const viewBox = svg.viewBox?.baseVal;
@@ -3421,6 +3559,7 @@ function createYouDrawIt(options) {
3421
3559
  }
3422
3560
  },
3423
3561
  hide() {
3562
+ activePointer = null;
3424
3563
  root.style.display = "none";
3425
3564
  svgEl?.querySelector("[data-you-draw-it]")?.remove();
3426
3565
  cleanupPointerEvents?.();
@@ -3430,17 +3569,7 @@ function createYouDrawIt(options) {
3430
3569
  doReveal();
3431
3570
  },
3432
3571
  reset() {
3433
- if (destroyed) return;
3434
- revealed = false;
3435
- guessByX.clear();
3436
- revealButton.disabled = false;
3437
- root.classList.remove("oc-ydi-revealed");
3438
- if (config && svgEl) {
3439
- render(config, svgEl);
3440
- prompt.textContent = config.prompt;
3441
- cleanupPointerEvents?.();
3442
- cleanupPointerEvents = wirePointerEvents(svgEl);
3443
- }
3572
+ doReset();
3444
3573
  },
3445
3574
  get isRevealed() {
3446
3575
  return revealed;
@@ -4843,21 +4972,6 @@ function wireLegendInteraction(svg, _layout, toggleSeries, onLegendToggle, onEdi
4843
4972
 
4844
4973
  // src/interactions/selection.ts
4845
4974
  import { elementRef as elementRef2 } from "@opendata-ai/openchart-core";
4846
-
4847
- // src/dom-helpers.ts
4848
- function applySrOnlyStyles(el) {
4849
- el.style.position = "absolute";
4850
- el.style.width = "1px";
4851
- el.style.height = "1px";
4852
- el.style.padding = "0";
4853
- el.style.margin = "-1px";
4854
- el.style.overflow = "clip";
4855
- el.style.clipPath = "inset(50%)";
4856
- el.style.whiteSpace = "nowrap";
4857
- el.style.borderWidth = "0";
4858
- }
4859
-
4860
- // src/interactions/selection.ts
4861
4975
  function findElementByRef(svg, ref) {
4862
4976
  switch (ref.type) {
4863
4977
  case "annotation": {
@@ -7994,4 +8108,4 @@ export {
7994
8108
  scrubCamera,
7995
8109
  createGeoMap
7996
8110
  };
7997
- //# sourceMappingURL=chunk-W3WPGHYV.js.map
8111
+ //# sourceMappingURL=chunk-5O624XON.js.map