@opendata-ai/openchart-vanilla 7.6.0 → 7.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.
@@ -0,0 +1,1680 @@
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
+ // src/renderers/svg-dom.ts
11
+ var SVG_NS = "http://www.w3.org/2000/svg";
12
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
13
+ function createSVGElement(tag) {
14
+ return document.createElementNS(SVG_NS, tag);
15
+ }
16
+ function setAttrs(el, attrs) {
17
+ for (const [key, value] of Object.entries(attrs)) {
18
+ el.setAttribute(key, String(value));
19
+ }
20
+ }
21
+ function applyTextStyle(el, style) {
22
+ const inline = el.style;
23
+ inline.setProperty("fill", style.fill);
24
+ inline.setProperty("font-size", `${style.fontSize}px`);
25
+ inline.setProperty("font-weight", String(style.fontWeight));
26
+ inline.setProperty("font-family", style.fontFamily);
27
+ if (style.textAnchor) {
28
+ el.setAttribute("text-anchor", style.textAnchor);
29
+ }
30
+ if (style.dominantBaseline) {
31
+ el.setAttribute("dominant-baseline", style.dominantBaseline);
32
+ }
33
+ if (style.fontVariant) {
34
+ el.setAttribute("font-variant", style.fontVariant);
35
+ }
36
+ }
37
+
38
+ // src/gradient-utils.ts
39
+ import { isGradientDef } from "@opendata-ai/openchart-core";
40
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
41
+ function gradientKey(def) {
42
+ return sortedStringify(def);
43
+ }
44
+ function sortedStringify(value) {
45
+ if (value === null || value === void 0) return String(value);
46
+ if (Array.isArray(value)) return `[${value.map(sortedStringify).join(",")}]`;
47
+ if (typeof value === "object") {
48
+ const sorted = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${sortedStringify(value[k])}`);
49
+ return `{${sorted.join(",")}}`;
50
+ }
51
+ return JSON.stringify(value);
52
+ }
53
+ function createGradientElement(def, id) {
54
+ if (def.gradient === "linear") {
55
+ return createLinearGradient(def, id);
56
+ }
57
+ return createRadialGradient(def, id);
58
+ }
59
+ function createLinearGradient(def, id) {
60
+ const el = document.createElementNS(SVG_NS2, "linearGradient");
61
+ el.setAttribute("id", id);
62
+ el.setAttribute("gradientUnits", "objectBoundingBox");
63
+ el.setAttribute("x1", String(def.x1 ?? 0));
64
+ el.setAttribute("y1", String(def.y1 ?? 0));
65
+ el.setAttribute("x2", String(def.x2 ?? 0));
66
+ el.setAttribute("y2", String(def.y2 ?? 1));
67
+ for (const stop of def.stops) {
68
+ appendStop(el, stop);
69
+ }
70
+ return el;
71
+ }
72
+ function createRadialGradient(def, id) {
73
+ const el = document.createElementNS(SVG_NS2, "radialGradient");
74
+ el.setAttribute("id", id);
75
+ el.setAttribute("gradientUnits", "objectBoundingBox");
76
+ el.setAttribute("cx", String(def.x2 ?? 0.5));
77
+ el.setAttribute("cy", String(def.y2 ?? 0.5));
78
+ el.setAttribute("r", String(def.r2 ?? 0.5));
79
+ el.setAttribute("fx", String(def.x1 ?? 0.5));
80
+ el.setAttribute("fy", String(def.y1 ?? 0.5));
81
+ el.setAttribute("fr", String(def.r1 ?? 0));
82
+ for (const stop of def.stops) {
83
+ appendStop(el, stop);
84
+ }
85
+ return el;
86
+ }
87
+ function appendStop(parent, stop) {
88
+ const stopEl = document.createElementNS(SVG_NS2, "stop");
89
+ stopEl.setAttribute("offset", String(stop.offset));
90
+ stopEl.setAttribute("stop-color", stop.color);
91
+ if (stop.opacity !== void 0) {
92
+ stopEl.setAttribute("stop-opacity", String(stop.opacity));
93
+ }
94
+ parent.appendChild(stopEl);
95
+ }
96
+ function buildGradientDefs(marks, defs) {
97
+ const map = /* @__PURE__ */ new Map();
98
+ for (const mark of marks) {
99
+ const fill = mark.fill;
100
+ if (fill && isGradientDef(fill)) {
101
+ const key = gradientKey(fill);
102
+ if (!map.has(key)) {
103
+ const id = nextSvgId("oc-grad");
104
+ const el = createGradientElement(fill, id);
105
+ defs.appendChild(el);
106
+ map.set(key, id);
107
+ }
108
+ }
109
+ }
110
+ return map;
111
+ }
112
+ function resolveMarkFill(fill, gradientMap) {
113
+ if (typeof fill === "string") return fill;
114
+ const key = gradientKey(fill);
115
+ const id = gradientMap.get(key);
116
+ return id ? `url(#${id})` : "#000000";
117
+ }
118
+
119
+ // src/renderers/marks.ts
120
+ var currentAnimation;
121
+ var currentGradientMap = /* @__PURE__ */ new Map();
122
+ function setMarkRenderState(state) {
123
+ currentAnimation = state.animation;
124
+ currentGradientMap = state.gradientMap;
125
+ }
126
+ function resetMarkRenderState() {
127
+ currentAnimation = void 0;
128
+ currentGradientMap = /* @__PURE__ */ new Map();
129
+ }
130
+ function stampAnimationAttrs(el, mark, fallbackIndex) {
131
+ if (!currentAnimation?.enabled) return;
132
+ const idx = mark.animationIndex ?? fallbackIndex;
133
+ el.setAttribute("data-animation-index", String(idx));
134
+ el.style.setProperty("--oc-mark-index", String(idx));
135
+ }
136
+ var markRenderers = {};
137
+ function registerMarkRenderer(type, renderer) {
138
+ markRenderers[type] = renderer;
139
+ }
140
+ function renderLineMark(mark, index) {
141
+ const g = createSVGElement("g");
142
+ g.setAttribute("data-mark-id", `line-${mark.seriesKey ?? index}`);
143
+ g.setAttribute("class", "oc-mark oc-mark-line");
144
+ stampAnimationAttrs(g, mark, index);
145
+ if (mark.points.length > 1) {
146
+ const path = createSVGElement("path");
147
+ const d = mark.path ?? mark.points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
148
+ setAttrs(path, {
149
+ d,
150
+ fill: "none",
151
+ stroke: mark.stroke,
152
+ "stroke-width": mark.strokeWidth
153
+ });
154
+ if (mark.strokeDasharray) {
155
+ path.setAttribute("stroke-dasharray", mark.strokeDasharray);
156
+ }
157
+ if (mark.opacity != null) {
158
+ path.setAttribute("opacity", String(mark.opacity));
159
+ }
160
+ g.appendChild(path);
161
+ }
162
+ if (mark.label?.visible) {
163
+ const label = createSVGElement("text");
164
+ label.setAttribute("class", "oc-mark-label");
165
+ if (mark.seriesKey) {
166
+ label.setAttribute("data-series", mark.seriesKey);
167
+ }
168
+ setAttrs(label, { x: mark.label.x, y: mark.label.y });
169
+ applyTextStyle(label, mark.label.style);
170
+ label.textContent = mark.label.text;
171
+ g.appendChild(label);
172
+ if (mark.label.connector) {
173
+ const connector = createSVGElement("line");
174
+ connector.setAttribute("class", "oc-mark-connector");
175
+ setAttrs(connector, {
176
+ x1: mark.label.connector.from.x,
177
+ y1: mark.label.connector.from.y,
178
+ x2: mark.label.connector.to.x,
179
+ y2: mark.label.connector.to.y,
180
+ stroke: mark.label.connector.stroke,
181
+ "stroke-width": 1,
182
+ "stroke-opacity": 0.5
183
+ });
184
+ g.appendChild(connector);
185
+ }
186
+ }
187
+ return g;
188
+ }
189
+ function renderAreaMark(mark, index) {
190
+ const g = createSVGElement("g");
191
+ g.setAttribute("data-mark-id", `area-${mark.seriesKey ?? index}`);
192
+ g.setAttribute("class", "oc-mark oc-mark-area");
193
+ stampAnimationAttrs(g, mark, index);
194
+ if (mark.path) {
195
+ const fill = createSVGElement("path");
196
+ setAttrs(fill, {
197
+ d: mark.path,
198
+ fill: resolveMarkFill(mark.fill, currentGradientMap),
199
+ "fill-opacity": mark.fillOpacity,
200
+ stroke: "none"
201
+ });
202
+ g.appendChild(fill);
203
+ if (mark.stroke && mark.topPath) {
204
+ const strokePath = createSVGElement("path");
205
+ strokePath.setAttribute("class", "oc-area-top");
206
+ setAttrs(strokePath, {
207
+ d: mark.topPath,
208
+ fill: "none",
209
+ stroke: mark.stroke,
210
+ "stroke-width": mark.strokeWidth ?? 1
211
+ });
212
+ g.appendChild(strokePath);
213
+ }
214
+ }
215
+ return g;
216
+ }
217
+ function _rectPathWithCorners(mark, sides) {
218
+ const { x, y, width: w, height: h } = mark;
219
+ const r = Math.max(0, Math.min(mark.cornerRadius ?? 0, w / 2, h / 2));
220
+ const tl = sides.tl ? r : 0;
221
+ const tr = sides.tr ? r : 0;
222
+ const br = sides.br ? r : 0;
223
+ const bl = sides.bl ? r : 0;
224
+ return [
225
+ `M${x + tl},${y}`,
226
+ `H${x + w - tr}`,
227
+ tr ? `A${tr},${tr} 0 0 1 ${x + w},${y + tr}` : "",
228
+ `V${y + h - br}`,
229
+ br ? `A${br},${br} 0 0 1 ${x + w - br},${y + h}` : "",
230
+ `H${x + bl}`,
231
+ bl ? `A${bl},${bl} 0 0 1 ${x},${y + h - bl}` : "",
232
+ `V${y + tl}`,
233
+ tl ? `A${tl},${tl} 0 0 1 ${x + tl},${y}` : "",
234
+ "Z"
235
+ ].filter(Boolean).join(" ");
236
+ }
237
+ function renderRectMark(mark, index) {
238
+ const g = createSVGElement("g");
239
+ g.setAttribute("data-mark-id", `rect-${index}`);
240
+ g.setAttribute("class", "oc-mark oc-mark-rect");
241
+ stampAnimationAttrs(g, mark, index);
242
+ if (currentAnimation?.enabled && mark.orient === "horizontal") {
243
+ g.setAttribute("data-orient", "horizontal");
244
+ }
245
+ const sides = mark.cornerRadiusSides;
246
+ const partialCorners = !!sides && (!sides.tl || !sides.tr || !sides.br || !sides.bl) && !!mark.cornerRadius;
247
+ const shapeEl = partialCorners ? createSVGElement("path") : createSVGElement("rect");
248
+ if (partialCorners) {
249
+ shapeEl.setAttribute("d", _rectPathWithCorners(mark, sides));
250
+ } else {
251
+ setAttrs(shapeEl, {
252
+ x: mark.x,
253
+ y: mark.y,
254
+ width: mark.width,
255
+ height: mark.height
256
+ });
257
+ if (mark.cornerRadius) {
258
+ setAttrs(shapeEl, { rx: mark.cornerRadius, ry: mark.cornerRadius });
259
+ }
260
+ }
261
+ shapeEl.setAttribute("fill", String(resolveMarkFill(mark.fill, currentGradientMap)));
262
+ if (mark.stroke) {
263
+ shapeEl.setAttribute("stroke", mark.stroke);
264
+ }
265
+ if (mark.strokeWidth) {
266
+ shapeEl.setAttribute("stroke-width", String(mark.strokeWidth));
267
+ }
268
+ g.appendChild(shapeEl);
269
+ return g;
270
+ }
271
+ function renderArcMark(mark, index) {
272
+ const g = createSVGElement("g");
273
+ g.setAttribute("data-mark-id", `arc-${index}`);
274
+ g.setAttribute("class", "oc-mark oc-mark-arc");
275
+ g.setAttribute("transform", `translate(${mark.center.x},${mark.center.y})`);
276
+ stampAnimationAttrs(g, mark, index);
277
+ const path = createSVGElement("path");
278
+ setAttrs(path, {
279
+ d: mark.path,
280
+ fill: resolveMarkFill(mark.fill, currentGradientMap),
281
+ stroke: mark.stroke,
282
+ "stroke-width": mark.strokeWidth
283
+ });
284
+ g.appendChild(path);
285
+ if (mark.label?.visible) {
286
+ const label = createSVGElement("text");
287
+ label.setAttribute("class", "oc-mark-label");
288
+ setAttrs(label, {
289
+ x: mark.label.x - mark.center.x,
290
+ y: mark.label.y - mark.center.y
291
+ });
292
+ applyTextStyle(label, mark.label.style);
293
+ label.textContent = mark.label.text;
294
+ g.appendChild(label);
295
+ }
296
+ return g;
297
+ }
298
+ function renderPointMark(mark, index) {
299
+ const circle = createSVGElement("circle");
300
+ circle.setAttribute("data-mark-id", `point-${index}`);
301
+ circle.setAttribute("class", "oc-mark oc-mark-point");
302
+ stampAnimationAttrs(circle, mark, index);
303
+ setAttrs(circle, {
304
+ cx: mark.cx,
305
+ cy: mark.cy,
306
+ r: mark.r,
307
+ fill: resolveMarkFill(mark.fill, currentGradientMap),
308
+ stroke: mark.stroke,
309
+ "stroke-width": mark.strokeWidth
310
+ });
311
+ if (mark.fillOpacity !== void 0) {
312
+ circle.setAttribute("fill-opacity", String(mark.fillOpacity));
313
+ }
314
+ return circle;
315
+ }
316
+ function renderTextMark(mark, index) {
317
+ const text = createSVGElement("text");
318
+ text.setAttribute("data-mark-id", `textMark-${index}`);
319
+ text.setAttribute("class", "oc-mark oc-mark-text");
320
+ stampAnimationAttrs(text, mark, index);
321
+ setAttrs(text, {
322
+ x: mark.x,
323
+ y: mark.y,
324
+ "font-size": mark.fontSize,
325
+ "text-anchor": mark.textAnchor
326
+ });
327
+ text.style.setProperty("fill", mark.fill);
328
+ if (mark.fontWeight) {
329
+ text.setAttribute("font-weight", String(mark.fontWeight));
330
+ }
331
+ if (mark.fontFamily) {
332
+ text.setAttribute("font-family", mark.fontFamily);
333
+ }
334
+ if (mark.angle) {
335
+ text.setAttribute("transform", `rotate(${mark.angle}, ${mark.x}, ${mark.y})`);
336
+ }
337
+ text.textContent = mark.text;
338
+ return text;
339
+ }
340
+ function renderRuleMark(mark, index) {
341
+ const line = createSVGElement("line");
342
+ line.setAttribute("data-mark-id", `rule-${index}`);
343
+ line.setAttribute("class", "oc-mark oc-mark-rule");
344
+ stampAnimationAttrs(line, mark, index);
345
+ setAttrs(line, {
346
+ x1: mark.x1,
347
+ y1: mark.y1,
348
+ x2: mark.x2,
349
+ y2: mark.y2,
350
+ stroke: mark.stroke,
351
+ "stroke-width": mark.strokeWidth
352
+ });
353
+ if (mark.strokeDasharray) {
354
+ line.setAttribute("stroke-dasharray", mark.strokeDasharray);
355
+ }
356
+ if (mark.opacity != null) {
357
+ line.setAttribute("opacity", String(mark.opacity));
358
+ }
359
+ return line;
360
+ }
361
+ function renderTickMark(mark, index) {
362
+ const line = createSVGElement("line");
363
+ line.setAttribute("data-mark-id", `tick-${index}`);
364
+ line.setAttribute("class", "oc-mark oc-mark-tick");
365
+ stampAnimationAttrs(line, mark, index);
366
+ const half = mark.length / 2;
367
+ if (mark.orient === "vertical") {
368
+ setAttrs(line, {
369
+ x1: mark.x,
370
+ y1: mark.y - half,
371
+ x2: mark.x,
372
+ y2: mark.y + half,
373
+ stroke: mark.stroke,
374
+ "stroke-width": mark.strokeWidth
375
+ });
376
+ } else {
377
+ setAttrs(line, {
378
+ x1: mark.x - half,
379
+ y1: mark.y,
380
+ x2: mark.x + half,
381
+ y2: mark.y,
382
+ stroke: mark.stroke,
383
+ "stroke-width": mark.strokeWidth
384
+ });
385
+ }
386
+ if (mark.opacity != null) {
387
+ line.setAttribute("opacity", String(mark.opacity));
388
+ }
389
+ return line;
390
+ }
391
+ registerMarkRenderer("line", renderLineMark);
392
+ registerMarkRenderer("area", renderAreaMark);
393
+ registerMarkRenderer("rect", renderRectMark);
394
+ registerMarkRenderer("arc", renderArcMark);
395
+ registerMarkRenderer("point", renderPointMark);
396
+ registerMarkRenderer("textMark", renderTextMark);
397
+ registerMarkRenderer("rule", renderRuleMark);
398
+ registerMarkRenderer("tick", renderTickMark);
399
+ function getMarkSeries(mark) {
400
+ if (mark.type === "line" || mark.type === "area") {
401
+ return mark.seriesKey;
402
+ }
403
+ if (mark.type === "arc") {
404
+ return mark.aria.label?.split(":")[0]?.trim();
405
+ }
406
+ if (mark.aria?.label) {
407
+ const beforeColon = mark.aria.label.split(":")[0]?.trim();
408
+ if (beforeColon) return beforeColon;
409
+ }
410
+ return void 0;
411
+ }
412
+ function renderMarks(parent, layout) {
413
+ const g = createSVGElement("g");
414
+ g.setAttribute("class", "oc-marks");
415
+ for (let i = 0; i < layout.marks.length; i++) {
416
+ const mark = layout.marks[i];
417
+ const renderer = markRenderers[mark.type];
418
+ if (!renderer) continue;
419
+ const el = renderer(mark, i);
420
+ if (mark.aria?.decorative) {
421
+ el.setAttribute("aria-hidden", "true");
422
+ } else if (mark.aria?.label) {
423
+ el.setAttribute("aria-label", mark.aria.label);
424
+ }
425
+ const series = getMarkSeries(mark);
426
+ if (series) {
427
+ el.setAttribute("data-series", series);
428
+ }
429
+ if (currentAnimation?.enabled && mark.type === "rect") {
430
+ const rect = mark;
431
+ if (rect.stackGroup && rect.stackPos !== void 0) {
432
+ el.setAttribute("data-stack-pos", String(rect.stackPos));
433
+ el.style.setProperty(
434
+ "--oc-stack-pos",
435
+ String(rect.stackPos)
436
+ );
437
+ }
438
+ }
439
+ g.appendChild(el);
440
+ }
441
+ parent.appendChild(g);
442
+ let labelsGroup;
443
+ for (let i = 0; i < layout.marks.length; i++) {
444
+ const mark = layout.marks[i];
445
+ if (mark.type !== "rect") continue;
446
+ const rect = mark;
447
+ if (!rect.label?.visible) continue;
448
+ if (!labelsGroup) {
449
+ labelsGroup = createSVGElement("g");
450
+ labelsGroup.setAttribute("class", "oc-mark-labels");
451
+ }
452
+ const label = createSVGElement("text");
453
+ label.setAttribute("class", "oc-mark-label");
454
+ setAttrs(label, { x: rect.label.x, y: rect.label.y });
455
+ applyTextStyle(label, rect.label.style);
456
+ label.textContent = rect.label.text;
457
+ if (currentAnimation?.enabled) {
458
+ const idx = rect.animationIndex ?? i;
459
+ label.setAttribute("data-animation-index", String(idx));
460
+ label.style.setProperty(
461
+ "--oc-mark-index",
462
+ String(idx)
463
+ );
464
+ }
465
+ labelsGroup.appendChild(label);
466
+ }
467
+ return labelsGroup;
468
+ }
469
+
470
+ // src/svg-renderer.ts
471
+ import { clampStaggerDelay } from "@opendata-ai/openchart-engine";
472
+
473
+ // src/renderers/annotations.ts
474
+ function renderCurvedArrow(parent, from, to, stroke) {
475
+ const pad = 6;
476
+ const tipY = to.y - pad;
477
+ const dy = tipY - from.y;
478
+ const dist = Math.sqrt((to.x - from.x) ** 2 + dy ** 2) || 1;
479
+ const arrowLen = 8;
480
+ const arrowWidth = 4;
481
+ const bulge = Math.max(dist * 0.4, 35);
482
+ const cp1x = from.x + bulge;
483
+ const cp1y = from.y + dy * 0.35;
484
+ const cp2x = to.x;
485
+ const cp2y = tipY - Math.abs(dy) * 0.25;
486
+ const tx = to.x - cp2x;
487
+ const ty = tipY - cp2y;
488
+ const tLen = Math.sqrt(tx * tx + ty * ty) || 1;
489
+ const ux = tx / tLen;
490
+ const uy = ty / tLen;
491
+ const baseX = to.x - ux * arrowLen;
492
+ const baseY = tipY - uy * arrowLen;
493
+ const path = createSVGElement("path");
494
+ path.setAttribute("class", "oc-annotation-connector");
495
+ setAttrs(path, {
496
+ d: `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${baseX} ${baseY}`,
497
+ fill: "none",
498
+ stroke,
499
+ "stroke-width": 1.5
500
+ });
501
+ parent.appendChild(path);
502
+ const px = -uy;
503
+ const py = ux;
504
+ const arrow = createSVGElement("polygon");
505
+ arrow.setAttribute("class", "oc-annotation-connector");
506
+ setAttrs(arrow, {
507
+ points: [
508
+ `${to.x},${tipY}`,
509
+ `${baseX + px * arrowWidth},${baseY + py * arrowWidth}`,
510
+ `${baseX - px * arrowWidth},${baseY - py * arrowWidth}`
511
+ ].join(" "),
512
+ fill: stroke
513
+ });
514
+ parent.appendChild(arrow);
515
+ }
516
+ function renderAnnotation(parent, annotation, index, bgColor) {
517
+ const g = createSVGElement("g");
518
+ g.setAttribute("class", `oc-annotation oc-annotation-${annotation.type}`);
519
+ g.setAttribute("data-annotation-index", String(index));
520
+ if (annotation.id) {
521
+ g.setAttribute("data-annotation-id", annotation.id);
522
+ }
523
+ if (annotation.rect) {
524
+ const rect = createSVGElement("rect");
525
+ rect.setAttribute("class", "oc-annotation-range");
526
+ rect.setAttribute("pointer-events", "none");
527
+ setAttrs(rect, {
528
+ x: annotation.rect.x,
529
+ y: annotation.rect.y,
530
+ width: annotation.rect.width,
531
+ height: annotation.rect.height
532
+ });
533
+ if (annotation.fill) rect.setAttribute("fill", annotation.fill);
534
+ if (annotation.opacity !== void 0) {
535
+ rect.setAttribute("fill-opacity", String(annotation.opacity));
536
+ }
537
+ g.appendChild(rect);
538
+ }
539
+ if (annotation.line) {
540
+ const line = createSVGElement("line");
541
+ line.setAttribute("class", "oc-annotation-line");
542
+ setAttrs(line, {
543
+ x1: annotation.line.start.x,
544
+ y1: annotation.line.start.y,
545
+ x2: annotation.line.end.x,
546
+ y2: annotation.line.end.y,
547
+ "stroke-width": annotation.strokeWidth ?? 1
548
+ });
549
+ if (annotation.stroke) line.setAttribute("stroke", annotation.stroke);
550
+ if (annotation.strokeDasharray) {
551
+ line.setAttribute("stroke-dasharray", annotation.strokeDasharray);
552
+ }
553
+ g.appendChild(line);
554
+ }
555
+ if (annotation.footnoteIndex != null && annotation.label) {
556
+ const cx = annotation.label.connector?.endpoint?.x ?? annotation.label.connector?.to.x ?? annotation.label.x;
557
+ const cy = annotation.label.connector?.endpoint?.y ?? annotation.label.connector?.to.y ?? annotation.label.y;
558
+ const r = 8;
559
+ const circle = createSVGElement("circle");
560
+ circle.setAttribute("class", "oc-annotation-footnote-marker");
561
+ setAttrs(circle, {
562
+ cx,
563
+ cy,
564
+ r,
565
+ fill: bgColor ?? "#ffffff",
566
+ stroke: annotation.label.style.fill ?? "#666",
567
+ "stroke-width": 1.5
568
+ });
569
+ g.appendChild(circle);
570
+ const num = createSVGElement("text");
571
+ num.setAttribute("class", "oc-annotation-footnote-number");
572
+ setAttrs(num, {
573
+ x: cx,
574
+ y: cy,
575
+ "dominant-baseline": "central",
576
+ "text-anchor": "middle"
577
+ });
578
+ applyTextStyle(num, {
579
+ ...annotation.label.style,
580
+ fontSize: 9,
581
+ fontWeight: 600
582
+ });
583
+ num.textContent = String(annotation.footnoteIndex);
584
+ g.appendChild(num);
585
+ parent.appendChild(g);
586
+ return;
587
+ }
588
+ if (annotation.label?.visible) {
589
+ if (annotation.label.connector) {
590
+ const c = annotation.label.connector;
591
+ if (c.style === "curve") {
592
+ renderCurvedArrow(g, c.from, c.to, c.stroke);
593
+ } else if (c.style === "drop-line") {
594
+ const connector = createSVGElement("line");
595
+ connector.setAttribute("class", "oc-annotation-connector oc-annotation-drop-line");
596
+ setAttrs(connector, {
597
+ x1: c.from.x,
598
+ y1: c.from.y,
599
+ x2: c.to.x,
600
+ y2: c.to.y,
601
+ stroke: c.stroke,
602
+ "stroke-width": 1,
603
+ "stroke-opacity": 0.6,
604
+ "shape-rendering": "crispEdges"
605
+ });
606
+ g.appendChild(connector);
607
+ } else {
608
+ const connector = createSVGElement("line");
609
+ connector.setAttribute("class", "oc-annotation-connector");
610
+ setAttrs(connector, {
611
+ x1: c.from.x,
612
+ y1: c.from.y,
613
+ x2: c.to.x,
614
+ y2: c.to.y,
615
+ stroke: c.stroke,
616
+ "stroke-width": 1,
617
+ "stroke-opacity": 0.5
618
+ });
619
+ g.appendChild(connector);
620
+ }
621
+ if (c.endpoint && c.style !== "curve") {
622
+ const ring = createSVGElement("circle");
623
+ ring.setAttribute("class", "oc-annotation-endpoint-ring");
624
+ setAttrs(ring, {
625
+ cx: c.endpoint.x,
626
+ cy: c.endpoint.y,
627
+ r: 5,
628
+ fill: bgColor ?? "#ffffff",
629
+ stroke: c.stroke,
630
+ "stroke-width": 1.5
631
+ });
632
+ g.appendChild(ring);
633
+ const dot = createSVGElement("circle");
634
+ dot.setAttribute("class", "oc-annotation-endpoint-dot");
635
+ setAttrs(dot, {
636
+ cx: c.endpoint.x,
637
+ cy: c.endpoint.y,
638
+ r: 2,
639
+ fill: c.stroke
640
+ });
641
+ g.appendChild(dot);
642
+ }
643
+ }
644
+ if (annotation.dot) {
645
+ const dot = createSVGElement("circle");
646
+ dot.setAttribute("class", "oc-annotation-dot");
647
+ setAttrs(dot, {
648
+ cx: annotation.dot.x,
649
+ cy: annotation.dot.y,
650
+ r: annotation.dot.radius,
651
+ fill: annotation.dot.fill,
652
+ stroke: annotation.dot.stroke,
653
+ "stroke-width": annotation.dot.strokeWidth
654
+ });
655
+ g.appendChild(dot);
656
+ }
657
+ const text = createSVGElement("text");
658
+ text.setAttribute("class", "oc-annotation-label");
659
+ setAttrs(text, { x: annotation.label.x, y: annotation.label.y });
660
+ applyTextStyle(text, annotation.label.style);
661
+ const lines = annotation.label.text.split("\n");
662
+ const fontSize = annotation.label.style.fontSize ?? 12;
663
+ const lineHeight = fontSize * (annotation.label.style.lineHeight ?? 1.3);
664
+ const isMultiLine = lines.length > 1;
665
+ if (isMultiLine) {
666
+ for (let i = 0; i < lines.length; i++) {
667
+ const tspan = createSVGElement("tspan");
668
+ setAttrs(tspan, { x: annotation.label.x, dy: i === 0 ? 0 : lineHeight });
669
+ tspan.textContent = lines[i];
670
+ text.appendChild(tspan);
671
+ }
672
+ } else {
673
+ text.textContent = annotation.label.text;
674
+ }
675
+ if (annotation.label.background) {
676
+ const pad = 3;
677
+ let bgX;
678
+ let bgY;
679
+ let bgW;
680
+ let bgH;
681
+ if (annotation.label.bounds) {
682
+ const b = annotation.label.bounds;
683
+ bgX = b.x - pad;
684
+ bgY = b.y - pad;
685
+ bgW = b.width + pad * 2;
686
+ bgH = b.height + pad * 2;
687
+ } else {
688
+ const charWidth = fontSize * 0.55;
689
+ const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
690
+ const totalHeight = lines.length * lineHeight;
691
+ bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
692
+ bgY = annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad;
693
+ bgW = maxLineWidth + pad * 2;
694
+ bgH = totalHeight + pad * 2;
695
+ }
696
+ const bgRect = createSVGElement("rect");
697
+ bgRect.setAttribute("class", "oc-annotation-bg");
698
+ setAttrs(bgRect, {
699
+ x: bgX,
700
+ y: bgY,
701
+ width: bgW,
702
+ height: bgH,
703
+ fill: annotation.label.background,
704
+ rx: 2
705
+ });
706
+ g.appendChild(bgRect);
707
+ } else if (bgColor && annotation.label.halo !== false) {
708
+ text.style.paintOrder = "stroke";
709
+ text.style.stroke = bgColor;
710
+ text.style.strokeWidth = `${Math.round(fontSize * 0.3)}px`;
711
+ text.style.strokeLinejoin = "round";
712
+ }
713
+ g.appendChild(text);
714
+ if (annotation.subtitle) {
715
+ const sub = createSVGElement("text");
716
+ sub.setAttribute("class", "oc-annotation-subtitle");
717
+ setAttrs(sub, { x: annotation.subtitle.x, y: annotation.subtitle.y });
718
+ applyTextStyle(sub, annotation.subtitle.style);
719
+ sub.textContent = annotation.subtitle.text;
720
+ g.appendChild(sub);
721
+ }
722
+ }
723
+ parent.appendChild(g);
724
+ }
725
+ function renderAnnotations(parent, layout) {
726
+ if (layout.annotations.length === 0) return;
727
+ const g = createSVGElement("g");
728
+ g.setAttribute("class", "oc-annotations");
729
+ const bgColor = layout.theme.colors.background;
730
+ for (let i = 0; i < layout.annotations.length; i++) {
731
+ renderAnnotation(g, layout.annotations[i], i, bgColor);
732
+ }
733
+ parent.appendChild(g);
734
+ }
735
+
736
+ // src/renderers/axes.ts
737
+ import {
738
+ axisTitleOffset,
739
+ estimateTextWidth,
740
+ getAxisTitleOffset,
741
+ TICK_LABEL_OFFSET,
742
+ textAscent
743
+ } from "@opendata-ai/openchart-core";
744
+ function appendCompoundLabel(parent, primaryText, subtitle, fontWeight) {
745
+ const primarySpan = createSVGElement("tspan");
746
+ primarySpan.setAttribute("font-weight", String(fontWeight));
747
+ primarySpan.textContent = primaryText;
748
+ parent.appendChild(primarySpan);
749
+ const subtitleSpan = createSVGElement("tspan");
750
+ subtitleSpan.setAttribute("dx", "0.5em");
751
+ subtitleSpan.textContent = subtitle;
752
+ subtitleSpan.setAttribute("font-weight", "400");
753
+ subtitleSpan.setAttribute("fill-opacity", "0.6");
754
+ parent.appendChild(subtitleSpan);
755
+ }
756
+ function renderAxis(parent, axis, orientation, layout) {
757
+ const g = createSVGElement("g");
758
+ const isRight = orientation === "y" && axis.orient === "right";
759
+ const isInlineY = orientation === "y" && axis.tickPosition === "inline" && !isRight;
760
+ g.setAttribute(
761
+ "class",
762
+ `oc-axis oc-axis-${isRight ? "y2" : orientation}${isInlineY ? " oc-axis-inline" : ""}`
763
+ );
764
+ const { area } = layout;
765
+ if (orientation === "x" && axis.domainLine !== false) {
766
+ const line = createSVGElement("line");
767
+ line.setAttribute("class", "oc-axis-line");
768
+ setAttrs(line, {
769
+ x1: axis.start.x,
770
+ y1: axis.start.y,
771
+ x2: axis.end.x,
772
+ y2: axis.end.y,
773
+ stroke: layout.theme.colors.axis,
774
+ "stroke-width": 1
775
+ });
776
+ g.appendChild(line);
777
+ }
778
+ for (const tick of axis.ticks) {
779
+ if (orientation === "x") {
780
+ const label = createSVGElement("text");
781
+ label.setAttribute("class", "oc-axis-tick");
782
+ if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
783
+ const labelX = tick.position;
784
+ const xLabelPad = axis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
785
+ const labelY = area.y + area.height + xLabelPad;
786
+ setAttrs(label, {
787
+ x: labelX,
788
+ y: labelY,
789
+ "text-anchor": axis.tickAngle < 0 ? "end" : "start",
790
+ "dominant-baseline": "central",
791
+ transform: `rotate(${axis.tickAngle}, ${labelX}, ${labelY})`
792
+ });
793
+ } else {
794
+ const xLabelPad = axis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
795
+ setAttrs(label, {
796
+ x: tick.position,
797
+ y: area.y + area.height + xLabelPad + textAscent(axis.tickLabelStyle.fontSize),
798
+ "text-anchor": "middle"
799
+ });
800
+ }
801
+ applyTextStyle(label, axis.tickLabelStyle);
802
+ label.textContent = tick.label;
803
+ g.appendChild(label);
804
+ } else if (isInlineY) {
805
+ const label = createSVGElement("text");
806
+ label.setAttribute("class", "oc-axis-tick oc-axis-tick-inline");
807
+ setAttrs(label, {
808
+ x: area.x,
809
+ y: tick.position - 6,
810
+ "text-anchor": "start"
811
+ });
812
+ applyTextStyle(label, axis.tickLabelStyle);
813
+ label.textContent = tick.label;
814
+ g.appendChild(label);
815
+ } else {
816
+ const label = createSVGElement("text");
817
+ label.setAttribute("class", "oc-axis-tick");
818
+ setAttrs(label, {
819
+ x: isRight ? area.x + area.width + TICK_LABEL_OFFSET : area.x - TICK_LABEL_OFFSET,
820
+ y: tick.position,
821
+ "text-anchor": isRight ? "start" : "end",
822
+ "dominant-baseline": "central"
823
+ });
824
+ applyTextStyle(label, axis.tickLabelStyle);
825
+ if (!isRight) {
826
+ const availableWidth = area.x - TICK_LABEL_OFFSET;
827
+ const fontSize = axis.tickLabelStyle.fontSize;
828
+ const fontWeight = axis.tickLabelStyle.fontWeight;
829
+ if (tick.subtitle) {
830
+ const gapWidth = fontSize * 0.5;
831
+ const subtitleWidth = estimateTextWidth(tick.subtitle, fontSize, fontWeight);
832
+ const primaryWidth = estimateTextWidth(tick.label, fontSize, fontWeight);
833
+ const totalWidth = primaryWidth + gapWidth + subtitleWidth;
834
+ if (totalWidth > availableWidth && availableWidth > 20) {
835
+ const ellipsis = "\u2026";
836
+ const ellipsisWidth = estimateTextWidth(ellipsis, fontSize, fontWeight);
837
+ const budgetForPrimary = availableWidth - gapWidth - subtitleWidth - ellipsisWidth;
838
+ let primaryText = tick.label;
839
+ if (budgetForPrimary > 0) {
840
+ let lo = 0;
841
+ let hi = tick.label.length;
842
+ while (lo < hi) {
843
+ const mid = lo + hi + 1 >>> 1;
844
+ const candidate = tick.label.slice(0, mid);
845
+ if (estimateTextWidth(candidate, fontSize, fontWeight) <= budgetForPrimary) {
846
+ lo = mid;
847
+ } else {
848
+ hi = mid - 1;
849
+ }
850
+ }
851
+ primaryText = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
852
+ } else {
853
+ primaryText = ellipsis;
854
+ }
855
+ appendCompoundLabel(label, primaryText, tick.subtitle, fontWeight);
856
+ const titleEl = createSVGElement("title");
857
+ titleEl.textContent = `${tick.label} ${tick.subtitle}`;
858
+ label.appendChild(titleEl);
859
+ } else {
860
+ appendCompoundLabel(label, tick.label, tick.subtitle, fontWeight);
861
+ }
862
+ } else {
863
+ const fullWidth = estimateTextWidth(tick.label, fontSize, fontWeight);
864
+ if (fullWidth > availableWidth && availableWidth > 20) {
865
+ const ellipsis = "\u2026";
866
+ const ellipsisWidth = estimateTextWidth(ellipsis, fontSize, fontWeight);
867
+ let lo = 0;
868
+ let hi = tick.label.length;
869
+ while (lo < hi) {
870
+ const mid = lo + hi + 1 >>> 1;
871
+ const candidate = tick.label.slice(0, mid);
872
+ if (estimateTextWidth(candidate, fontSize, fontWeight) + ellipsisWidth <= availableWidth) {
873
+ lo = mid;
874
+ } else {
875
+ hi = mid - 1;
876
+ }
877
+ }
878
+ label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
879
+ const titleEl = createSVGElement("title");
880
+ titleEl.textContent = tick.label;
881
+ label.appendChild(titleEl);
882
+ } else {
883
+ label.textContent = tick.label;
884
+ }
885
+ }
886
+ } else {
887
+ label.textContent = tick.label;
888
+ }
889
+ g.appendChild(label);
890
+ }
891
+ }
892
+ if (!isRight) {
893
+ for (const gridline of axis.gridlines) {
894
+ const gl = createSVGElement("line");
895
+ gl.setAttribute("class", "oc-gridline");
896
+ if (orientation === "y") {
897
+ setAttrs(gl, {
898
+ x1: area.x,
899
+ y1: gridline.position,
900
+ x2: area.x + area.width,
901
+ y2: gridline.position,
902
+ stroke: layout.theme.colors.gridline,
903
+ "stroke-width": 1,
904
+ "stroke-opacity": 0.6
905
+ });
906
+ } else {
907
+ setAttrs(gl, {
908
+ x1: gridline.position,
909
+ y1: area.y,
910
+ x2: gridline.position,
911
+ y2: area.y + area.height,
912
+ stroke: layout.theme.colors.gridline,
913
+ "stroke-width": 1,
914
+ "stroke-opacity": 0.6
915
+ });
916
+ }
917
+ g.appendChild(gl);
918
+ }
919
+ }
920
+ if (axis.label && axis.labelStyle) {
921
+ const axisLabel = createSVGElement("text");
922
+ axisLabel.setAttribute("class", "oc-axis-title");
923
+ applyTextStyle(axisLabel, axis.labelStyle);
924
+ axisLabel.textContent = axis.label;
925
+ const tp = axis.titlePosition;
926
+ if (tp) {
927
+ const attrs = {
928
+ x: tp.x,
929
+ y: tp.y,
930
+ "text-anchor": "middle"
931
+ };
932
+ if (tp.angle) {
933
+ attrs.transform = `rotate(${tp.angle}, ${tp.x}, ${tp.y})`;
934
+ }
935
+ setAttrs(axisLabel, attrs);
936
+ } else if (orientation === "x") {
937
+ let titleY = area.y + area.height + 35;
938
+ if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
939
+ const angleRad = Math.abs(axis.tickAngle) * (Math.PI / 180);
940
+ let maxLabelWidth = 40;
941
+ for (const tick of axis.ticks) {
942
+ const w = estimateTextWidth(
943
+ tick.label,
944
+ axis.tickLabelStyle.fontSize,
945
+ axis.tickLabelStyle.fontWeight
946
+ );
947
+ if (w > maxLabelWidth) maxLabelWidth = w;
948
+ }
949
+ const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
950
+ titleY = area.y + area.height + rotatedHeight + 14;
951
+ }
952
+ setAttrs(axisLabel, {
953
+ x: area.x + area.width / 2,
954
+ y: titleY,
955
+ "text-anchor": "middle"
956
+ });
957
+ } else if (isRight) {
958
+ const titleOffset = getAxisTitleOffset(layout.dimensions.width);
959
+ const titleX = area.x + area.width + titleOffset;
960
+ setAttrs(axisLabel, {
961
+ x: titleX,
962
+ y: area.y + area.height / 2,
963
+ "text-anchor": "middle",
964
+ transform: `rotate(90, ${titleX}, ${area.y + area.height / 2})`
965
+ });
966
+ } else {
967
+ const maxTickLabelWidth = axis.ticks.reduce((max, t) => {
968
+ const w = estimateTextWidth(
969
+ t.label,
970
+ axis.tickLabelStyle.fontSize,
971
+ axis.tickLabelStyle.fontWeight ?? 400
972
+ );
973
+ return Math.max(max, w);
974
+ }, 0);
975
+ const titleOffset = axisTitleOffset(
976
+ maxTickLabelWidth,
977
+ axis.labelStyle.fontSize,
978
+ layout.dimensions.width
979
+ );
980
+ setAttrs(axisLabel, {
981
+ x: area.x - titleOffset,
982
+ y: area.y + area.height / 2,
983
+ "text-anchor": "middle",
984
+ transform: `rotate(-90, ${area.x - titleOffset}, ${area.y + area.height / 2})`
985
+ });
986
+ }
987
+ g.appendChild(axisLabel);
988
+ }
989
+ parent.appendChild(g);
990
+ }
991
+ function renderAxes(parent, layout) {
992
+ if (layout.axes.x) {
993
+ renderAxis(parent, layout.axes.x, "x", layout);
994
+ }
995
+ if (layout.axes.y) {
996
+ renderAxis(parent, layout.axes.y, "y", layout);
997
+ }
998
+ if (layout.axes.y2) {
999
+ renderAxis(parent, layout.axes.y2, "y", layout);
1000
+ }
1001
+ }
1002
+
1003
+ // src/renderers/brand.ts
1004
+ import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH, textAscent as textAscent2 } from "@opendata-ai/openchart-core";
1005
+ var BRAND_URL = "https://tryopendata.ai";
1006
+ function renderBrand(parent, layout) {
1007
+ if (layout.dimensions.width < BRAND_MIN_WIDTH) return;
1008
+ const { width } = layout.dimensions;
1009
+ const padding = layout.theme.spacing.padding;
1010
+ const rightEdge = width - padding;
1011
+ const fill = layout.theme.colors.axis;
1012
+ const { chrome } = layout;
1013
+ const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
1014
+ const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
1015
+ const { legend } = layout;
1016
+ const bottomLegendOffset = legend.position === "bottom" && legend.bounds.height > 0 ? legend.bounds.height + 8 : 0;
1017
+ const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter + bottomLegendOffset;
1018
+ const a = createSVGElement("a");
1019
+ a.setAttribute("href", BRAND_URL);
1020
+ a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
1021
+ a.setAttribute("target", "_blank");
1022
+ a.setAttribute("rel", "noopener");
1023
+ a.setAttribute("class", "oc-chrome-ref");
1024
+ const BRAND_LARGE = 16;
1025
+ const text = createSVGElement("text");
1026
+ setAttrs(text, {
1027
+ x: rightEdge,
1028
+ y: chromeY + textAscent2(BRAND_LARGE),
1029
+ "font-family": layout.theme.fonts.family,
1030
+ "font-size": BRAND_FONT_SIZE,
1031
+ "text-anchor": "end",
1032
+ "fill-opacity": 0.55
1033
+ });
1034
+ text.style.setProperty("fill", fill);
1035
+ const trySpan = createSVGElement("tspan");
1036
+ trySpan.setAttribute("font-weight", "500");
1037
+ trySpan.textContent = "try";
1038
+ text.appendChild(trySpan);
1039
+ const openDataSpan = createSVGElement("tspan");
1040
+ openDataSpan.setAttribute("font-weight", "600");
1041
+ openDataSpan.setAttribute("font-size", String(BRAND_LARGE));
1042
+ openDataSpan.textContent = "OpenData";
1043
+ text.appendChild(openDataSpan);
1044
+ const aiSpan = createSVGElement("tspan");
1045
+ aiSpan.setAttribute("font-weight", "500");
1046
+ aiSpan.textContent = ".ai";
1047
+ text.appendChild(aiSpan);
1048
+ a.appendChild(text);
1049
+ parent.appendChild(a);
1050
+ }
1051
+
1052
+ // src/renderers/chrome.ts
1053
+ import { estimateTextWidth as estimateTextWidth2, textAscent as textAscent3, wrapText } from "@opendata-ai/openchart-core";
1054
+ function renderChromeElement(parent, element, className, chromeKey, measureText, uppercase = false) {
1055
+ const text = createSVGElement("text");
1056
+ setAttrs(text, { x: element.x, y: element.y + textAscent3(element.style.fontSize) });
1057
+ applyTextStyle(text, element.style);
1058
+ text.setAttribute("class", className);
1059
+ text.setAttribute("data-chrome-key", chromeKey);
1060
+ const renderedText = uppercase ? element.text.toUpperCase() : element.text;
1061
+ const lines = wrapText(
1062
+ renderedText,
1063
+ element.style.fontSize,
1064
+ element.style.fontWeight,
1065
+ element.maxWidth,
1066
+ measureText
1067
+ );
1068
+ if (lines.length === 1) {
1069
+ text.textContent = renderedText;
1070
+ } else {
1071
+ const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
1072
+ for (let i = 0; i < lines.length; i++) {
1073
+ const tspan = createSVGElement("tspan");
1074
+ setAttrs(tspan, { x: element.x, dy: i === 0 ? 0 : lineHeight });
1075
+ tspan.textContent = lines[i];
1076
+ text.appendChild(tspan);
1077
+ }
1078
+ }
1079
+ parent.appendChild(text);
1080
+ }
1081
+ function renderChrome(parent, layout) {
1082
+ const g = createSVGElement("g");
1083
+ g.setAttribute("class", "oc-chrome");
1084
+ const { chrome, measureText } = layout;
1085
+ if (chrome.eyebrow) {
1086
+ const eyebrow = chrome.eyebrow;
1087
+ const dotR = 3;
1088
+ const dotGap = 8;
1089
+ const dotX = eyebrow.x + dotR;
1090
+ const dotY = eyebrow.y + eyebrow.style.fontSize * 0.42;
1091
+ const dot = createSVGElement("circle");
1092
+ dot.setAttribute("class", "oc-eyebrow-dot");
1093
+ setAttrs(dot, { cx: dotX, cy: dotY, r: dotR });
1094
+ dot.setAttribute("fill", eyebrow.style.fill ?? "currentColor");
1095
+ g.appendChild(dot);
1096
+ const shifted = {
1097
+ ...eyebrow,
1098
+ x: eyebrow.x + dotR * 2 + dotGap
1099
+ };
1100
+ renderChromeElement(g, shifted, "oc-eyebrow", "eyebrow", measureText, true);
1101
+ }
1102
+ if (chrome.title) {
1103
+ renderChromeElement(g, chrome.title, "oc-title", "title", measureText);
1104
+ }
1105
+ if (chrome.subtitle) {
1106
+ renderChromeElement(g, chrome.subtitle, "oc-subtitle", "subtitle", measureText);
1107
+ }
1108
+ const bottomOffset = layout.chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
1109
+ let footnoteBandHeight = 0;
1110
+ if (chrome.footnotes && chrome.footnotes.length > 0) {
1111
+ const fontSize = layout.theme.fonts.sizes.small;
1112
+ const pad = layout.theme.spacing.padding;
1113
+ const lineHeight = fontSize * 1.3;
1114
+ const style = {
1115
+ fontFamily: layout.theme.fonts.family,
1116
+ fontSize,
1117
+ fontWeight: layout.theme.fonts.weights.normal,
1118
+ fill: layout.theme.chrome.source.color,
1119
+ lineHeight: 1.3,
1120
+ textAnchor: "start"
1121
+ };
1122
+ for (let i = 0; i < chrome.footnotes.length; i++) {
1123
+ const f = chrome.footnotes[i];
1124
+ const y = bottomOffset + layout.theme.spacing.chartToFooter + textAscent3(fontSize) + i * lineHeight;
1125
+ const el = createSVGElement("text");
1126
+ el.setAttribute("class", "oc-footnotes");
1127
+ setAttrs(el, { x: pad, y });
1128
+ applyTextStyle(el, style);
1129
+ el.textContent = `${f.index}. ${f.text}`;
1130
+ g.appendChild(el);
1131
+ }
1132
+ footnoteBandHeight = chrome.footnotes.length * lineHeight + 4;
1133
+ }
1134
+ if (chrome.source) {
1135
+ renderChromeElement(
1136
+ g,
1137
+ { ...chrome.source, y: bottomOffset + chrome.source.y + footnoteBandHeight },
1138
+ "oc-source",
1139
+ "source",
1140
+ measureText
1141
+ );
1142
+ }
1143
+ if (chrome.byline) {
1144
+ renderChromeElement(
1145
+ g,
1146
+ { ...chrome.byline, y: bottomOffset + chrome.byline.y + footnoteBandHeight },
1147
+ "oc-byline",
1148
+ "byline",
1149
+ measureText
1150
+ );
1151
+ }
1152
+ if (chrome.footer) {
1153
+ renderChromeElement(
1154
+ g,
1155
+ { ...chrome.footer, y: bottomOffset + chrome.footer.y + footnoteBandHeight },
1156
+ "oc-footer",
1157
+ "footer",
1158
+ measureText
1159
+ );
1160
+ }
1161
+ if (chrome.brand) {
1162
+ const brandY = bottomOffset + chrome.brand.y + footnoteBandHeight;
1163
+ renderChromeElement(g, { ...chrome.brand, y: brandY }, "oc-brand", "brand", measureText);
1164
+ const textWidth = estimateTextWidth2(
1165
+ chrome.brand.text,
1166
+ chrome.brand.style.fontSize,
1167
+ chrome.brand.style.fontWeight
1168
+ );
1169
+ const dotX = chrome.brand.x - textWidth - 12;
1170
+ const dotY = brandY + chrome.brand.style.fontSize / 2;
1171
+ const dot = createSVGElement("circle");
1172
+ dot.setAttribute("class", "oc-brand-dot");
1173
+ setAttrs(dot, { cx: dotX, cy: dotY, r: 3 });
1174
+ g.appendChild(dot);
1175
+ }
1176
+ parent.appendChild(g);
1177
+ }
1178
+
1179
+ // src/renderers/endpoint-labels.ts
1180
+ var LEADER_STROKE_WIDTH = 1;
1181
+ var LEADER_OPACITY = 0.45;
1182
+ function renderColumn(root, entries, bounds, ep, leaderAnchorX, side) {
1183
+ const labelFontSize = ep.labelStyle.fontSize ?? 11;
1184
+ const labelLineHeight = labelFontSize * (ep.labelStyle.lineHeight ?? 1.25);
1185
+ const valueFontSize = ep.valueStyle.fontSize ?? 11;
1186
+ const chipX = bounds.x;
1187
+ const chipWidth = ep.swatchSize;
1188
+ const textX = chipX + chipWidth + ep.gap;
1189
+ for (let i = 0; i < entries.length; i++) {
1190
+ const entry = entries[i];
1191
+ const entryG = createSVGElement("g");
1192
+ entryG.setAttribute("class", "oc-endpoint-label-entry");
1193
+ entryG.setAttribute("role", "listitem");
1194
+ entryG.setAttribute("data-endpoint-index", String(i));
1195
+ entryG.setAttribute("data-endpoint-key", entry.seriesKey);
1196
+ entryG.setAttribute("data-endpoint-side", side);
1197
+ entryG.setAttribute("aria-label", `${entry.seriesKey}: ${entry.value}`);
1198
+ if (entry.showLeader) {
1199
+ const leader = createSVGElement("line");
1200
+ leader.setAttribute("class", "oc-endpoint-leader");
1201
+ setAttrs(leader, {
1202
+ x1: chipX,
1203
+ y1: entry.labelY + labelFontSize / 2,
1204
+ x2: leaderAnchorX,
1205
+ y2: entry.dataY,
1206
+ stroke: entry.color,
1207
+ "stroke-width": LEADER_STROKE_WIDTH,
1208
+ "stroke-opacity": LEADER_OPACITY
1209
+ });
1210
+ entryG.appendChild(leader);
1211
+ }
1212
+ const rowY = entry.labelY + labelFontSize / 2;
1213
+ const line = createSVGElement("line");
1214
+ line.setAttribute("class", "oc-endpoint-swatch-line");
1215
+ setAttrs(line, {
1216
+ x1: chipX,
1217
+ y1: rowY,
1218
+ x2: chipX + chipWidth,
1219
+ y2: rowY,
1220
+ stroke: entry.color,
1221
+ "stroke-width": 2,
1222
+ "stroke-linecap": "round"
1223
+ });
1224
+ entryG.appendChild(line);
1225
+ const label = createSVGElement("text");
1226
+ label.setAttribute("class", "oc-endpoint-label");
1227
+ setAttrs(label, { x: textX, y: entry.labelY + labelFontSize });
1228
+ applyTextStyle(label, ep.labelStyle);
1229
+ label.style.setProperty("fill", entry.color);
1230
+ if (entry.labelLines.length <= 1) {
1231
+ label.textContent = entry.labelLines[0] ?? entry.seriesKey;
1232
+ } else {
1233
+ for (let li = 0; li < entry.labelLines.length; li++) {
1234
+ const tspan = createSVGElement("tspan");
1235
+ setAttrs(tspan, { x: textX, dy: li === 0 ? 0 : labelLineHeight });
1236
+ tspan.textContent = entry.labelLines[li];
1237
+ label.appendChild(tspan);
1238
+ }
1239
+ }
1240
+ entryG.appendChild(label);
1241
+ const lineCount = Math.max(entry.labelLines.length, 1);
1242
+ const valueY = entry.labelY + labelFontSize + (lineCount - 1) * labelLineHeight + ep.valueGap + valueFontSize;
1243
+ const value = createSVGElement("text");
1244
+ value.setAttribute("class", "oc-endpoint-value");
1245
+ setAttrs(value, { x: textX, y: valueY });
1246
+ applyTextStyle(value, ep.valueStyle);
1247
+ value.textContent = entry.value;
1248
+ entryG.appendChild(value);
1249
+ if (entry.marker) {
1250
+ const marker = createSVGElement("circle");
1251
+ marker.setAttribute("class", "oc-endpoint-marker");
1252
+ setAttrs(marker, {
1253
+ cx: entry.marker.x,
1254
+ cy: entry.marker.y,
1255
+ r: entry.marker.radius,
1256
+ fill: entry.marker.fill,
1257
+ stroke: entry.marker.stroke,
1258
+ "stroke-width": entry.marker.strokeWidth
1259
+ });
1260
+ entryG.appendChild(marker);
1261
+ }
1262
+ root.appendChild(entryG);
1263
+ }
1264
+ }
1265
+ function renderEndpointLabels(parent, layout) {
1266
+ const ep = layout.endpointLabels;
1267
+ if (!ep || ep.entries.length === 0) return;
1268
+ const chartArea = layout.area;
1269
+ const root = createSVGElement("g");
1270
+ root.setAttribute("class", "oc-endpoint-labels");
1271
+ root.setAttribute("role", "list");
1272
+ root.setAttribute("aria-label", "Endpoint labels");
1273
+ const chartRightX = chartArea.x + chartArea.width;
1274
+ renderColumn(root, ep.entries, ep.bounds, ep, chartRightX, "trailing");
1275
+ if (ep.leading && ep.leading.length > 0 && ep.leadingBounds) {
1276
+ const chartLeftX = chartArea.x;
1277
+ renderColumn(root, ep.leading, ep.leadingBounds, ep, chartLeftX, "leading");
1278
+ }
1279
+ parent.appendChild(root);
1280
+ }
1281
+
1282
+ // src/renderers/legend.ts
1283
+ import { estimateTextWidth as estimateTextWidth3 } from "@opendata-ai/openchart-core";
1284
+ function isCategorical(legend) {
1285
+ return !legend.type || legend.type === "categorical";
1286
+ }
1287
+ function renderLegend(parent, legend) {
1288
+ if (!isCategorical(legend) || legend.entries.length === 0) return;
1289
+ const g = createSVGElement("g");
1290
+ g.setAttribute("class", "oc-legend");
1291
+ g.setAttribute("role", "list");
1292
+ g.setAttribute("aria-label", "Chart legend");
1293
+ const isHorizontal = legend.position === "top" || legend.position === "bottom";
1294
+ const positions = "entryPositions" in legend ? legend.entryPositions : void 0;
1295
+ let offsetX = legend.bounds.x;
1296
+ let offsetY = legend.bounds.y;
1297
+ for (let i = 0; i < legend.entries.length; i++) {
1298
+ const entry = legend.entries[i];
1299
+ const pos = positions?.[i];
1300
+ if (pos) {
1301
+ offsetX = pos.x;
1302
+ offsetY = pos.y;
1303
+ } else if (isHorizontal && i > 0) {
1304
+ const labelWidth = estimateTextWidth3(
1305
+ entry.label,
1306
+ legend.labelStyle.fontSize,
1307
+ legend.labelStyle.fontWeight
1308
+ );
1309
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
1310
+ if (offsetX + entryWidth > legend.bounds.x + legend.bounds.width) {
1311
+ offsetX = legend.bounds.x;
1312
+ offsetY += legend.swatchSize + 6;
1313
+ }
1314
+ }
1315
+ const entryG = createSVGElement("g");
1316
+ entryG.setAttribute("class", "oc-legend-entry");
1317
+ entryG.setAttribute("role", "listitem");
1318
+ entryG.setAttribute("data-legend-index", String(i));
1319
+ entryG.setAttribute("data-legend-label", entry.label);
1320
+ if (entry.overflow) {
1321
+ entryG.setAttribute("data-legend-overflow", "true");
1322
+ entryG.setAttribute("aria-label", entry.label);
1323
+ entryG.setAttribute("opacity", "0.5");
1324
+ } else {
1325
+ entryG.setAttribute(
1326
+ "aria-label",
1327
+ `${entry.label}: ${entry.active !== false ? "visible" : "hidden"}`
1328
+ );
1329
+ entryG.setAttribute("style", "cursor: pointer");
1330
+ if (entry.active === false) {
1331
+ entryG.setAttribute("opacity", "0.3");
1332
+ }
1333
+ }
1334
+ const midX = offsetX + legend.swatchSize / 2;
1335
+ const midY = offsetY + legend.swatchSize / 2;
1336
+ if (entry.shape === "circle") {
1337
+ const circle = createSVGElement("circle");
1338
+ setAttrs(circle, {
1339
+ cx: midX,
1340
+ cy: midY,
1341
+ r: legend.swatchSize / 2,
1342
+ fill: entry.color
1343
+ });
1344
+ entryG.appendChild(circle);
1345
+ } else if (entry.shape === "line") {
1346
+ const lineWidth = legend.swatchSize;
1347
+ const line = createSVGElement("line");
1348
+ line.setAttribute("class", "oc-legend-swatch-line");
1349
+ setAttrs(line, {
1350
+ x1: offsetX,
1351
+ y1: midY,
1352
+ x2: offsetX + lineWidth,
1353
+ y2: midY,
1354
+ stroke: entry.color,
1355
+ "stroke-width": 2,
1356
+ "stroke-linecap": "round"
1357
+ });
1358
+ entryG.appendChild(line);
1359
+ } else {
1360
+ const rectSize = Math.round(legend.swatchSize * 0.6);
1361
+ const rect = createSVGElement("rect");
1362
+ rect.setAttribute("class", "oc-legend-swatch-rect");
1363
+ setAttrs(rect, {
1364
+ x: offsetX + (legend.swatchSize - rectSize) / 2,
1365
+ y: midY - rectSize / 2,
1366
+ width: rectSize,
1367
+ height: rectSize,
1368
+ rx: 2,
1369
+ ry: 2,
1370
+ fill: entry.color
1371
+ });
1372
+ entryG.appendChild(rect);
1373
+ }
1374
+ const label = createSVGElement("text");
1375
+ setAttrs(label, {
1376
+ x: offsetX + legend.swatchSize + legend.swatchGap,
1377
+ y: offsetY + legend.swatchSize / 2,
1378
+ "dominant-baseline": "central"
1379
+ });
1380
+ applyTextStyle(label, legend.labelStyle);
1381
+ label.textContent = entry.label;
1382
+ entryG.appendChild(label);
1383
+ g.appendChild(entryG);
1384
+ if (!pos) {
1385
+ if (isHorizontal) {
1386
+ const labelWidth = estimateTextWidth3(
1387
+ entry.label,
1388
+ legend.labelStyle.fontSize,
1389
+ legend.labelStyle.fontWeight
1390
+ );
1391
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
1392
+ offsetX += entryWidth;
1393
+ } else {
1394
+ offsetY += "rowHeight" in legend && legend.rowHeight ? legend.rowHeight : legend.swatchSize + legend.entryGap;
1395
+ }
1396
+ }
1397
+ }
1398
+ parent.appendChild(g);
1399
+ }
1400
+
1401
+ // src/renderers/metrics.ts
1402
+ var DELTA_SIZE_RATIO = 12 / 22;
1403
+ function renderMetrics(parent, layout) {
1404
+ const bar = layout.metrics;
1405
+ if (!bar || bar.cells.length === 0) return;
1406
+ const labelSize = layout.theme.fonts.sizes.metricLabel;
1407
+ const valueSize = layout.theme.fonts.sizes.metricValue;
1408
+ const deltaSize = Math.round(valueSize * DELTA_SIZE_RATIO);
1409
+ const g = createSVGElement("g");
1410
+ g.setAttribute("class", "oc-metrics");
1411
+ for (const cell of bar.cells) {
1412
+ const label = createSVGElement("text");
1413
+ label.setAttribute("class", "oc-metric-label");
1414
+ setAttrs(label, { x: cell.x, y: cell.labelY, "font-size": labelSize });
1415
+ label.textContent = cell.metric.label.toUpperCase();
1416
+ g.appendChild(label);
1417
+ const value = createSVGElement("text");
1418
+ value.setAttribute("class", "oc-metric-value");
1419
+ setAttrs(value, { x: cell.x, y: cell.valueY, "font-size": valueSize });
1420
+ value.textContent = cell.metric.value;
1421
+ if (cell.metric.delta) {
1422
+ const delta = createSVGElement("tspan");
1423
+ const tone = cell.metric.deltaTone ?? "up";
1424
+ delta.setAttribute("class", tone === "down" ? "oc-metric-delta-down" : "oc-metric-delta-up");
1425
+ delta.setAttribute("dx", "8");
1426
+ delta.setAttribute("font-size", String(deltaSize));
1427
+ delta.textContent = cell.metric.delta;
1428
+ value.appendChild(delta);
1429
+ }
1430
+ if (cell.metric.secondary) {
1431
+ const secondary = createSVGElement("tspan");
1432
+ secondary.setAttribute("class", "oc-metric-secondary");
1433
+ secondary.setAttribute("dx", "6");
1434
+ secondary.setAttribute("font-size", String(deltaSize));
1435
+ secondary.textContent = cell.metric.secondary;
1436
+ value.appendChild(secondary);
1437
+ }
1438
+ g.appendChild(value);
1439
+ }
1440
+ parent.appendChild(g);
1441
+ }
1442
+
1443
+ // src/svg-renderer.ts
1444
+ var EASE_VAR_MAP = {
1445
+ smooth: "var(--oc-ease-smooth)",
1446
+ snappy: "var(--oc-ease-snappy)"
1447
+ };
1448
+ function renderChartSVG(layout, container, opts) {
1449
+ const { width, height } = layout.dimensions;
1450
+ const animation = layout.animation;
1451
+ const svg = createSVGElement("svg");
1452
+ setAttrs(svg, {
1453
+ viewBox: `0 0 ${width} ${height}`,
1454
+ xmlns: SVG_NS,
1455
+ // The SVG spec default is overflow:"hidden", which clips anything a hair
1456
+ // outside the viewBox. We now position all text on the alphabetic/central
1457
+ // baseline (dominant-baseline:hanging was dropped because WebKit computed
1458
+ // it from different metrics), but WebKit/iOS still reports getBBox extents
1459
+ // with a few pixels of slack around tspans, so text touching an edge can
1460
+ // still get clipped. overflow:visible avoids that. Chart marks are already
1461
+ // constrained by a clipPath, so nothing bleeds out.
1462
+ overflow: "visible",
1463
+ // Hint browsers to enable sub-pixel font hinting and kerning for chart text.
1464
+ "text-rendering": "optimizeLegibility"
1465
+ });
1466
+ svg.style.height = `${height}px`;
1467
+ svg.setAttribute("role", layout.a11y.role);
1468
+ svg.setAttribute("aria-label", layout.a11y.altText);
1469
+ if (layout.display === "sparkline") {
1470
+ svg.setAttribute("data-display", "sparkline");
1471
+ }
1472
+ const classes = opts?.animate ? "oc-chart oc-animate" : "oc-chart";
1473
+ svg.setAttribute("class", classes);
1474
+ if (animation?.enabled) {
1475
+ const markCount = layout.marks.length;
1476
+ const stagger = clampStaggerDelay(animation.staggerDelay, markCount);
1477
+ svg.style.setProperty("--oc-animation-duration", `${animation.duration}ms`);
1478
+ svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
1479
+ svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
1480
+ const easeVar = EASE_VAR_MAP[animation.ease] || EASE_VAR_MAP.smooth;
1481
+ svg.style.setProperty("--oc-animation-ease", easeVar);
1482
+ let maxSegments = 0;
1483
+ for (const m of layout.marks) {
1484
+ if (m.type === "rect") {
1485
+ const pos = m.stackPos;
1486
+ if (pos !== void 0 && pos + 1 > maxSegments) {
1487
+ maxSegments = pos + 1;
1488
+ }
1489
+ }
1490
+ }
1491
+ if (maxSegments > 0) {
1492
+ const segDuration = Math.round(animation.duration / maxSegments);
1493
+ svg.style.setProperty("--oc-stack-segment-duration", `${segDuration}ms`);
1494
+ }
1495
+ }
1496
+ if (layout.display !== "sparkline") {
1497
+ const bg = createSVGElement("rect");
1498
+ setAttrs(bg, {
1499
+ x: 0,
1500
+ y: 0,
1501
+ width,
1502
+ height,
1503
+ fill: layout.theme.colors.background
1504
+ });
1505
+ svg.appendChild(bg);
1506
+ }
1507
+ const clipId = nextSvgId("oc-clip");
1508
+ const defs = createSVGElement("defs");
1509
+ const clipPath = createSVGElement("clipPath");
1510
+ clipPath.setAttribute("id", clipId);
1511
+ const maxPointR = layout.marks.reduce(
1512
+ (max, m) => m.type === "point" && m.r ? Math.max(max, m.r) : max,
1513
+ 0
1514
+ );
1515
+ const clipPad = Math.max(maxPointR, 2);
1516
+ const clipRect = createSVGElement("rect");
1517
+ setAttrs(clipRect, {
1518
+ x: 0,
1519
+ y: layout.area.y - clipPad,
1520
+ width,
1521
+ height: layout.area.height + clipPad * 2
1522
+ });
1523
+ clipPath.appendChild(clipRect);
1524
+ defs.appendChild(clipPath);
1525
+ const gradientMap = buildGradientDefs(layout.marks, defs);
1526
+ svg.appendChild(defs);
1527
+ setMarkRenderState({ animation, gradientMap });
1528
+ try {
1529
+ if (layout.facet) {
1530
+ renderFacetedPanels(svg, layout, layout.facet.panels, defs);
1531
+ } else {
1532
+ renderAxes(svg, layout);
1533
+ const clippedGroup = createSVGElement("g");
1534
+ clippedGroup.setAttribute("clip-path", `url(#${clipId})`);
1535
+ const markLabelsOverlay = renderMarks(clippedGroup, layout);
1536
+ const hasLineOrAreaWithDataPoints = layout.marks.some(
1537
+ (m) => (m.type === "line" || m.type === "area") && m.dataPoints && m.dataPoints.length > 0
1538
+ );
1539
+ if (hasLineOrAreaWithDataPoints) {
1540
+ const pointEls = clippedGroup.querySelectorAll("circle.oc-mark-point");
1541
+ for (const el of pointEls) {
1542
+ el.setAttribute("pointer-events", "none");
1543
+ }
1544
+ const overlay = createSVGElement("rect");
1545
+ setAttrs(overlay, {
1546
+ x: layout.area.x,
1547
+ y: layout.area.y,
1548
+ width: layout.area.width,
1549
+ height: layout.area.height,
1550
+ fill: "transparent"
1551
+ });
1552
+ overlay.setAttribute("class", "oc-voronoi-overlay");
1553
+ overlay.setAttribute("data-voronoi-overlay", "true");
1554
+ clippedGroup.appendChild(overlay);
1555
+ if (opts?.crosshair) {
1556
+ const crosshairLine = createSVGElement("line");
1557
+ crosshairLine.setAttribute("data-crosshair", "true");
1558
+ crosshairLine.setAttribute("class", "oc-crosshair");
1559
+ setAttrs(crosshairLine, {
1560
+ x1: 0,
1561
+ y1: layout.area.y,
1562
+ x2: 0,
1563
+ y2: layout.area.y + layout.area.height,
1564
+ stroke: layout.theme.colors.axis,
1565
+ "stroke-opacity": "0.4",
1566
+ "stroke-dasharray": "3,3",
1567
+ "stroke-width": "1",
1568
+ "pointer-events": "none"
1569
+ });
1570
+ crosshairLine.style.display = "none";
1571
+ clippedGroup.appendChild(crosshairLine);
1572
+ }
1573
+ const dotsGroup = createSVGElement("g");
1574
+ dotsGroup.setAttribute("data-snap-dots", "true");
1575
+ dotsGroup.setAttribute("class", "oc-snap-dots");
1576
+ dotsGroup.setAttribute("pointer-events", "none");
1577
+ clippedGroup.appendChild(dotsGroup);
1578
+ }
1579
+ svg.appendChild(clippedGroup);
1580
+ if (markLabelsOverlay) {
1581
+ svg.appendChild(markLabelsOverlay);
1582
+ }
1583
+ renderAnnotations(svg, layout);
1584
+ renderEndpointLabels(svg, layout);
1585
+ const epEntries = layout.endpointLabels?.entries ?? [];
1586
+ if (epEntries.length > 0) {
1587
+ const pointEls = clippedGroup.querySelectorAll("circle.oc-mark-point");
1588
+ for (const entry of epEntries) {
1589
+ if (!entry.marker) continue;
1590
+ const mx = entry.marker.dataX;
1591
+ const my = entry.marker.y;
1592
+ for (const el of pointEls) {
1593
+ const cx = Number(el.getAttribute("cx"));
1594
+ const cy = Number(el.getAttribute("cy"));
1595
+ if (Math.abs(cx - mx) < 0.5 && Math.abs(cy - my) < 0.5) {
1596
+ el.setAttribute("opacity", "0");
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ renderLegend(svg, layout.legend);
1603
+ renderChrome(svg, layout);
1604
+ renderMetrics(svg, layout);
1605
+ if (layout.watermark && !layout.chrome.brand) {
1606
+ renderBrand(svg, layout);
1607
+ }
1608
+ } finally {
1609
+ resetMarkRenderState();
1610
+ }
1611
+ container.appendChild(svg);
1612
+ return svg;
1613
+ }
1614
+ function renderFacetedPanels(svg, layout, panels, defs) {
1615
+ for (const panel of panels) {
1616
+ const g = createSVGElement("g");
1617
+ g.setAttribute("class", "oc-facet-panel");
1618
+ g.setAttribute("data-facet", panel.key);
1619
+ const bg = createSVGElement("rect");
1620
+ setAttrs(bg, {
1621
+ x: panel.area.x,
1622
+ y: panel.header.y - panel.header.fontSize * 0.6,
1623
+ width: panel.area.width,
1624
+ height: panel.area.height + panel.header.fontSize + 4,
1625
+ rx: 3,
1626
+ fill: layout.theme.isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"
1627
+ });
1628
+ g.appendChild(bg);
1629
+ const headerText = createSVGElement("text");
1630
+ setAttrs(headerText, {
1631
+ x: panel.header.x,
1632
+ y: panel.header.y,
1633
+ "text-anchor": "middle",
1634
+ "font-family": layout.theme.fonts.family,
1635
+ "font-size": panel.header.fontSize,
1636
+ "font-weight": panel.header.fontWeight,
1637
+ fill: layout.theme.colors.text
1638
+ });
1639
+ headerText.textContent = panel.header.text;
1640
+ g.appendChild(headerText);
1641
+ const panelLayout = {
1642
+ ...layout,
1643
+ area: panel.area,
1644
+ axes: panel.axes,
1645
+ marks: panel.marks,
1646
+ annotations: panel.annotations
1647
+ };
1648
+ renderAxes(g, panelLayout);
1649
+ const panelClipId = nextSvgId("oc-facet-clip");
1650
+ const clipPath = createSVGElement("clipPath");
1651
+ clipPath.setAttribute("id", panelClipId);
1652
+ const clipRect = createSVGElement("rect");
1653
+ setAttrs(clipRect, {
1654
+ x: panel.area.x,
1655
+ y: panel.area.y,
1656
+ width: panel.area.width,
1657
+ height: panel.area.height
1658
+ });
1659
+ clipPath.appendChild(clipRect);
1660
+ defs.appendChild(clipPath);
1661
+ const clippedGroup = createSVGElement("g");
1662
+ clippedGroup.setAttribute("clip-path", `url(#${panelClipId})`);
1663
+ const panelLabelsOverlay = renderMarks(clippedGroup, panelLayout);
1664
+ g.appendChild(clippedGroup);
1665
+ if (panelLabelsOverlay) {
1666
+ g.appendChild(panelLabelsOverlay);
1667
+ }
1668
+ renderAnnotations(g, panelLayout);
1669
+ svg.appendChild(g);
1670
+ }
1671
+ }
1672
+
1673
+ export {
1674
+ resetSvgIdCounter,
1675
+ SVG_NS,
1676
+ renderLegend,
1677
+ registerMarkRenderer,
1678
+ renderChartSVG
1679
+ };
1680
+ //# sourceMappingURL=chunk-PSMDJEXK.js.map