@opendata-ai/openchart-vanilla 7.11.0 → 8.0.0-rc.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.
Files changed (79) hide show
  1. package/dist/{chunk-EQ6DYFO7.js → chunk-5EOV5JQZ.js} +431 -538
  2. package/dist/chunk-5EOV5JQZ.js.map +1 -0
  3. package/dist/chunk-ENXSUXC5.js +6513 -0
  4. package/dist/chunk-ENXSUXC5.js.map +1 -0
  5. package/dist/chunk-W34KN7FD.js +298 -0
  6. package/dist/chunk-W34KN7FD.js.map +1 -0
  7. package/dist/index.d.ts +110 -116
  8. package/dist/index.js +189 -4408
  9. package/dist/index.js.map +1 -1
  10. package/dist/mount-CUKQLeqD.d.ts +207 -0
  11. package/dist/static.js +16 -11
  12. package/dist/static.js.map +1 -1
  13. package/dist/story/index.d.ts +277 -0
  14. package/dist/story/index.js +537 -0
  15. package/dist/story/index.js.map +1 -0
  16. package/dist/styles.css +2 -1
  17. package/dist/styles.css.map +1 -0
  18. package/package.json +9 -3
  19. package/src/__test-fixtures__/specs.ts +1 -1
  20. package/src/__tests__/annotations-render.test.ts +201 -0
  21. package/src/__tests__/continuous-legend.test.ts +91 -0
  22. package/src/__tests__/edit-events.test.ts +155 -0
  23. package/src/__tests__/map.test.ts +293 -0
  24. package/src/__tests__/mount.test.ts +26 -0
  25. package/src/__tests__/pattern-fills.test.ts +105 -0
  26. package/src/__tests__/series-search.test.ts +311 -0
  27. package/src/__tests__/table-mount.test.ts +34 -0
  28. package/src/__tests__/tooltip.test.ts +26 -0
  29. package/src/__tests__/transition.test.ts +196 -11
  30. package/src/__tests__/you-draw-it.test.ts +273 -0
  31. package/src/animation-vars.ts +42 -0
  32. package/src/barlist-renderer.ts +2 -2
  33. package/src/dom-helpers.ts +17 -0
  34. package/src/export.ts +33 -16
  35. package/src/index.ts +7 -0
  36. package/src/interactions/editing-drags.ts +9 -6
  37. package/src/interactions/legend-interaction.ts +9 -1
  38. package/src/interactions/selection.ts +12 -3
  39. package/src/map-camera.ts +88 -0
  40. package/src/map-mount.ts +701 -0
  41. package/src/map-renderer.ts +338 -0
  42. package/src/map-transition.ts +83 -0
  43. package/src/mount.ts +212 -26
  44. package/src/pattern-utils.ts +110 -0
  45. package/src/renderers/annotations.ts +162 -117
  46. package/src/renderers/axes.ts +13 -6
  47. package/src/renderers/brand.ts +7 -2
  48. package/src/renderers/chrome.ts +32 -10
  49. package/src/renderers/legend.ts +171 -2
  50. package/src/renderers/marks.ts +33 -5
  51. package/src/sankey-renderer.ts +7 -11
  52. package/src/series-search.ts +392 -0
  53. package/src/static.ts +13 -8
  54. package/src/story/__tests__/camera-math.test.ts +145 -0
  55. package/src/story/__tests__/create-chart-story.test.ts +317 -0
  56. package/src/story/__tests__/progress-math.test.ts +95 -0
  57. package/src/story/__tests__/scroll-driver.test.ts +130 -0
  58. package/src/story/__tests__/tween.test.ts +159 -0
  59. package/src/story/camera-math.ts +125 -0
  60. package/src/story/create-chart-story.ts +353 -0
  61. package/src/story/crossfade.ts +70 -0
  62. package/src/story/index.ts +39 -0
  63. package/src/story/progress-math.ts +85 -0
  64. package/src/story/resolve-camera-target.ts +119 -0
  65. package/src/story/scroll-driver.ts +206 -0
  66. package/src/story/story-camera.ts +49 -0
  67. package/src/story/tween.ts +112 -0
  68. package/src/story/types.ts +76 -0
  69. package/src/svg-renderer.ts +60 -29
  70. package/src/table-mount.ts +12 -3
  71. package/src/table-renderer.ts +9 -28
  72. package/src/text-edit-overlay.ts +1 -1
  73. package/src/theme-tokens.ts +4 -4
  74. package/src/tilemap-mount.ts +27 -6
  75. package/src/tilemap-renderer.ts +30 -88
  76. package/src/tooltip.ts +11 -0
  77. package/src/transition.ts +376 -33
  78. package/src/you-draw-it.ts +480 -0
  79. package/dist/chunk-EQ6DYFO7.js.map +0 -1
@@ -1,12 +1,3 @@
1
- // src/svg-ids.ts
2
- var counter = 0;
3
- function nextSvgId(prefix) {
4
- return `${prefix}-${counter++}`;
5
- }
6
- function resetSvgIdCounter() {
7
- counter = 0;
8
- }
9
-
10
1
  // src/renderers/svg-dom.ts
11
2
  var SVG_NS = "http://www.w3.org/2000/svg";
12
3
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -35,6 +26,15 @@ function applyTextStyle(el, style) {
35
26
  }
36
27
  }
37
28
 
29
+ // src/svg-ids.ts
30
+ var counter = 0;
31
+ function nextSvgId(prefix) {
32
+ return `${prefix}-${counter++}`;
33
+ }
34
+ function resetSvgIdCounter() {
35
+ counter = 0;
36
+ }
37
+
38
38
  // src/gradient-utils.ts
39
39
  import { isGradientDef } from "@opendata-ai/openchart-core";
40
40
  var SVG_NS2 = "http://www.w3.org/2000/svg";
@@ -116,16 +116,79 @@ function resolveMarkFill(fill, gradientMap) {
116
116
  return id ? `url(#${id})` : "#000000";
117
117
  }
118
118
 
119
+ // src/pattern-utils.ts
120
+ var SVG_NS3 = "http://www.w3.org/2000/svg";
121
+ var TILE = 6;
122
+ var LINE_OPACITY = 0.55;
123
+ function patternKey(p) {
124
+ return `${p.type}|${p.base}|${p.line}`;
125
+ }
126
+ function createPatternElement(p, id) {
127
+ const el = document.createElementNS(SVG_NS3, "pattern");
128
+ el.setAttribute("id", id);
129
+ el.setAttribute("patternUnits", "userSpaceOnUse");
130
+ el.setAttribute("width", String(TILE));
131
+ el.setAttribute("height", String(TILE));
132
+ const bg = document.createElementNS(SVG_NS3, "rect");
133
+ bg.setAttribute("width", String(TILE));
134
+ bg.setAttribute("height", String(TILE));
135
+ bg.setAttribute("fill", p.base);
136
+ el.appendChild(bg);
137
+ if (p.type === "dot") {
138
+ const dot = document.createElementNS(SVG_NS3, "circle");
139
+ dot.setAttribute("cx", String(TILE / 2));
140
+ dot.setAttribute("cy", String(TILE / 2));
141
+ dot.setAttribute("r", "1.2");
142
+ dot.setAttribute("fill", p.line);
143
+ dot.setAttribute("fill-opacity", String(LINE_OPACITY));
144
+ el.appendChild(dot);
145
+ return el;
146
+ }
147
+ const d = p.type === "diagonal" ? `M0,${TILE} l${TILE},-${TILE} M-1.5,1.5 l3,-3 M${TILE - 1.5},${TILE + 1.5} l3,-3` : p.type === "crosshatch" ? `M0,0 l${TILE},${TILE} M${TILE},0 l-${TILE},${TILE}` : `M${TILE / 2},0 V${TILE}`;
148
+ const path = document.createElementNS(SVG_NS3, "path");
149
+ path.setAttribute("d", d);
150
+ path.setAttribute("stroke", p.line);
151
+ path.setAttribute("stroke-width", "1");
152
+ path.setAttribute("stroke-opacity", String(LINE_OPACITY));
153
+ el.appendChild(path);
154
+ return el;
155
+ }
156
+ function buildPatternDefs(marks, defs) {
157
+ const map = /* @__PURE__ */ new Map();
158
+ for (const mark of marks) {
159
+ const pattern = mark.pattern;
160
+ if (!pattern) continue;
161
+ const key = patternKey(pattern);
162
+ if (!map.has(key)) {
163
+ const id = nextSvgId("oc-pattern");
164
+ defs.appendChild(createPatternElement(pattern, id));
165
+ map.set(key, id);
166
+ }
167
+ }
168
+ return map;
169
+ }
170
+ function resolvePatternFill(pattern, patternMap) {
171
+ const id = patternMap.get(patternKey(pattern));
172
+ return id ? `url(#${id})` : pattern.base;
173
+ }
174
+
119
175
  // src/renderers/marks.ts
120
176
  var currentAnimation;
121
177
  var currentGradientMap = /* @__PURE__ */ new Map();
178
+ var currentPatternMap = /* @__PURE__ */ new Map();
122
179
  function setMarkRenderState(state) {
123
180
  currentAnimation = state.animation;
124
181
  currentGradientMap = state.gradientMap;
182
+ currentPatternMap = state.patternMap ?? /* @__PURE__ */ new Map();
125
183
  }
126
184
  function resetMarkRenderState() {
127
185
  currentAnimation = void 0;
128
186
  currentGradientMap = /* @__PURE__ */ new Map();
187
+ currentPatternMap = /* @__PURE__ */ new Map();
188
+ }
189
+ function resolveFillOrPattern(fill, pattern) {
190
+ if (pattern) return resolvePatternFill(pattern, currentPatternMap);
191
+ return resolveMarkFill(fill, currentGradientMap);
129
192
  }
130
193
  function stampAnimationAttrs(el, mark, fallbackIndex) {
131
194
  if (mark.key) {
@@ -198,7 +261,7 @@ function renderAreaMark(mark, index) {
198
261
  const fill = createSVGElement("path");
199
262
  setAttrs(fill, {
200
263
  d: mark.path,
201
- fill: resolveMarkFill(mark.fill, currentGradientMap),
264
+ fill: resolveFillOrPattern(mark.fill, mark.pattern),
202
265
  "fill-opacity": mark.fillOpacity,
203
266
  stroke: "none"
204
267
  });
@@ -261,7 +324,7 @@ function renderRectMark(mark, index) {
261
324
  setAttrs(shapeEl, { rx: mark.cornerRadius, ry: mark.cornerRadius });
262
325
  }
263
326
  }
264
- shapeEl.setAttribute("fill", String(resolveMarkFill(mark.fill, currentGradientMap)));
327
+ shapeEl.setAttribute("fill", resolveFillOrPattern(mark.fill, mark.pattern));
265
328
  if (mark.stroke) {
266
329
  shapeEl.setAttribute("stroke", mark.stroke);
267
330
  }
@@ -280,7 +343,7 @@ function renderArcMark(mark, index) {
280
343
  const path = createSVGElement("path");
281
344
  setAttrs(path, {
282
345
  d: mark.path,
283
- fill: resolveMarkFill(mark.fill, currentGradientMap),
346
+ fill: resolveFillOrPattern(mark.fill, mark.pattern),
284
347
  stroke: mark.stroke,
285
348
  "stroke-width": mark.strokeWidth
286
349
  });
@@ -327,6 +390,9 @@ function renderTextMark(mark, index) {
327
390
  "font-size": mark.fontSize,
328
391
  "text-anchor": mark.textAnchor
329
392
  });
393
+ if (mark.dominantBaseline) {
394
+ text.setAttribute("dominant-baseline", mark.dominantBaseline);
395
+ }
330
396
  text.style.setProperty("fill", mark.fill);
331
397
  if (mark.fontWeight) {
332
398
  text.setAttribute("font-weight", String(mark.fontWeight));
@@ -478,53 +544,99 @@ function renderMarks(parent, layout) {
478
544
  // src/svg-renderer.ts
479
545
  import { clampStaggerDelay } from "@opendata-ai/openchart-engine";
480
546
 
547
+ // src/animation-vars.ts
548
+ var EASE_VAR_MAP = {
549
+ smooth: "var(--oc-ease-smooth)",
550
+ snappy: "var(--oc-ease-snappy)"
551
+ };
552
+ function stampAnimationVars(el, v) {
553
+ el.style.setProperty("--oc-animation-duration", `${v.duration}ms`);
554
+ el.style.setProperty("--oc-animation-stagger", `${v.stagger}ms`);
555
+ if (v.annotationDelay !== void 0) {
556
+ el.style.setProperty("--oc-annotation-delay", `${v.annotationDelay}ms`);
557
+ }
558
+ if (v.ease !== void 0) {
559
+ el.style.setProperty("--oc-animation-ease", EASE_VAR_MAP[v.ease] || EASE_VAR_MAP.smooth);
560
+ }
561
+ if (v.stackSegmentDuration !== void 0) {
562
+ el.style.setProperty("--oc-stack-segment-duration", `${v.stackSegmentDuration}ms`);
563
+ }
564
+ }
565
+
481
566
  // src/renderers/annotations.ts
482
- function renderCurvedArrow(parent, from, to, stroke) {
483
- const pad = 6;
484
- const tipY = to.y - pad;
485
- const dy = tipY - from.y;
486
- const dist = Math.sqrt((to.x - from.x) ** 2 + dy ** 2) || 1;
487
- const arrowLen = 8;
488
- const arrowWidth = 4;
489
- const bulge = Math.max(dist * 0.4, 35);
490
- const cp1x = from.x + bulge;
491
- const cp1y = from.y + dy * 0.35;
492
- const cp2x = to.x;
493
- const cp2y = tipY - Math.abs(dy) * 0.25;
494
- const tx = to.x - cp2x;
495
- const ty = tipY - cp2y;
567
+ import {
568
+ ARROWHEAD_LENGTH,
569
+ BOLD_SPAN_FONT_WEIGHT,
570
+ computeArrowheadPoints,
571
+ DEFAULT_ANNOTATION_FONT_SIZE,
572
+ DEFAULT_LINE_HEIGHT,
573
+ parseAnnotationSpans
574
+ } from "@opendata-ai/openchart-engine";
575
+ var CONNECTOR_STROKE_WIDTH = 1.25;
576
+ function renderArrowhead(parent, tipX, tipY, tangentX, tangentY, stroke) {
577
+ const head = computeArrowheadPoints(tipX, tipY, tangentX, tangentY);
578
+ const arrow = createSVGElement("polyline");
579
+ arrow.setAttribute("class", "oc-annotation-connector oc-annotation-arrowhead");
580
+ setAttrs(arrow, {
581
+ points: [
582
+ `${head.baseLeft.x},${head.baseLeft.y}`,
583
+ `${head.tip.x},${head.tip.y}`,
584
+ `${head.baseRight.x},${head.baseRight.y}`
585
+ ].join(" "),
586
+ fill: "none",
587
+ stroke,
588
+ "stroke-width": CONNECTOR_STROKE_WIDTH,
589
+ "stroke-linecap": "round",
590
+ "stroke-linejoin": "round"
591
+ });
592
+ parent.appendChild(arrow);
593
+ }
594
+ function renderCurvedConnector(parent, from, to, stroke, showArrow, exit) {
595
+ const dx = to.x - from.x;
596
+ const dy = to.y - from.y;
597
+ const vertical = exit ? exit === "vertical" : Math.abs(dy) >= Math.abs(dx);
598
+ const cpx = from.x + dx * (vertical ? 0.2 : 0.6);
599
+ const cpy = from.y + dy * (vertical ? 0.6 : 0.2);
600
+ const tx = to.x - cpx;
601
+ const ty = to.y - cpy;
496
602
  const tLen = Math.sqrt(tx * tx + ty * ty) || 1;
497
- const ux = tx / tLen;
498
- const uy = ty / tLen;
499
- const baseX = to.x - ux * arrowLen;
500
- const baseY = tipY - uy * arrowLen;
603
+ const endX = showArrow ? to.x - tx / tLen * ARROWHEAD_LENGTH : to.x;
604
+ const endY = showArrow ? to.y - ty / tLen * ARROWHEAD_LENGTH : to.y;
501
605
  const path = createSVGElement("path");
502
606
  path.setAttribute("class", "oc-annotation-connector");
503
607
  setAttrs(path, {
504
- d: `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${baseX} ${baseY}`,
608
+ d: `M ${from.x} ${from.y} Q ${cpx} ${cpy} ${endX} ${endY}`,
505
609
  fill: "none",
506
610
  stroke,
507
- "stroke-width": 1.5
611
+ "stroke-width": CONNECTOR_STROKE_WIDTH,
612
+ "stroke-linecap": "round"
508
613
  });
509
614
  parent.appendChild(path);
510
- const px = -uy;
511
- const py = ux;
512
- const arrow = createSVGElement("polygon");
513
- arrow.setAttribute("class", "oc-annotation-connector");
514
- setAttrs(arrow, {
515
- points: [
516
- `${to.x},${tipY}`,
517
- `${baseX + px * arrowWidth},${baseY + py * arrowWidth}`,
518
- `${baseX - px * arrowWidth},${baseY - py * arrowWidth}`
519
- ].join(" "),
520
- fill: stroke
521
- });
522
- parent.appendChild(arrow);
615
+ if (showArrow) {
616
+ renderArrowhead(parent, to.x, to.y, tx, ty, stroke);
617
+ }
618
+ }
619
+ function fillRichText(text, content, x, lineHeight) {
620
+ const lines = content.split("\n");
621
+ for (let i = 0; i < lines.length; i++) {
622
+ const spans = parseAnnotationSpans(lines[i]);
623
+ for (let s = 0; s < spans.length; s++) {
624
+ const tspan = createSVGElement("tspan");
625
+ if (s === 0) {
626
+ setAttrs(tspan, { x, dy: i === 0 ? 0 : lineHeight });
627
+ }
628
+ if (spans[s].bold) {
629
+ tspan.setAttribute("font-weight", String(BOLD_SPAN_FONT_WEIGHT));
630
+ }
631
+ tspan.textContent = spans[s].text;
632
+ text.appendChild(tspan);
633
+ }
634
+ }
523
635
  }
524
636
  function renderAnnotation(parent, annotation, index, bgColor) {
525
637
  const g = createSVGElement("g");
526
638
  g.setAttribute("class", `oc-annotation oc-annotation-${annotation.type}`);
527
- g.setAttribute("data-annotation-index", String(index));
639
+ g.setAttribute("data-annotation-index", String(annotation.specIndex ?? index));
528
640
  if (annotation.id) {
529
641
  g.setAttribute("data-annotation-id", annotation.id);
530
642
  }
@@ -561,8 +673,8 @@ function renderAnnotation(parent, annotation, index, bgColor) {
561
673
  g.appendChild(line);
562
674
  }
563
675
  if (annotation.footnoteIndex != null && annotation.label) {
564
- const cx = annotation.label.connector?.endpoint?.x ?? annotation.label.connector?.to.x ?? annotation.label.x;
565
- const cy = annotation.label.connector?.endpoint?.y ?? annotation.label.connector?.to.y ?? annotation.label.y;
676
+ const cx = annotation.label.connector?.endpoint?.x ?? annotation.label.connector?.to.x ?? annotation.dot?.x ?? annotation.label.x;
677
+ const cy = annotation.label.connector?.endpoint?.y ?? annotation.label.connector?.to.y ?? annotation.dot?.y ?? annotation.label.y;
566
678
  const r = 8;
567
679
  const circle = createSVGElement("circle");
568
680
  circle.setAttribute("class", "oc-annotation-footnote-marker");
@@ -597,7 +709,7 @@ function renderAnnotation(parent, annotation, index, bgColor) {
597
709
  if (annotation.label.connector) {
598
710
  const c = annotation.label.connector;
599
711
  if (c.style === "curve") {
600
- renderCurvedArrow(g, c.from, c.to, c.stroke);
712
+ renderCurvedConnector(g, c.from, c.to, c.stroke, c.arrow, c.exit);
601
713
  } else if (c.style === "drop-line") {
602
714
  const connector = createSVGElement("line");
603
715
  connector.setAttribute("class", "oc-annotation-connector oc-annotation-drop-line");
@@ -608,10 +720,31 @@ function renderAnnotation(parent, annotation, index, bgColor) {
608
720
  y2: c.to.y,
609
721
  stroke: c.stroke,
610
722
  "stroke-width": 1,
611
- "stroke-opacity": 0.6,
723
+ "stroke-opacity": 0.7,
612
724
  "shape-rendering": "crispEdges"
613
725
  });
614
726
  g.appendChild(connector);
727
+ } else if (c.arrow) {
728
+ const dx = c.to.x - c.from.x;
729
+ const dy = c.to.y - c.from.y;
730
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
731
+ const ux = dx / dist;
732
+ const uy = dy / dist;
733
+ const lineEndX = c.to.x - ux * ARROWHEAD_LENGTH;
734
+ const lineEndY = c.to.y - uy * ARROWHEAD_LENGTH;
735
+ const connector = createSVGElement("line");
736
+ connector.setAttribute("class", "oc-annotation-connector");
737
+ setAttrs(connector, {
738
+ x1: c.from.x,
739
+ y1: c.from.y,
740
+ x2: lineEndX,
741
+ y2: lineEndY,
742
+ stroke: c.stroke,
743
+ "stroke-width": CONNECTOR_STROKE_WIDTH,
744
+ "stroke-linecap": "round"
745
+ });
746
+ g.appendChild(connector);
747
+ renderArrowhead(g, c.to.x, c.to.y, dx, dy, c.stroke);
615
748
  } else {
616
749
  const connector = createSVGElement("line");
617
750
  connector.setAttribute("class", "oc-annotation-connector");
@@ -621,33 +754,11 @@ function renderAnnotation(parent, annotation, index, bgColor) {
621
754
  x2: c.to.x,
622
755
  y2: c.to.y,
623
756
  stroke: c.stroke,
624
- "stroke-width": 1,
625
- "stroke-opacity": 0.5
757
+ "stroke-width": CONNECTOR_STROKE_WIDTH,
758
+ "stroke-linecap": "round"
626
759
  });
627
760
  g.appendChild(connector);
628
761
  }
629
- if (c.endpoint && c.style !== "curve") {
630
- const ring = createSVGElement("circle");
631
- ring.setAttribute("class", "oc-annotation-endpoint-ring");
632
- setAttrs(ring, {
633
- cx: c.endpoint.x,
634
- cy: c.endpoint.y,
635
- r: 5,
636
- fill: bgColor ?? "#ffffff",
637
- stroke: c.stroke,
638
- "stroke-width": 1.5
639
- });
640
- g.appendChild(ring);
641
- const dot = createSVGElement("circle");
642
- dot.setAttribute("class", "oc-annotation-endpoint-dot");
643
- setAttrs(dot, {
644
- cx: c.endpoint.x,
645
- cy: c.endpoint.y,
646
- r: 2,
647
- fill: c.stroke
648
- });
649
- g.appendChild(dot);
650
- }
651
762
  }
652
763
  if (annotation.dot) {
653
764
  const dot = createSVGElement("circle");
@@ -666,41 +777,16 @@ function renderAnnotation(parent, annotation, index, bgColor) {
666
777
  text.setAttribute("class", "oc-annotation-label");
667
778
  setAttrs(text, { x: annotation.label.x, y: annotation.label.y });
668
779
  applyTextStyle(text, annotation.label.style);
669
- const lines = annotation.label.text.split("\n");
670
- const fontSize = annotation.label.style.fontSize ?? 12;
671
- const lineHeight = fontSize * (annotation.label.style.lineHeight ?? 1.3);
672
- const isMultiLine = lines.length > 1;
673
- if (isMultiLine) {
674
- for (let i = 0; i < lines.length; i++) {
675
- const tspan = createSVGElement("tspan");
676
- setAttrs(tspan, { x: annotation.label.x, dy: i === 0 ? 0 : lineHeight });
677
- tspan.textContent = lines[i];
678
- text.appendChild(tspan);
679
- }
680
- } else {
681
- text.textContent = annotation.label.text;
682
- }
683
- if (annotation.label.background) {
780
+ const fontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
781
+ const lineHeight = fontSize * (annotation.label.style.lineHeight ?? DEFAULT_LINE_HEIGHT);
782
+ fillRichText(text, annotation.label.text, annotation.label.x, lineHeight);
783
+ if (annotation.label.background && annotation.label.bounds) {
684
784
  const pad = 3;
685
- let bgX;
686
- let bgY;
687
- let bgW;
688
- let bgH;
689
- if (annotation.label.bounds) {
690
- const b = annotation.label.bounds;
691
- bgX = b.x - pad;
692
- bgY = b.y - pad;
693
- bgW = b.width + pad * 2;
694
- bgH = b.height + pad * 2;
695
- } else {
696
- const charWidth = fontSize * 0.55;
697
- const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
698
- const totalHeight = lines.length * lineHeight;
699
- bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
700
- bgY = annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad;
701
- bgW = maxLineWidth + pad * 2;
702
- bgH = totalHeight + pad * 2;
703
- }
785
+ const b = annotation.label.bounds;
786
+ const bgX = b.x - pad;
787
+ const bgY = b.y - pad;
788
+ const bgW = b.width + pad * 2;
789
+ const bgH = b.height + pad * 2;
704
790
  const bgRect = createSVGElement("rect");
705
791
  bgRect.setAttribute("class", "oc-annotation-bg");
706
792
  setAttrs(bgRect, {
@@ -724,7 +810,9 @@ function renderAnnotation(parent, annotation, index, bgColor) {
724
810
  sub.setAttribute("class", "oc-annotation-subtitle");
725
811
  setAttrs(sub, { x: annotation.subtitle.x, y: annotation.subtitle.y });
726
812
  applyTextStyle(sub, annotation.subtitle.style);
727
- sub.textContent = annotation.subtitle.text;
813
+ const subFontSize = annotation.subtitle.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
814
+ const subLineHeight = subFontSize * (annotation.subtitle.style.lineHeight ?? DEFAULT_LINE_HEIGHT);
815
+ fillRichText(sub, annotation.subtitle.text, annotation.subtitle.x, subLineHeight);
728
816
  g.appendChild(sub);
729
817
  }
730
818
  }
@@ -811,6 +899,11 @@ function renderAxis(parent, axis, orientation, layout) {
811
899
  }
812
900
  applyTextStyle(label, axis.tickLabelStyle);
813
901
  label.textContent = tick.label;
902
+ if (tick.fullLabel) {
903
+ const title = createSVGElement("title");
904
+ title.textContent = tick.fullLabel;
905
+ label.appendChild(title);
906
+ }
814
907
  g.appendChild(label);
815
908
  } else if (isInlineY) {
816
909
  const label = createSVGElement("text");
@@ -955,16 +1048,14 @@ function renderAxis(parent, axis, orientation, layout) {
955
1048
  }
956
1049
  setAttrs(axisLabel, attrs);
957
1050
  } else if (orientation === "x") {
958
- let titleY = area.y + area.height + 35;
1051
+ const tickFontSize = axis.tickLabelStyle.fontSize;
1052
+ const tickBand = Math.max(26, 4 + tickFontSize + Math.max(11, Math.ceil(tickFontSize * 0.7)));
1053
+ let titleY = area.y + area.height + tickBand + 9;
959
1054
  if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
960
1055
  const angleRad = Math.abs(axis.tickAngle) * (Math.PI / 180);
961
1056
  let maxLabelWidth = 40;
962
1057
  for (const tick of axis.ticks) {
963
- const w = estimateTextWidth(
964
- tick.label,
965
- axis.tickLabelStyle.fontSize,
966
- axis.tickLabelStyle.fontWeight
967
- );
1058
+ const w = estimateTextWidth(tick.label, tickFontSize, axis.tickLabelStyle.fontWeight);
968
1059
  if (w > maxLabelWidth) maxLabelWidth = w;
969
1060
  }
970
1061
  const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
@@ -1023,59 +1114,22 @@ function renderAxes(parent, layout) {
1023
1114
  }
1024
1115
 
1025
1116
  // src/renderers/brand.ts
1026
- import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH, textAscent as textAscent2 } from "@opendata-ai/openchart-core";
1027
- var BRAND_URL = "https://tryopendata.ai";
1028
- function renderBrand(parent, layout) {
1029
- if (layout.dimensions.width < BRAND_MIN_WIDTH) return;
1030
- const { width } = layout.dimensions;
1031
- const padding = layout.theme.spacing.padding;
1032
- const rightEdge = width - padding;
1033
- const fill = layout.theme.colors.axis;
1034
- const { chrome } = layout;
1035
- const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
1036
- const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
1037
- const { legend } = layout;
1038
- const bottomLegendOffset = legend.position === "bottom" && legend.bounds.height > 0 ? legend.bounds.height + 8 : 0;
1039
- const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter + bottomLegendOffset;
1040
- const a = createSVGElement("a");
1041
- a.setAttribute("href", BRAND_URL);
1042
- a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
1043
- a.setAttribute("target", "_blank");
1044
- a.setAttribute("rel", "noopener");
1045
- a.setAttribute("class", "oc-chrome-ref");
1046
- const BRAND_LARGE = 16;
1047
- const text = createSVGElement("text");
1048
- setAttrs(text, {
1049
- x: rightEdge,
1050
- y: chromeY + textAscent2(BRAND_LARGE),
1051
- "font-family": layout.theme.fonts.family,
1052
- "font-size": BRAND_FONT_SIZE,
1053
- "text-anchor": "end",
1054
- "fill-opacity": 0.55
1055
- });
1056
- text.style.setProperty("fill", fill);
1057
- const trySpan = createSVGElement("tspan");
1058
- trySpan.setAttribute("font-weight", "500");
1059
- trySpan.textContent = "try";
1060
- text.appendChild(trySpan);
1061
- const openDataSpan = createSVGElement("tspan");
1062
- openDataSpan.setAttribute("font-weight", "600");
1063
- openDataSpan.setAttribute("font-size", String(BRAND_LARGE));
1064
- openDataSpan.textContent = "OpenData";
1065
- text.appendChild(openDataSpan);
1066
- const aiSpan = createSVGElement("tspan");
1067
- aiSpan.setAttribute("font-weight", "500");
1068
- aiSpan.textContent = ".ai";
1069
- text.appendChild(aiSpan);
1070
- a.appendChild(text);
1071
- parent.appendChild(a);
1072
- }
1117
+ import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH, textAscent as textAscent3 } from "@opendata-ai/openchart-core";
1073
1118
 
1074
1119
  // src/renderers/chrome.ts
1075
- import { estimateTextWidth as estimateTextWidth2, textAscent as textAscent3, wrapText } from "@opendata-ai/openchart-core";
1120
+ import {
1121
+ estimateTextWidth as estimateTextWidth2,
1122
+ FOOTNOTE_LINE_HEIGHT,
1123
+ footnoteBandHeight as footnoteBandHeightFor,
1124
+ textAscent as textAscent2,
1125
+ wrapText
1126
+ } from "@opendata-ai/openchart-core";
1127
+ function footnoteBandHeight(layout) {
1128
+ return footnoteBandHeightFor(layout.chrome.footnotes?.length ?? 0, layout.theme);
1129
+ }
1076
1130
  function renderChromeElement(parent, element, className, chromeKey, measureText, uppercase = false) {
1077
1131
  const text = createSVGElement("text");
1078
- setAttrs(text, { x: element.x, y: element.y + textAscent3(element.style.fontSize) });
1132
+ setAttrs(text, { x: element.x, y: element.y + textAscent2(element.style.fontSize) });
1079
1133
  applyTextStyle(text, element.style);
1080
1134
  text.setAttribute("class", className);
1081
1135
  text.setAttribute("data-chrome-key", chromeKey);
@@ -1128,22 +1182,22 @@ function renderChrome(parent, layout) {
1128
1182
  renderChromeElement(g, chrome.subtitle, "oc-subtitle", "subtitle", measureText);
1129
1183
  }
1130
1184
  const bottomOffset = layout.chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
1131
- let footnoteBandHeight = 0;
1185
+ const bandHeight = footnoteBandHeight(layout);
1132
1186
  if (chrome.footnotes && chrome.footnotes.length > 0) {
1133
1187
  const fontSize = layout.theme.fonts.sizes.small;
1134
1188
  const pad = layout.theme.spacing.padding;
1135
- const lineHeight = fontSize * 1.3;
1189
+ const lineHeight = fontSize * FOOTNOTE_LINE_HEIGHT;
1136
1190
  const style = {
1137
1191
  fontFamily: layout.theme.fonts.family,
1138
1192
  fontSize,
1139
1193
  fontWeight: layout.theme.fonts.weights.normal,
1140
1194
  fill: layout.theme.chrome.source.color,
1141
- lineHeight: 1.3,
1195
+ lineHeight: FOOTNOTE_LINE_HEIGHT,
1142
1196
  textAnchor: "start"
1143
1197
  };
1144
1198
  for (let i = 0; i < chrome.footnotes.length; i++) {
1145
1199
  const f = chrome.footnotes[i];
1146
- const y = bottomOffset + layout.theme.spacing.chartToFooter + textAscent3(fontSize) + i * lineHeight;
1200
+ const y = bottomOffset + layout.theme.spacing.chartToFooter + textAscent2(fontSize) + i * lineHeight;
1147
1201
  const el = createSVGElement("text");
1148
1202
  el.setAttribute("class", "oc-footnotes");
1149
1203
  setAttrs(el, { x: pad, y });
@@ -1151,12 +1205,11 @@ function renderChrome(parent, layout) {
1151
1205
  el.textContent = `${f.index}. ${f.text}`;
1152
1206
  g.appendChild(el);
1153
1207
  }
1154
- footnoteBandHeight = chrome.footnotes.length * lineHeight + 4;
1155
1208
  }
1156
1209
  if (chrome.source) {
1157
1210
  renderChromeElement(
1158
1211
  g,
1159
- { ...chrome.source, y: bottomOffset + chrome.source.y + footnoteBandHeight },
1212
+ { ...chrome.source, y: bottomOffset + chrome.source.y + bandHeight },
1160
1213
  "oc-source",
1161
1214
  "source",
1162
1215
  measureText
@@ -1165,7 +1218,7 @@ function renderChrome(parent, layout) {
1165
1218
  if (chrome.byline) {
1166
1219
  renderChromeElement(
1167
1220
  g,
1168
- { ...chrome.byline, y: bottomOffset + chrome.byline.y + footnoteBandHeight },
1221
+ { ...chrome.byline, y: bottomOffset + chrome.byline.y + bandHeight },
1169
1222
  "oc-byline",
1170
1223
  "byline",
1171
1224
  measureText
@@ -1174,14 +1227,14 @@ function renderChrome(parent, layout) {
1174
1227
  if (chrome.footer) {
1175
1228
  renderChromeElement(
1176
1229
  g,
1177
- { ...chrome.footer, y: bottomOffset + chrome.footer.y + footnoteBandHeight },
1230
+ { ...chrome.footer, y: bottomOffset + chrome.footer.y + bandHeight },
1178
1231
  "oc-footer",
1179
1232
  "footer",
1180
1233
  measureText
1181
1234
  );
1182
1235
  }
1183
1236
  if (chrome.brand) {
1184
- const brandY = bottomOffset + chrome.brand.y + footnoteBandHeight;
1237
+ const brandY = bottomOffset + chrome.brand.y + bandHeight;
1185
1238
  renderChromeElement(g, { ...chrome.brand, y: brandY }, "oc-brand", "brand", measureText);
1186
1239
  const textWidth = estimateTextWidth2(
1187
1240
  chrome.brand.text,
@@ -1198,6 +1251,55 @@ function renderChrome(parent, layout) {
1198
1251
  parent.appendChild(g);
1199
1252
  }
1200
1253
 
1254
+ // src/renderers/brand.ts
1255
+ var BRAND_URL = "https://tryopendata.ai";
1256
+ function renderBrand(parent, layout) {
1257
+ if (layout.dimensions.width < BRAND_MIN_WIDTH) return;
1258
+ const { width } = layout.dimensions;
1259
+ const padding = layout.theme.spacing.padding;
1260
+ const rightEdge = width - padding;
1261
+ const fill = layout.theme.colors.axis;
1262
+ const { chrome } = layout;
1263
+ const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
1264
+ const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
1265
+ const { legend } = layout;
1266
+ const bottomLegendOffset = legend.position === "bottom" && legend.bounds.height > 0 ? legend.bounds.height + 8 : 0;
1267
+ const bandHeight = footnoteBandHeight(layout);
1268
+ const chromeY = firstBottom ? bottomOffset + firstBottom.y + bandHeight : bottomOffset + layout.theme.spacing.chartToFooter + bottomLegendOffset + bandHeight;
1269
+ const a = createSVGElement("a");
1270
+ a.setAttribute("href", BRAND_URL);
1271
+ a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
1272
+ a.setAttribute("target", "_blank");
1273
+ a.setAttribute("rel", "noopener");
1274
+ a.setAttribute("class", "oc-chrome-ref");
1275
+ const BRAND_LARGE = 16;
1276
+ const text = createSVGElement("text");
1277
+ setAttrs(text, {
1278
+ x: rightEdge,
1279
+ y: chromeY + textAscent3(BRAND_LARGE),
1280
+ "font-family": layout.theme.fonts.family,
1281
+ "font-size": BRAND_FONT_SIZE,
1282
+ "text-anchor": "end",
1283
+ "fill-opacity": 0.55
1284
+ });
1285
+ text.style.setProperty("fill", fill);
1286
+ const trySpan = createSVGElement("tspan");
1287
+ trySpan.setAttribute("font-weight", "500");
1288
+ trySpan.textContent = "try";
1289
+ text.appendChild(trySpan);
1290
+ const openDataSpan = createSVGElement("tspan");
1291
+ openDataSpan.setAttribute("font-weight", "600");
1292
+ openDataSpan.setAttribute("font-size", String(BRAND_LARGE));
1293
+ openDataSpan.textContent = "OpenData";
1294
+ text.appendChild(openDataSpan);
1295
+ const aiSpan = createSVGElement("tspan");
1296
+ aiSpan.setAttribute("font-weight", "500");
1297
+ aiSpan.textContent = ".ai";
1298
+ text.appendChild(aiSpan);
1299
+ a.appendChild(text);
1300
+ parent.appendChild(a);
1301
+ }
1302
+
1201
1303
  // src/renderers/endpoint-labels.ts
1202
1304
  var LEADER_STROKE_WIDTH = 1;
1203
1305
  var LEADER_OPACITY = 0.45;
@@ -1306,7 +1408,125 @@ import { estimateTextWidth as estimateTextWidth3 } from "@opendata-ai/openchart-
1306
1408
  function isCategorical(legend) {
1307
1409
  return !legend.type || legend.type === "categorical";
1308
1410
  }
1411
+ function renderContinuousLegend(parent, legend) {
1412
+ if (legend.bounds.width <= 0 || legend.bounds.height <= 0) return;
1413
+ const g = createSVGElement("g");
1414
+ g.setAttribute("class", "oc-legend oc-legend--continuous");
1415
+ g.setAttribute("role", "img");
1416
+ const first = legend.ticks[0];
1417
+ const last = legend.ticks[legend.ticks.length - 1];
1418
+ if (first && last) {
1419
+ g.setAttribute("aria-label", `Color scale from ${first.label} to ${last.label}`);
1420
+ } else {
1421
+ g.setAttribute("aria-label", "Color scale");
1422
+ }
1423
+ if (legend.mode === "gradient") {
1424
+ let defs = parent.querySelector("defs");
1425
+ if (!defs) {
1426
+ defs = createSVGElement("defs");
1427
+ parent.insertBefore(defs, parent.firstChild);
1428
+ }
1429
+ const gradientId = nextSvgId("oc-legend-gradient");
1430
+ const grad = createSVGElement("linearGradient");
1431
+ grad.setAttribute("id", gradientId);
1432
+ setAttrs(grad, { x1: "0%", y1: "0%", x2: "100%", y2: "0%" });
1433
+ for (const stop of legend.colorStops) {
1434
+ const s = createSVGElement("stop");
1435
+ const attrs = {
1436
+ offset: `${stop.offset * 100}%`,
1437
+ "stop-color": stop.color
1438
+ };
1439
+ if (stop.opacity !== void 0) attrs["stop-opacity"] = stop.opacity;
1440
+ setAttrs(s, attrs);
1441
+ grad.appendChild(s);
1442
+ }
1443
+ defs.appendChild(grad);
1444
+ const bar = createSVGElement("rect");
1445
+ bar.setAttribute("class", "oc-legend-gradient-bar");
1446
+ setAttrs(bar, {
1447
+ x: legend.bar.x,
1448
+ y: legend.bar.y,
1449
+ width: legend.bar.width,
1450
+ height: legend.bar.height,
1451
+ rx: 2,
1452
+ ry: 2,
1453
+ fill: `url(#${gradientId})`
1454
+ });
1455
+ g.appendChild(bar);
1456
+ } else {
1457
+ for (const bin of legend.bins) {
1458
+ const rect = createSVGElement("rect");
1459
+ rect.setAttribute("class", "oc-legend-bin");
1460
+ setAttrs(rect, {
1461
+ x: bin.x,
1462
+ y: legend.bar.y,
1463
+ width: bin.width,
1464
+ height: legend.bar.height,
1465
+ fill: bin.color
1466
+ });
1467
+ g.appendChild(rect);
1468
+ }
1469
+ }
1470
+ for (const tick of legend.ticks) {
1471
+ const label = createSVGElement("text");
1472
+ setAttrs(label, { x: tick.x, y: legend.labelY, "text-anchor": tick.anchor });
1473
+ applyTextStyle(label, legend.labelStyle);
1474
+ label.textContent = tick.label;
1475
+ g.appendChild(label);
1476
+ }
1477
+ parent.appendChild(g);
1478
+ }
1479
+ function renderSizeLegend(parent, legend) {
1480
+ if (legend.circles.length === 0) return;
1481
+ const g = createSVGElement("g");
1482
+ g.setAttribute("class", "oc-legend oc-legend--size");
1483
+ g.setAttribute("aria-hidden", "true");
1484
+ const ox = legend.bounds.x;
1485
+ const oy = legend.bounds.y;
1486
+ const labelX = ox + legend.circles[0].cx + legend.circles[0].radius + 8;
1487
+ const leaderX = ox + legend.circles[0].cx + legend.circles[0].radius + 4;
1488
+ for (const circle of legend.circles) {
1489
+ const c = createSVGElement("circle");
1490
+ setAttrs(c, {
1491
+ cx: ox + circle.cx,
1492
+ cy: oy + circle.cy,
1493
+ r: circle.radius,
1494
+ fill: "none",
1495
+ stroke: legend.stroke,
1496
+ "stroke-width": 1
1497
+ });
1498
+ g.appendChild(c);
1499
+ const line = createSVGElement("line");
1500
+ setAttrs(line, {
1501
+ x1: ox + circle.cx,
1502
+ y1: oy + circle.cy - circle.radius,
1503
+ x2: leaderX,
1504
+ y2: oy + circle.cy - circle.radius,
1505
+ stroke: legend.stroke,
1506
+ "stroke-width": 1,
1507
+ "stroke-dasharray": "2 2"
1508
+ });
1509
+ g.appendChild(line);
1510
+ const label = createSVGElement("text");
1511
+ setAttrs(label, {
1512
+ x: labelX,
1513
+ y: oy + circle.labelY
1514
+ });
1515
+ applyTextStyle(label, legend.labelStyle);
1516
+ label.textContent = circle.label;
1517
+ g.appendChild(label);
1518
+ }
1519
+ parent.appendChild(g);
1520
+ }
1309
1521
  function renderLegend(parent, legend) {
1522
+ if (legend.type === "continuous") {
1523
+ renderContinuousLegend(parent, legend);
1524
+ return;
1525
+ }
1526
+ if (legend.type === "size") {
1527
+ renderSizeLegend(parent, legend);
1528
+ return;
1529
+ }
1310
1530
  if (!isCategorical(legend) || legend.entries.length === 0) return;
1311
1531
  const g = createSVGElement("g");
1312
1532
  g.setAttribute("class", "oc-legend");
@@ -1463,10 +1683,6 @@ function renderMetrics(parent, layout) {
1463
1683
  }
1464
1684
 
1465
1685
  // src/svg-renderer.ts
1466
- var EASE_VAR_MAP = {
1467
- smooth: "var(--oc-ease-smooth)",
1468
- snappy: "var(--oc-ease-snappy)"
1469
- };
1470
1686
  function renderChartSVG(layout, container, opts) {
1471
1687
  const { width, height } = layout.dimensions;
1472
1688
  const animation = layout.animation;
@@ -1486,8 +1702,12 @@ function renderChartSVG(layout, container, opts) {
1486
1702
  "text-rendering": "optimizeLegibility"
1487
1703
  });
1488
1704
  svg.style.height = `${height}px`;
1489
- svg.setAttribute("role", layout.a11y.role);
1490
- svg.setAttribute("aria-label", layout.a11y.altText);
1705
+ if (layout.a11y.hidden) {
1706
+ svg.setAttribute("aria-hidden", "true");
1707
+ } else {
1708
+ svg.setAttribute("role", layout.a11y.role);
1709
+ svg.setAttribute("aria-label", layout.a11y.altText);
1710
+ }
1491
1711
  if (layout.display === "sparkline") {
1492
1712
  svg.setAttribute("data-display", "sparkline");
1493
1713
  }
@@ -1497,11 +1717,6 @@ function renderChartSVG(layout, container, opts) {
1497
1717
  if (enterPhase) {
1498
1718
  const markCount = layout.marks.length;
1499
1719
  const stagger = clampStaggerDelay(enterPhase.staggerDelay, markCount);
1500
- svg.style.setProperty("--oc-animation-duration", `${enterPhase.duration}ms`);
1501
- svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
1502
- svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
1503
- const easeVar = EASE_VAR_MAP[enterPhase.ease] || EASE_VAR_MAP.smooth;
1504
- svg.style.setProperty("--oc-animation-ease", easeVar);
1505
1720
  let maxSegments = 0;
1506
1721
  for (const m of layout.marks) {
1507
1722
  if (m.type === "rect") {
@@ -1511,10 +1726,14 @@ function renderChartSVG(layout, container, opts) {
1511
1726
  }
1512
1727
  }
1513
1728
  }
1514
- if (maxSegments > 0) {
1515
- const segDuration = Math.round(enterPhase.duration / maxSegments);
1516
- svg.style.setProperty("--oc-stack-segment-duration", `${segDuration}ms`);
1517
- }
1729
+ const stackSegmentDuration = maxSegments > 0 ? Math.round(enterPhase.duration / maxSegments) : void 0;
1730
+ stampAnimationVars(svg, {
1731
+ duration: enterPhase.duration,
1732
+ stagger,
1733
+ annotationDelay: animation.annotationDelay,
1734
+ ease: enterPhase.ease,
1735
+ stackSegmentDuration
1736
+ });
1518
1737
  }
1519
1738
  if (layout.display !== "sparkline") {
1520
1739
  const bg = createSVGElement("rect");
@@ -1546,15 +1765,22 @@ function renderChartSVG(layout, container, opts) {
1546
1765
  clipPath.appendChild(clipRect);
1547
1766
  defs.appendChild(clipPath);
1548
1767
  const gradientMap = buildGradientDefs(layout.marks, defs);
1768
+ const patternMap = buildPatternDefs(
1769
+ layout.marks,
1770
+ defs
1771
+ );
1549
1772
  svg.appendChild(defs);
1550
- setMarkRenderState({ animation, gradientMap });
1773
+ setMarkRenderState({ animation, gradientMap, patternMap });
1551
1774
  try {
1552
1775
  if (layout.facet) {
1553
1776
  renderFacetedPanels(svg, layout, layout.facet.panels, defs);
1554
1777
  } else {
1555
1778
  renderAxes(svg, layout);
1779
+ const clipGroup = createSVGElement("g");
1780
+ clipGroup.setAttribute("clip-path", `url(#${clipId})`);
1556
1781
  const clippedGroup = createSVGElement("g");
1557
- clippedGroup.setAttribute("clip-path", `url(#${clipId})`);
1782
+ clippedGroup.setAttribute("data-oc-marks-group", "true");
1783
+ clipGroup.appendChild(clippedGroup);
1558
1784
  const markLabelsOverlay = renderMarks(clippedGroup, layout);
1559
1785
  const hasLineOrAreaWithDataPoints = layout.marks.some(
1560
1786
  (m) => (m.type === "line" || m.type === "area") && m.dataPoints && m.dataPoints.length > 0
@@ -1599,7 +1825,7 @@ function renderChartSVG(layout, container, opts) {
1599
1825
  dotsGroup.setAttribute("pointer-events", "none");
1600
1826
  clippedGroup.appendChild(dotsGroup);
1601
1827
  }
1602
- svg.appendChild(clippedGroup);
1828
+ svg.appendChild(clipGroup);
1603
1829
  if (markLabelsOverlay) {
1604
1830
  svg.appendChild(markLabelsOverlay);
1605
1831
  }
@@ -1622,7 +1848,9 @@ function renderChartSVG(layout, container, opts) {
1622
1848
  }
1623
1849
  }
1624
1850
  }
1625
- renderLegend(svg, layout.legend);
1851
+ for (const legend of layout.legends ?? [layout.legend]) {
1852
+ renderLegend(svg, legend);
1853
+ }
1626
1854
  renderChrome(svg, layout);
1627
1855
  renderMetrics(svg, layout);
1628
1856
  if (layout.watermark && !layout.chrome.brand) {
@@ -1642,9 +1870,9 @@ function renderFacetedPanels(svg, layout, panels, defs) {
1642
1870
  const bg = createSVGElement("rect");
1643
1871
  setAttrs(bg, {
1644
1872
  x: panel.area.x,
1645
- y: panel.header.y - panel.header.fontSize * 0.6,
1873
+ y: panel.area.y,
1646
1874
  width: panel.area.width,
1647
- height: panel.area.height + panel.header.fontSize + 4,
1875
+ height: panel.area.height,
1648
1876
  rx: 3,
1649
1877
  fill: layout.theme.isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"
1650
1878
  });
@@ -1693,355 +1921,20 @@ function renderFacetedPanels(svg, layout, panels, defs) {
1693
1921
  }
1694
1922
  }
1695
1923
 
1696
- // src/tilemap-renderer.ts
1697
- import { textAscent as textAscent4 } from "@opendata-ai/openchart-core";
1698
- var SVG_NS3 = "http://www.w3.org/2000/svg";
1699
- var XLINK_NS2 = "http://www.w3.org/1999/xlink";
1700
- var BRAND_URL2 = "https://tryopendata.ai";
1701
- var EASE_VAR_MAP2 = {
1702
- smooth: "var(--oc-ease-smooth)",
1703
- snappy: "var(--oc-ease-snappy)"
1704
- };
1705
- function createSVGElement2(tag) {
1706
- return document.createElementNS(SVG_NS3, tag);
1707
- }
1708
- function setAttrs2(el, attrs) {
1709
- for (const [key, value] of Object.entries(attrs)) {
1710
- el.setAttribute(key, String(value));
1711
- }
1712
- }
1713
- function renderChrome2(parent, layout) {
1714
- const g = createSVGElement2("g");
1715
- g.setAttribute("class", "oc-chrome");
1716
- const { chrome } = layout;
1717
- const bottomOffset = layout.area.y + layout.area.height;
1718
- if (chrome.title) {
1719
- const text = createSVGElement2("text");
1720
- setAttrs2(text, {
1721
- x: chrome.title.x,
1722
- y: chrome.title.y + textAscent4(chrome.title.style.fontSize)
1723
- });
1724
- text.setAttribute("class", "oc-title");
1725
- text.setAttribute("font-family", chrome.title.style.fontFamily);
1726
- text.setAttribute("font-size", String(chrome.title.style.fontSize));
1727
- text.setAttribute("font-weight", String(chrome.title.style.fontWeight));
1728
- text.style.setProperty("fill", chrome.title.style.fill);
1729
- text.textContent = chrome.title.text;
1730
- g.appendChild(text);
1731
- }
1732
- if (chrome.subtitle) {
1733
- const text = createSVGElement2("text");
1734
- setAttrs2(text, {
1735
- x: chrome.subtitle.x,
1736
- y: chrome.subtitle.y + textAscent4(chrome.subtitle.style.fontSize)
1737
- });
1738
- text.setAttribute("class", "oc-subtitle");
1739
- text.setAttribute("font-family", chrome.subtitle.style.fontFamily);
1740
- text.setAttribute("font-size", String(chrome.subtitle.style.fontSize));
1741
- text.setAttribute("font-weight", String(chrome.subtitle.style.fontWeight));
1742
- text.style.setProperty(
1743
- "fill",
1744
- chrome.subtitle.style.fill
1745
- );
1746
- text.textContent = chrome.subtitle.text;
1747
- g.appendChild(text);
1748
- }
1749
- if (chrome.source) {
1750
- const text = createSVGElement2("text");
1751
- setAttrs2(text, {
1752
- x: chrome.source.x,
1753
- y: bottomOffset + chrome.source.y + textAscent4(chrome.source.style.fontSize)
1754
- });
1755
- text.setAttribute("class", "oc-source");
1756
- text.setAttribute("font-family", chrome.source.style.fontFamily);
1757
- text.setAttribute("font-size", String(chrome.source.style.fontSize));
1758
- text.setAttribute("font-weight", String(chrome.source.style.fontWeight));
1759
- text.style.setProperty(
1760
- "fill",
1761
- chrome.source.style.fill
1762
- );
1763
- text.textContent = chrome.source.text;
1764
- g.appendChild(text);
1765
- }
1766
- if (chrome.byline) {
1767
- const text = createSVGElement2("text");
1768
- setAttrs2(text, {
1769
- x: chrome.byline.x,
1770
- y: bottomOffset + chrome.byline.y + textAscent4(chrome.byline.style.fontSize)
1771
- });
1772
- text.setAttribute("class", "oc-byline");
1773
- text.setAttribute("font-family", chrome.byline.style.fontFamily);
1774
- text.setAttribute("font-size", String(chrome.byline.style.fontSize));
1775
- text.setAttribute("font-weight", String(chrome.byline.style.fontWeight));
1776
- text.style.setProperty(
1777
- "fill",
1778
- chrome.byline.style.fill
1779
- );
1780
- text.textContent = chrome.byline.text;
1781
- g.appendChild(text);
1782
- }
1783
- if (chrome.footer) {
1784
- const text = createSVGElement2("text");
1785
- setAttrs2(text, {
1786
- x: chrome.footer.x,
1787
- y: bottomOffset + chrome.footer.y + textAscent4(chrome.footer.style.fontSize)
1788
- });
1789
- text.setAttribute("class", "oc-footer");
1790
- text.setAttribute("font-family", chrome.footer.style.fontFamily);
1791
- text.setAttribute("font-size", String(chrome.footer.style.fontSize));
1792
- text.setAttribute("font-weight", String(chrome.footer.style.fontWeight));
1793
- text.style.setProperty(
1794
- "fill",
1795
- chrome.footer.style.fill
1796
- );
1797
- text.textContent = chrome.footer.text;
1798
- g.appendChild(text);
1799
- }
1800
- parent.appendChild(g);
1801
- }
1802
- function renderWatermark(parent, layout) {
1803
- if (layout.width < 480) return;
1804
- const { width, height } = layout;
1805
- const { theme } = layout;
1806
- const padding = theme.spacing.padding;
1807
- const rightEdge = width - padding;
1808
- const bottomEdge = height - padding;
1809
- const fill = theme.colors.axis;
1810
- const a = createSVGElement2("a");
1811
- a.setAttribute("href", BRAND_URL2);
1812
- a.setAttributeNS(XLINK_NS2, "xlink:href", BRAND_URL2);
1813
- a.setAttribute("target", "_blank");
1814
- a.setAttribute("rel", "noopener");
1815
- a.setAttribute("class", "oc-chrome-ref");
1816
- const text = createSVGElement2("text");
1817
- setAttrs2(text, {
1818
- x: rightEdge,
1819
- y: bottomEdge,
1820
- "dominant-baseline": "alphabetic",
1821
- "text-anchor": "end",
1822
- "font-family": theme.fonts.family,
1823
- "font-size": 12,
1824
- "fill-opacity": 0.55
1825
- });
1826
- text.style.setProperty("fill", fill);
1827
- const trySpan = createSVGElement2("tspan");
1828
- setAttrs2(trySpan, { "font-weight": 500 });
1829
- trySpan.textContent = "try";
1830
- const openDataSpan = createSVGElement2("tspan");
1831
- setAttrs2(openDataSpan, { "font-weight": 600, "font-size": 16 });
1832
- openDataSpan.textContent = "OpenData";
1833
- const aiSpan = createSVGElement2("tspan");
1834
- setAttrs2(aiSpan, { "font-weight": 500 });
1835
- aiSpan.textContent = ".ai";
1836
- text.appendChild(trySpan);
1837
- text.appendChild(openDataSpan);
1838
- text.appendChild(aiSpan);
1839
- a.appendChild(text);
1840
- parent.appendChild(a);
1841
- }
1842
- function renderTiles(parent, tiles, animation) {
1843
- const g = createSVGElement2("g");
1844
- g.setAttribute("class", "oc-tilemap-tiles");
1845
- g.setAttribute("role", "list");
1846
- const tileDelays = [];
1847
- if (animation?.enter) {
1848
- const baseStagger = 800 / Math.max(tiles.length, 1);
1849
- let seed = 17;
1850
- for (let i = 0; i < tiles.length; i++) {
1851
- const idx = tiles[i].animationIndex ?? i;
1852
- seed = seed * 1103515245 + 12345 & 2147483647;
1853
- const jitter = (seed % 1e3 / 1e3 - 0.5) * 0.8;
1854
- tileDelays.push(Math.max(0, Math.round(idx * baseStagger * (1 + jitter))));
1855
- }
1856
- }
1857
- for (let i = 0; i < tiles.length; i++) {
1858
- const tile = tiles[i];
1859
- const tileGroup = createSVGElement2("g");
1860
- tileGroup.setAttribute("class", "oc-tilemap-tile");
1861
- tileGroup.setAttribute("data-state", tile.stateCode);
1862
- tileGroup.setAttribute("role", "listitem");
1863
- if (tile.aria?.label) {
1864
- tileGroup.setAttribute("aria-label", tile.aria.label);
1865
- }
1866
- if (animation?.enter) {
1867
- const idx = tile.animationIndex ?? i;
1868
- tileGroup.setAttribute("data-animation-index", String(idx));
1869
- tileGroup.style.setProperty(
1870
- "--oc-mark-index",
1871
- String(idx)
1872
- );
1873
- tileGroup.style.setProperty(
1874
- "--oc-tile-delay",
1875
- `${tileDelays[i]}ms`
1876
- );
1877
- }
1878
- const rect = createSVGElement2("rect");
1879
- setAttrs2(rect, {
1880
- x: tile.x,
1881
- y: tile.y,
1882
- width: tile.size,
1883
- height: tile.size,
1884
- rx: tile.cornerRadius,
1885
- fill: tile.fill,
1886
- "fill-opacity": tile.fillOpacity ?? 1,
1887
- stroke: tile.stroke,
1888
- "stroke-width": tile.strokeWidth
1889
- });
1890
- tileGroup.appendChild(rect);
1891
- const codeLabel = createSVGElement2("text");
1892
- setAttrs2(codeLabel, {
1893
- x: tile.label.x,
1894
- y: tile.label.y,
1895
- "text-anchor": "middle",
1896
- "dominant-baseline": "central",
1897
- "font-family": tile.label.style.fontFamily,
1898
- "font-size": tile.label.style.fontSize,
1899
- "font-weight": tile.label.style.fontWeight
1900
- });
1901
- codeLabel.style.setProperty(
1902
- "fill",
1903
- tile.label.style.fill
1904
- );
1905
- codeLabel.textContent = tile.label.text;
1906
- tileGroup.appendChild(codeLabel);
1907
- if (tile.valueLabel.visible && tile.valueLabel.text) {
1908
- const valueLabel = createSVGElement2("text");
1909
- setAttrs2(valueLabel, {
1910
- x: tile.valueLabel.x,
1911
- y: tile.valueLabel.y,
1912
- "text-anchor": "middle",
1913
- "dominant-baseline": "central",
1914
- "font-family": tile.valueLabel.style.fontFamily,
1915
- "font-size": tile.valueLabel.style.fontSize,
1916
- "font-weight": tile.valueLabel.style.fontWeight
1917
- });
1918
- valueLabel.style.setProperty(
1919
- "fill",
1920
- tile.valueLabel.style.fill
1921
- );
1922
- valueLabel.textContent = tile.valueLabel.text;
1923
- tileGroup.appendChild(valueLabel);
1924
- }
1925
- g.appendChild(tileGroup);
1926
- }
1927
- parent.appendChild(g);
1928
- }
1929
- function renderGradientLegend(parent, layout) {
1930
- if (!layout.gradientLegend) return;
1931
- const { gradientLegend } = layout;
1932
- const g = createSVGElement2("g");
1933
- g.setAttribute("class", "oc-tilemap-legend");
1934
- const defs = parent.querySelector("defs") || createSVGElement2("defs");
1935
- const exists = parent.querySelector("defs");
1936
- if (!exists) {
1937
- parent.insertBefore(defs, parent.firstChild);
1938
- }
1939
- const gradientId = nextSvgId("oc-tilemap-legend-gradient");
1940
- const grad = createSVGElement2("linearGradient");
1941
- grad.id = gradientId;
1942
- grad.setAttribute("x1", "0%");
1943
- grad.setAttribute("y1", "0%");
1944
- grad.setAttribute("x2", "100%");
1945
- grad.setAttribute("y2", "0%");
1946
- for (const stop of gradientLegend.colorStops) {
1947
- const s = createSVGElement2("stop");
1948
- const attrs = {
1949
- offset: `${stop.offset * 100}%`,
1950
- "stop-color": stop.color
1951
- };
1952
- if (stop.opacity !== void 0) {
1953
- attrs["stop-opacity"] = stop.opacity;
1954
- }
1955
- setAttrs2(s, attrs);
1956
- grad.appendChild(s);
1957
- }
1958
- defs.appendChild(grad);
1959
- const barHeight = gradientLegend.bounds.height;
1960
- const bar = createSVGElement2("rect");
1961
- setAttrs2(bar, {
1962
- x: gradientLegend.bounds.x,
1963
- y: gradientLegend.bounds.y,
1964
- width: gradientLegend.bounds.width,
1965
- height: barHeight,
1966
- rx: barHeight / 2,
1967
- fill: `url(#${gradientId})`
1968
- });
1969
- g.appendChild(bar);
1970
- const minText = createSVGElement2("text");
1971
- setAttrs2(minText, {
1972
- x: gradientLegend.bounds.x,
1973
- y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
1974
- "text-anchor": "start",
1975
- "font-family": gradientLegend.labelStyle.fontFamily,
1976
- "font-size": gradientLegend.labelStyle.fontSize,
1977
- "font-weight": gradientLegend.labelStyle.fontWeight
1978
- });
1979
- minText.style.setProperty(
1980
- "fill",
1981
- gradientLegend.labelStyle.fill
1982
- );
1983
- minText.textContent = gradientLegend.minLabel;
1984
- g.appendChild(minText);
1985
- const maxText = createSVGElement2("text");
1986
- setAttrs2(maxText, {
1987
- x: gradientLegend.bounds.x + gradientLegend.bounds.width,
1988
- y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
1989
- "text-anchor": "end",
1990
- "font-family": gradientLegend.labelStyle.fontFamily,
1991
- "font-size": gradientLegend.labelStyle.fontSize,
1992
- "font-weight": gradientLegend.labelStyle.fontWeight
1993
- });
1994
- maxText.style.setProperty(
1995
- "fill",
1996
- gradientLegend.labelStyle.fill
1997
- );
1998
- maxText.textContent = gradientLegend.maxLabel;
1999
- g.appendChild(maxText);
2000
- parent.appendChild(g);
2001
- }
2002
- function renderTileMapSVG(layout, opts) {
2003
- const { width, height, tiles, a11y, watermark, animation } = layout;
2004
- const animate = opts?.animate && !!animation?.enter;
2005
- const svg = createSVGElement2("svg");
2006
- svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
2007
- svg.setAttribute("role", "img");
2008
- if (a11y.altText) {
2009
- svg.setAttribute("aria-label", a11y.altText);
2010
- }
2011
- const classes = animate ? "oc-tilemap oc-animate" : "oc-tilemap";
2012
- svg.setAttribute("class", classes);
2013
- if (animate && animation?.enter) {
2014
- const stagger = Math.max(5, Math.round(800 / Math.max(tiles.length, 1)));
2015
- svg.style.setProperty("--oc-animation-duration", `${animation.enter.duration}ms`);
2016
- svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
2017
- svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
2018
- const easeVar = EASE_VAR_MAP2[animation.enter.ease] || EASE_VAR_MAP2.smooth;
2019
- svg.style.setProperty("--oc-animation-ease", easeVar);
2020
- }
2021
- const defs = createSVGElement2("defs");
2022
- svg.appendChild(defs);
2023
- renderChrome2(svg, layout);
2024
- renderTiles(svg, tiles, animate ? animation : void 0);
2025
- if (layout.categoricalLegend) {
2026
- renderLegend(svg, layout.categoricalLegend);
2027
- } else {
2028
- renderGradientLegend(svg, layout);
2029
- }
2030
- if (watermark) {
2031
- renderWatermark(svg, layout);
2032
- }
2033
- return svg;
2034
- }
2035
-
2036
1924
  export {
1925
+ stampAnimationVars,
1926
+ SVG_NS,
1927
+ createSVGElement,
1928
+ setAttrs,
1929
+ renderChromeElement,
1930
+ nextSvgId,
2037
1931
  resetSvgIdCounter,
1932
+ renderLegend,
2038
1933
  buildGradientDefs,
2039
1934
  resolveMarkFill,
2040
- SVG_NS,
2041
1935
  registerMarkRenderer,
2042
1936
  rectPathWithCorners,
2043
1937
  renderSingleMark,
2044
- renderChartSVG,
2045
- renderTileMapSVG
1938
+ renderChartSVG
2046
1939
  };
2047
- //# sourceMappingURL=chunk-EQ6DYFO7.js.map
1940
+ //# sourceMappingURL=chunk-5EOV5JQZ.js.map