@opendata-ai/openchart-vanilla 6.20.0 → 6.22.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.
package/dist/index.js CHANGED
@@ -3314,12 +3314,6 @@ function setupTableAnimationCleanup(wrapper) {
3314
3314
  }
3315
3315
 
3316
3316
  // src/svg-renderer.ts
3317
- import {
3318
- BRAND_FONT_SIZE as BRAND_FONT_SIZE2,
3319
- BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2,
3320
- estimateTextWidth,
3321
- wrapText
3322
- } from "@opendata-ai/openchart-core";
3323
3317
  import { clampStaggerDelay } from "@opendata-ai/openchart-engine";
3324
3318
 
3325
3319
  // src/gradient-utils.ts
@@ -3411,37 +3405,10 @@ function resolveMarkFill(fill, gradientMap) {
3411
3405
  return id ? `url(#${id})` : "#000000";
3412
3406
  }
3413
3407
 
3414
- // src/svg-renderer.ts
3408
+ // src/renderers/svg-dom.ts
3409
+ import { estimateTextWidth } from "@opendata-ai/openchart-core";
3415
3410
  var SVG_NS2 = "http://www.w3.org/2000/svg";
3416
- var currentAnimation;
3417
- var currentGradientMap = /* @__PURE__ */ new Map();
3418
- function stampAnimationAttrs(el, mark, fallbackIndex) {
3419
- if (!currentAnimation?.enabled) return;
3420
- const idx = mark.animationIndex ?? fallbackIndex;
3421
- el.setAttribute("data-animation-index", String(idx));
3422
- el.style.setProperty("--oc-mark-index", String(idx));
3423
- }
3424
- var EASE_VAR_MAP = {
3425
- smooth: "var(--oc-ease-smooth)",
3426
- snappy: "var(--oc-ease-snappy)"
3427
- };
3428
- function computeXAxisExtent(layout) {
3429
- const xAxis = layout.axes.x;
3430
- if (!xAxis) return 0;
3431
- if (xAxis.tickAngle && Math.abs(xAxis.tickAngle) > 10) {
3432
- const fontSize = xAxis.tickLabelStyle.fontSize;
3433
- const fontWeight = xAxis.tickLabelStyle.fontWeight;
3434
- const angleRad = Math.abs(xAxis.tickAngle) * (Math.PI / 180);
3435
- let maxLabelWidth = 40;
3436
- for (const tick of xAxis.ticks) {
3437
- const w = estimateTextWidth(tick.label, fontSize, fontWeight);
3438
- if (w > maxLabelWidth) maxLabelWidth = w;
3439
- }
3440
- const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
3441
- return xAxis.label ? rotatedHeight + 20 : rotatedHeight;
3442
- }
3443
- return xAxis.label ? 48 : 26;
3444
- }
3411
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
3445
3412
  function createSVGElement(tag) {
3446
3413
  return document.createElementNS(SVG_NS2, tag);
3447
3414
  }
@@ -3467,73 +3434,184 @@ function applyTextStyle(el, style) {
3467
3434
  el.setAttribute("font-variant", style.fontVariant);
3468
3435
  }
3469
3436
  }
3470
- function renderChromeElement(parent, element, className, chromeKey, measureText) {
3471
- const text = createSVGElement("text");
3472
- setAttrs(text, { x: element.x, y: element.y });
3473
- applyTextStyle(text, element.style);
3474
- text.setAttribute("class", className);
3475
- text.setAttribute("data-chrome-key", chromeKey);
3476
- const lines = wrapText(
3477
- element.text,
3478
- element.style.fontSize,
3479
- element.style.fontWeight,
3480
- element.maxWidth,
3481
- measureText
3482
- );
3483
- if (lines.length === 1) {
3484
- text.textContent = element.text;
3485
- } else {
3486
- const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
3487
- for (let i = 0; i < lines.length; i++) {
3488
- const tspan = createSVGElement("tspan");
3489
- setAttrs(tspan, { x: element.x, dy: i === 0 ? 0 : lineHeight });
3490
- tspan.textContent = lines[i];
3491
- text.appendChild(tspan);
3437
+ function computeXAxisExtent(layout) {
3438
+ const xAxis = layout.axes.x;
3439
+ if (!xAxis) return 0;
3440
+ if (xAxis.tickAngle && Math.abs(xAxis.tickAngle) > 10) {
3441
+ const fontSize = xAxis.tickLabelStyle.fontSize;
3442
+ const fontWeight = xAxis.tickLabelStyle.fontWeight;
3443
+ const angleRad = Math.abs(xAxis.tickAngle) * (Math.PI / 180);
3444
+ let maxLabelWidth = 40;
3445
+ for (const tick of xAxis.ticks) {
3446
+ const w = estimateTextWidth(tick.label, fontSize, fontWeight);
3447
+ if (w > maxLabelWidth) maxLabelWidth = w;
3492
3448
  }
3449
+ const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
3450
+ return xAxis.label ? rotatedHeight + 20 : rotatedHeight;
3493
3451
  }
3494
- parent.appendChild(text);
3452
+ return xAxis.label ? 48 : 26;
3495
3453
  }
3496
- function renderChrome(parent, layout) {
3454
+
3455
+ // src/renderers/annotations.ts
3456
+ function renderCurvedArrow(parent, from, to, stroke) {
3457
+ const pad = 6;
3458
+ const tipY = to.y - pad;
3459
+ const dy = tipY - from.y;
3460
+ const dist = Math.sqrt((to.x - from.x) ** 2 + dy ** 2) || 1;
3461
+ const arrowLen = 8;
3462
+ const arrowWidth = 4;
3463
+ const bulge = Math.max(dist * 0.4, 35);
3464
+ const cp1x = from.x + bulge;
3465
+ const cp1y = from.y + dy * 0.35;
3466
+ const cp2x = to.x;
3467
+ const cp2y = tipY - Math.abs(dy) * 0.25;
3468
+ const tx = to.x - cp2x;
3469
+ const ty = tipY - cp2y;
3470
+ const tLen = Math.sqrt(tx * tx + ty * ty) || 1;
3471
+ const ux = tx / tLen;
3472
+ const uy = ty / tLen;
3473
+ const baseX = to.x - ux * arrowLen;
3474
+ const baseY = tipY - uy * arrowLen;
3475
+ const path = createSVGElement("path");
3476
+ path.setAttribute("class", "oc-annotation-connector");
3477
+ setAttrs(path, {
3478
+ d: `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${baseX} ${baseY}`,
3479
+ fill: "none",
3480
+ stroke,
3481
+ "stroke-width": 1.5
3482
+ });
3483
+ parent.appendChild(path);
3484
+ const px = -uy;
3485
+ const py = ux;
3486
+ const arrow = createSVGElement("polygon");
3487
+ arrow.setAttribute("class", "oc-annotation-connector");
3488
+ setAttrs(arrow, {
3489
+ points: [
3490
+ `${to.x},${tipY}`,
3491
+ `${baseX + px * arrowWidth},${baseY + py * arrowWidth}`,
3492
+ `${baseX - px * arrowWidth},${baseY - py * arrowWidth}`
3493
+ ].join(" "),
3494
+ fill: stroke
3495
+ });
3496
+ parent.appendChild(arrow);
3497
+ }
3498
+ function renderAnnotation(parent, annotation, index2, bgColor) {
3497
3499
  const g = createSVGElement("g");
3498
- g.setAttribute("class", "oc-chrome");
3499
- const { chrome, measureText } = layout;
3500
- if (chrome.title) {
3501
- renderChromeElement(g, chrome.title, "oc-title", "title", measureText);
3500
+ g.setAttribute("class", `oc-annotation oc-annotation-${annotation.type}`);
3501
+ g.setAttribute("data-annotation-index", String(index2));
3502
+ if (annotation.id) {
3503
+ g.setAttribute("data-annotation-id", annotation.id);
3502
3504
  }
3503
- if (chrome.subtitle) {
3504
- renderChromeElement(g, chrome.subtitle, "oc-subtitle", "subtitle", measureText);
3505
+ if (annotation.rect) {
3506
+ const rect = createSVGElement("rect");
3507
+ rect.setAttribute("class", "oc-annotation-range");
3508
+ setAttrs(rect, {
3509
+ x: annotation.rect.x,
3510
+ y: annotation.rect.y,
3511
+ width: annotation.rect.width,
3512
+ height: annotation.rect.height
3513
+ });
3514
+ if (annotation.fill) rect.setAttribute("fill", annotation.fill);
3515
+ if (annotation.opacity !== void 0) {
3516
+ rect.setAttribute("fill-opacity", String(annotation.opacity));
3517
+ }
3518
+ g.appendChild(rect);
3505
3519
  }
3506
- const xAxisExtent = computeXAxisExtent(layout);
3507
- const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
3508
- if (chrome.source) {
3509
- renderChromeElement(
3510
- g,
3511
- { ...chrome.source, y: bottomOffset + chrome.source.y },
3512
- "oc-source",
3513
- "source",
3514
- measureText
3515
- );
3520
+ if (annotation.line) {
3521
+ const line = createSVGElement("line");
3522
+ line.setAttribute("class", "oc-annotation-line");
3523
+ setAttrs(line, {
3524
+ x1: annotation.line.start.x,
3525
+ y1: annotation.line.start.y,
3526
+ x2: annotation.line.end.x,
3527
+ y2: annotation.line.end.y,
3528
+ "stroke-width": annotation.strokeWidth ?? 1
3529
+ });
3530
+ if (annotation.stroke) line.setAttribute("stroke", annotation.stroke);
3531
+ if (annotation.strokeDasharray) {
3532
+ line.setAttribute("stroke-dasharray", annotation.strokeDasharray);
3533
+ }
3534
+ g.appendChild(line);
3516
3535
  }
3517
- if (chrome.byline) {
3518
- renderChromeElement(
3519
- g,
3520
- { ...chrome.byline, y: bottomOffset + chrome.byline.y },
3521
- "oc-byline",
3522
- "byline",
3523
- measureText
3524
- );
3536
+ if (annotation.label?.visible) {
3537
+ if (annotation.label.connector) {
3538
+ const c2 = annotation.label.connector;
3539
+ if (c2.style === "curve") {
3540
+ renderCurvedArrow(g, c2.from, c2.to, c2.stroke);
3541
+ } else {
3542
+ const connector = createSVGElement("line");
3543
+ connector.setAttribute("class", "oc-annotation-connector");
3544
+ setAttrs(connector, {
3545
+ x1: c2.from.x,
3546
+ y1: c2.from.y,
3547
+ x2: c2.to.x,
3548
+ y2: c2.to.y,
3549
+ stroke: c2.stroke,
3550
+ "stroke-width": 1,
3551
+ "stroke-opacity": 0.5
3552
+ });
3553
+ g.appendChild(connector);
3554
+ }
3555
+ }
3556
+ const text = createSVGElement("text");
3557
+ text.setAttribute("class", "oc-annotation-label");
3558
+ setAttrs(text, { x: annotation.label.x, y: annotation.label.y });
3559
+ applyTextStyle(text, annotation.label.style);
3560
+ const lines = annotation.label.text.split("\n");
3561
+ const fontSize = annotation.label.style.fontSize ?? 12;
3562
+ const lineHeight = fontSize * (annotation.label.style.lineHeight ?? 1.3);
3563
+ const isMultiLine = lines.length > 1;
3564
+ if (isMultiLine) {
3565
+ text.setAttribute("text-anchor", "middle");
3566
+ for (let i = 0; i < lines.length; i++) {
3567
+ const tspan = createSVGElement("tspan");
3568
+ setAttrs(tspan, { x: annotation.label.x, dy: i === 0 ? 0 : lineHeight });
3569
+ tspan.textContent = lines[i];
3570
+ text.appendChild(tspan);
3571
+ }
3572
+ } else {
3573
+ text.textContent = annotation.label.text;
3574
+ }
3575
+ if (annotation.label.background) {
3576
+ const charWidth = fontSize * 0.55;
3577
+ const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
3578
+ const totalHeight = lines.length * lineHeight;
3579
+ const pad = 3;
3580
+ const bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
3581
+ const bgRect = createSVGElement("rect");
3582
+ bgRect.setAttribute("class", "oc-annotation-bg");
3583
+ setAttrs(bgRect, {
3584
+ x: bgX,
3585
+ y: annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad,
3586
+ width: maxLineWidth + pad * 2,
3587
+ height: totalHeight + pad * 2,
3588
+ fill: annotation.label.background,
3589
+ rx: 2
3590
+ });
3591
+ g.appendChild(bgRect);
3592
+ } else if (bgColor) {
3593
+ text.style.paintOrder = "stroke";
3594
+ text.style.stroke = bgColor;
3595
+ text.style.strokeWidth = `${Math.round(fontSize * 0.3)}px`;
3596
+ text.style.strokeLinejoin = "round";
3597
+ }
3598
+ g.appendChild(text);
3525
3599
  }
3526
- if (chrome.footer) {
3527
- renderChromeElement(
3528
- g,
3529
- { ...chrome.footer, y: bottomOffset + chrome.footer.y },
3530
- "oc-footer",
3531
- "footer",
3532
- measureText
3533
- );
3600
+ parent.appendChild(g);
3601
+ }
3602
+ function renderAnnotations(parent, layout) {
3603
+ if (layout.annotations.length === 0) return;
3604
+ const g = createSVGElement("g");
3605
+ g.setAttribute("class", "oc-annotations");
3606
+ const bgColor = layout.theme.colors.background;
3607
+ for (let i = 0; i < layout.annotations.length; i++) {
3608
+ renderAnnotation(g, layout.annotations[i], i, bgColor);
3534
3609
  }
3535
3610
  parent.appendChild(g);
3536
3611
  }
3612
+
3613
+ // src/renderers/axes.ts
3614
+ import { estimateTextWidth as estimateTextWidth2 } from "@opendata-ai/openchart-core";
3537
3615
  function renderAxis(parent, axis, orientation, layout) {
3538
3616
  const g = createSVGElement("g");
3539
3617
  g.setAttribute("class", `oc-axis oc-axis-${orientation}`);
@@ -3626,7 +3704,7 @@ function renderAxis(parent, axis, orientation, layout) {
3626
3704
  const angleRad = Math.abs(axis.tickAngle) * (Math.PI / 180);
3627
3705
  let maxLabelWidth = 40;
3628
3706
  for (const tick of axis.ticks) {
3629
- const w = estimateTextWidth(
3707
+ const w = estimateTextWidth2(
3630
3708
  tick.label,
3631
3709
  axis.tickLabelStyle.fontSize,
3632
3710
  axis.tickLabelStyle.fontWeight
@@ -3661,9 +3739,255 @@ function renderAxes(parent, layout) {
3661
3739
  renderAxis(parent, layout.axes.y, "y", layout);
3662
3740
  }
3663
3741
  }
3664
- var markRenderers = {};
3665
- function registerMarkRenderer(type, renderer) {
3666
- markRenderers[type] = renderer;
3742
+
3743
+ // src/renderers/brand.ts
3744
+ import { BRAND_FONT_SIZE as BRAND_FONT_SIZE2, BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2 } from "@opendata-ai/openchart-core";
3745
+ var BRAND_URL = "https://tryopendata.ai";
3746
+ function renderBrand(parent, layout) {
3747
+ if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
3748
+ const { width } = layout.dimensions;
3749
+ const padding = layout.theme.spacing.padding;
3750
+ const rightEdge = width - padding;
3751
+ const fill = layout.theme.colors.axis;
3752
+ const { chrome } = layout;
3753
+ const xAxisExtent = computeXAxisExtent(layout);
3754
+ const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
3755
+ const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
3756
+ const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter;
3757
+ const a2 = createSVGElement("a");
3758
+ a2.setAttribute("href", BRAND_URL);
3759
+ a2.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
3760
+ a2.setAttribute("target", "_blank");
3761
+ a2.setAttribute("rel", "noopener");
3762
+ a2.setAttribute("class", "oc-chrome-ref");
3763
+ const BRAND_LARGE = 16;
3764
+ const text = createSVGElement("text");
3765
+ setAttrs(text, {
3766
+ x: rightEdge,
3767
+ y: chromeY + BRAND_LARGE,
3768
+ "dominant-baseline": "alphabetic",
3769
+ "font-family": layout.theme.fonts.family,
3770
+ "font-size": BRAND_FONT_SIZE2,
3771
+ "text-anchor": "end",
3772
+ "fill-opacity": 0.55
3773
+ });
3774
+ text.style.setProperty("fill", fill);
3775
+ const trySpan = createSVGElement("tspan");
3776
+ trySpan.setAttribute("font-weight", "500");
3777
+ trySpan.textContent = "try";
3778
+ text.appendChild(trySpan);
3779
+ const openDataSpan = createSVGElement("tspan");
3780
+ openDataSpan.setAttribute("font-weight", "600");
3781
+ openDataSpan.setAttribute("font-size", String(BRAND_LARGE));
3782
+ openDataSpan.textContent = "OpenData";
3783
+ text.appendChild(openDataSpan);
3784
+ const aiSpan = createSVGElement("tspan");
3785
+ aiSpan.setAttribute("font-weight", "500");
3786
+ aiSpan.textContent = ".ai";
3787
+ text.appendChild(aiSpan);
3788
+ a2.appendChild(text);
3789
+ parent.appendChild(a2);
3790
+ }
3791
+
3792
+ // src/renderers/chrome.ts
3793
+ import { wrapText } from "@opendata-ai/openchart-core";
3794
+ function renderChromeElement(parent, element, className, chromeKey, measureText) {
3795
+ const text = createSVGElement("text");
3796
+ setAttrs(text, { x: element.x, y: element.y });
3797
+ applyTextStyle(text, element.style);
3798
+ text.setAttribute("class", className);
3799
+ text.setAttribute("data-chrome-key", chromeKey);
3800
+ const lines = wrapText(
3801
+ element.text,
3802
+ element.style.fontSize,
3803
+ element.style.fontWeight,
3804
+ element.maxWidth,
3805
+ measureText
3806
+ );
3807
+ if (lines.length === 1) {
3808
+ text.textContent = element.text;
3809
+ } else {
3810
+ const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
3811
+ for (let i = 0; i < lines.length; i++) {
3812
+ const tspan = createSVGElement("tspan");
3813
+ setAttrs(tspan, { x: element.x, dy: i === 0 ? 0 : lineHeight });
3814
+ tspan.textContent = lines[i];
3815
+ text.appendChild(tspan);
3816
+ }
3817
+ }
3818
+ parent.appendChild(text);
3819
+ }
3820
+ function renderChrome(parent, layout) {
3821
+ const g = createSVGElement("g");
3822
+ g.setAttribute("class", "oc-chrome");
3823
+ const { chrome, measureText } = layout;
3824
+ if (chrome.title) {
3825
+ renderChromeElement(g, chrome.title, "oc-title", "title", measureText);
3826
+ }
3827
+ if (chrome.subtitle) {
3828
+ renderChromeElement(g, chrome.subtitle, "oc-subtitle", "subtitle", measureText);
3829
+ }
3830
+ const xAxisExtent = computeXAxisExtent(layout);
3831
+ const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
3832
+ if (chrome.source) {
3833
+ renderChromeElement(
3834
+ g,
3835
+ { ...chrome.source, y: bottomOffset + chrome.source.y },
3836
+ "oc-source",
3837
+ "source",
3838
+ measureText
3839
+ );
3840
+ }
3841
+ if (chrome.byline) {
3842
+ renderChromeElement(
3843
+ g,
3844
+ { ...chrome.byline, y: bottomOffset + chrome.byline.y },
3845
+ "oc-byline",
3846
+ "byline",
3847
+ measureText
3848
+ );
3849
+ }
3850
+ if (chrome.footer) {
3851
+ renderChromeElement(
3852
+ g,
3853
+ { ...chrome.footer, y: bottomOffset + chrome.footer.y },
3854
+ "oc-footer",
3855
+ "footer",
3856
+ measureText
3857
+ );
3858
+ }
3859
+ parent.appendChild(g);
3860
+ }
3861
+
3862
+ // src/renderers/legend.ts
3863
+ import { estimateTextWidth as estimateTextWidth3 } from "@opendata-ai/openchart-core";
3864
+ function renderLegend(parent, legend) {
3865
+ if (legend.entries.length === 0) return;
3866
+ const g = createSVGElement("g");
3867
+ g.setAttribute("class", "oc-legend");
3868
+ g.setAttribute("role", "list");
3869
+ g.setAttribute("aria-label", "Chart legend");
3870
+ const isHorizontal = legend.position === "top" || legend.position === "bottom";
3871
+ let offsetX = legend.bounds.x;
3872
+ let offsetY = legend.bounds.y;
3873
+ for (let i = 0; i < legend.entries.length; i++) {
3874
+ const entry = legend.entries[i];
3875
+ if (isHorizontal && i > 0) {
3876
+ const labelWidth = estimateTextWidth3(
3877
+ entry.label,
3878
+ legend.labelStyle.fontSize,
3879
+ legend.labelStyle.fontWeight
3880
+ );
3881
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
3882
+ if (offsetX + entryWidth > legend.bounds.x + legend.bounds.width) {
3883
+ offsetX = legend.bounds.x;
3884
+ offsetY += legend.swatchSize + 6;
3885
+ }
3886
+ }
3887
+ const entryG = createSVGElement("g");
3888
+ entryG.setAttribute("class", "oc-legend-entry");
3889
+ entryG.setAttribute("role", "listitem");
3890
+ entryG.setAttribute("data-legend-index", String(i));
3891
+ entryG.setAttribute("data-legend-label", entry.label);
3892
+ if (entry.overflow) {
3893
+ entryG.setAttribute("data-legend-overflow", "true");
3894
+ entryG.setAttribute("aria-label", entry.label);
3895
+ entryG.setAttribute("opacity", "0.5");
3896
+ } else {
3897
+ entryG.setAttribute(
3898
+ "aria-label",
3899
+ `${entry.label}: ${entry.active !== false ? "visible" : "hidden"}`
3900
+ );
3901
+ entryG.setAttribute("style", "cursor: pointer");
3902
+ if (entry.active === false) {
3903
+ entryG.setAttribute("opacity", "0.3");
3904
+ }
3905
+ }
3906
+ if (entry.shape === "circle") {
3907
+ const circle = createSVGElement("circle");
3908
+ setAttrs(circle, {
3909
+ cx: offsetX + legend.swatchSize / 2,
3910
+ cy: offsetY + legend.swatchSize / 2,
3911
+ r: legend.swatchSize / 2,
3912
+ fill: entry.color
3913
+ });
3914
+ entryG.appendChild(circle);
3915
+ } else if (entry.shape === "line") {
3916
+ const line = createSVGElement("line");
3917
+ setAttrs(line, {
3918
+ x1: offsetX,
3919
+ y1: offsetY + legend.swatchSize / 2,
3920
+ x2: offsetX + legend.swatchSize,
3921
+ y2: offsetY + legend.swatchSize / 2,
3922
+ stroke: entry.color,
3923
+ "stroke-width": 2
3924
+ });
3925
+ entryG.appendChild(line);
3926
+ const dot = createSVGElement("circle");
3927
+ setAttrs(dot, {
3928
+ cx: offsetX + legend.swatchSize / 2,
3929
+ cy: offsetY + legend.swatchSize / 2,
3930
+ r: 2.5,
3931
+ fill: entry.color
3932
+ });
3933
+ entryG.appendChild(dot);
3934
+ } else {
3935
+ const rect = createSVGElement("rect");
3936
+ setAttrs(rect, {
3937
+ x: offsetX,
3938
+ y: offsetY,
3939
+ width: legend.swatchSize,
3940
+ height: legend.swatchSize,
3941
+ fill: entry.color,
3942
+ rx: 2
3943
+ });
3944
+ entryG.appendChild(rect);
3945
+ }
3946
+ const label = createSVGElement("text");
3947
+ setAttrs(label, {
3948
+ x: offsetX + legend.swatchSize + legend.swatchGap,
3949
+ y: offsetY + legend.swatchSize / 2,
3950
+ "dominant-baseline": "central"
3951
+ });
3952
+ applyTextStyle(label, legend.labelStyle);
3953
+ label.textContent = entry.label;
3954
+ entryG.appendChild(label);
3955
+ g.appendChild(entryG);
3956
+ if (isHorizontal) {
3957
+ const labelWidth = estimateTextWidth3(
3958
+ entry.label,
3959
+ legend.labelStyle.fontSize,
3960
+ legend.labelStyle.fontWeight
3961
+ );
3962
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
3963
+ offsetX += entryWidth;
3964
+ } else {
3965
+ offsetY += legend.swatchSize + legend.entryGap;
3966
+ }
3967
+ }
3968
+ parent.appendChild(g);
3969
+ }
3970
+
3971
+ // src/renderers/marks.ts
3972
+ var currentAnimation;
3973
+ var currentGradientMap = /* @__PURE__ */ new Map();
3974
+ function setMarkRenderState(state) {
3975
+ currentAnimation = state.animation;
3976
+ currentGradientMap = state.gradientMap;
3977
+ }
3978
+ function resetMarkRenderState() {
3979
+ currentAnimation = void 0;
3980
+ currentGradientMap = /* @__PURE__ */ new Map();
3981
+ }
3982
+ function stampAnimationAttrs(el, mark, fallbackIndex) {
3983
+ if (!currentAnimation?.enabled) return;
3984
+ const idx = mark.animationIndex ?? fallbackIndex;
3985
+ el.setAttribute("data-animation-index", String(idx));
3986
+ el.style.setProperty("--oc-mark-index", String(idx));
3987
+ }
3988
+ var markRenderers = {};
3989
+ function registerMarkRenderer(type, renderer) {
3990
+ markRenderers[type] = renderer;
3667
3991
  }
3668
3992
  function renderLineMark(mark, index2) {
3669
3993
  const g = createSVGElement("g");
@@ -3948,319 +4272,15 @@ function renderMarks(parent, layout) {
3948
4272
  }
3949
4273
  parent.appendChild(g);
3950
4274
  }
3951
- function renderAnnotations(parent, layout) {
3952
- if (layout.annotations.length === 0) return;
3953
- const g = createSVGElement("g");
3954
- g.setAttribute("class", "oc-annotations");
3955
- const bgColor = layout.theme.colors.background;
3956
- for (let i = 0; i < layout.annotations.length; i++) {
3957
- renderAnnotation(g, layout.annotations[i], i, bgColor);
3958
- }
3959
- parent.appendChild(g);
3960
- }
3961
- function renderCurvedArrow(parent, from, to, stroke) {
3962
- const pad = 6;
3963
- const tipY = to.y - pad;
3964
- const dy = tipY - from.y;
3965
- const dist = Math.sqrt((to.x - from.x) ** 2 + dy ** 2) || 1;
3966
- const arrowLen = 8;
3967
- const arrowWidth = 4;
3968
- const bulge = Math.max(dist * 0.4, 35);
3969
- const cp1x = from.x + bulge;
3970
- const cp1y = from.y + dy * 0.35;
3971
- const cp2x = to.x;
3972
- const cp2y = tipY - Math.abs(dy) * 0.25;
3973
- const tx = to.x - cp2x;
3974
- const ty = tipY - cp2y;
3975
- const tLen = Math.sqrt(tx * tx + ty * ty) || 1;
3976
- const ux = tx / tLen;
3977
- const uy = ty / tLen;
3978
- const baseX = to.x - ux * arrowLen;
3979
- const baseY = tipY - uy * arrowLen;
3980
- const path = createSVGElement("path");
3981
- path.setAttribute("class", "oc-annotation-connector");
3982
- setAttrs(path, {
3983
- d: `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${baseX} ${baseY}`,
3984
- fill: "none",
3985
- stroke,
3986
- "stroke-width": 1.5
3987
- });
3988
- parent.appendChild(path);
3989
- const px = -uy;
3990
- const py = ux;
3991
- const arrow = createSVGElement("polygon");
3992
- arrow.setAttribute("class", "oc-annotation-connector");
3993
- setAttrs(arrow, {
3994
- points: [
3995
- `${to.x},${tipY}`,
3996
- `${baseX + px * arrowWidth},${baseY + py * arrowWidth}`,
3997
- `${baseX - px * arrowWidth},${baseY - py * arrowWidth}`
3998
- ].join(" "),
3999
- fill: stroke
4000
- });
4001
- parent.appendChild(arrow);
4002
- }
4003
- function renderAnnotation(parent, annotation, index2, bgColor) {
4004
- const g = createSVGElement("g");
4005
- g.setAttribute("class", `oc-annotation oc-annotation-${annotation.type}`);
4006
- g.setAttribute("data-annotation-index", String(index2));
4007
- if (annotation.id) {
4008
- g.setAttribute("data-annotation-id", annotation.id);
4009
- }
4010
- if (annotation.rect) {
4011
- const rect = createSVGElement("rect");
4012
- rect.setAttribute("class", "oc-annotation-range");
4013
- setAttrs(rect, {
4014
- x: annotation.rect.x,
4015
- y: annotation.rect.y,
4016
- width: annotation.rect.width,
4017
- height: annotation.rect.height
4018
- });
4019
- if (annotation.fill) rect.setAttribute("fill", annotation.fill);
4020
- if (annotation.opacity !== void 0) {
4021
- rect.setAttribute("fill-opacity", String(annotation.opacity));
4022
- }
4023
- g.appendChild(rect);
4024
- }
4025
- if (annotation.line) {
4026
- const line = createSVGElement("line");
4027
- line.setAttribute("class", "oc-annotation-line");
4028
- setAttrs(line, {
4029
- x1: annotation.line.start.x,
4030
- y1: annotation.line.start.y,
4031
- x2: annotation.line.end.x,
4032
- y2: annotation.line.end.y,
4033
- "stroke-width": annotation.strokeWidth ?? 1
4034
- });
4035
- if (annotation.stroke) line.setAttribute("stroke", annotation.stroke);
4036
- if (annotation.strokeDasharray) {
4037
- line.setAttribute("stroke-dasharray", annotation.strokeDasharray);
4038
- }
4039
- g.appendChild(line);
4040
- }
4041
- if (annotation.label?.visible) {
4042
- if (annotation.label.connector) {
4043
- const c2 = annotation.label.connector;
4044
- if (c2.style === "curve") {
4045
- renderCurvedArrow(g, c2.from, c2.to, c2.stroke);
4046
- } else {
4047
- const connector = createSVGElement("line");
4048
- connector.setAttribute("class", "oc-annotation-connector");
4049
- setAttrs(connector, {
4050
- x1: c2.from.x,
4051
- y1: c2.from.y,
4052
- x2: c2.to.x,
4053
- y2: c2.to.y,
4054
- stroke: c2.stroke,
4055
- "stroke-width": 1,
4056
- "stroke-opacity": 0.5
4057
- });
4058
- g.appendChild(connector);
4059
- }
4060
- }
4061
- const text = createSVGElement("text");
4062
- text.setAttribute("class", "oc-annotation-label");
4063
- setAttrs(text, { x: annotation.label.x, y: annotation.label.y });
4064
- applyTextStyle(text, annotation.label.style);
4065
- const lines = annotation.label.text.split("\n");
4066
- const fontSize = annotation.label.style.fontSize ?? 12;
4067
- const lineHeight = fontSize * (annotation.label.style.lineHeight ?? 1.3);
4068
- const isMultiLine = lines.length > 1;
4069
- if (isMultiLine) {
4070
- text.setAttribute("text-anchor", "middle");
4071
- for (let i = 0; i < lines.length; i++) {
4072
- const tspan = createSVGElement("tspan");
4073
- setAttrs(tspan, { x: annotation.label.x, dy: i === 0 ? 0 : lineHeight });
4074
- tspan.textContent = lines[i];
4075
- text.appendChild(tspan);
4076
- }
4077
- } else {
4078
- text.textContent = annotation.label.text;
4079
- }
4080
- if (annotation.label.background) {
4081
- const charWidth = fontSize * 0.55;
4082
- const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
4083
- const totalHeight = lines.length * lineHeight;
4084
- const pad = 3;
4085
- const bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
4086
- const bgRect = createSVGElement("rect");
4087
- bgRect.setAttribute("class", "oc-annotation-bg");
4088
- setAttrs(bgRect, {
4089
- x: bgX,
4090
- y: annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad,
4091
- width: maxLineWidth + pad * 2,
4092
- height: totalHeight + pad * 2,
4093
- fill: annotation.label.background,
4094
- rx: 2
4095
- });
4096
- g.appendChild(bgRect);
4097
- } else if (bgColor) {
4098
- text.style.paintOrder = "stroke";
4099
- text.style.stroke = bgColor;
4100
- text.style.strokeWidth = `${Math.round(fontSize * 0.3)}px`;
4101
- text.style.strokeLinejoin = "round";
4102
- }
4103
- g.appendChild(text);
4104
- }
4105
- parent.appendChild(g);
4106
- }
4107
- function renderLegend(parent, legend) {
4108
- if (legend.entries.length === 0) return;
4109
- const g = createSVGElement("g");
4110
- g.setAttribute("class", "oc-legend");
4111
- g.setAttribute("role", "list");
4112
- g.setAttribute("aria-label", "Chart legend");
4113
- const isHorizontal = legend.position === "top" || legend.position === "bottom";
4114
- let offsetX = legend.bounds.x;
4115
- let offsetY = legend.bounds.y;
4116
- for (let i = 0; i < legend.entries.length; i++) {
4117
- const entry = legend.entries[i];
4118
- if (isHorizontal && i > 0) {
4119
- const labelWidth = estimateTextWidth(
4120
- entry.label,
4121
- legend.labelStyle.fontSize,
4122
- legend.labelStyle.fontWeight
4123
- );
4124
- const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
4125
- if (offsetX + entryWidth > legend.bounds.x + legend.bounds.width) {
4126
- offsetX = legend.bounds.x;
4127
- offsetY += legend.swatchSize + 6;
4128
- }
4129
- }
4130
- const entryG = createSVGElement("g");
4131
- entryG.setAttribute("class", "oc-legend-entry");
4132
- entryG.setAttribute("role", "listitem");
4133
- entryG.setAttribute("data-legend-index", String(i));
4134
- entryG.setAttribute("data-legend-label", entry.label);
4135
- if (entry.overflow) {
4136
- entryG.setAttribute("data-legend-overflow", "true");
4137
- entryG.setAttribute("aria-label", entry.label);
4138
- entryG.setAttribute("opacity", "0.5");
4139
- } else {
4140
- entryG.setAttribute(
4141
- "aria-label",
4142
- `${entry.label}: ${entry.active !== false ? "visible" : "hidden"}`
4143
- );
4144
- entryG.setAttribute("style", "cursor: pointer");
4145
- if (entry.active === false) {
4146
- entryG.setAttribute("opacity", "0.3");
4147
- }
4148
- }
4149
- if (entry.shape === "circle") {
4150
- const circle = createSVGElement("circle");
4151
- setAttrs(circle, {
4152
- cx: offsetX + legend.swatchSize / 2,
4153
- cy: offsetY + legend.swatchSize / 2,
4154
- r: legend.swatchSize / 2,
4155
- fill: entry.color
4156
- });
4157
- entryG.appendChild(circle);
4158
- } else if (entry.shape === "line") {
4159
- const line = createSVGElement("line");
4160
- setAttrs(line, {
4161
- x1: offsetX,
4162
- y1: offsetY + legend.swatchSize / 2,
4163
- x2: offsetX + legend.swatchSize,
4164
- y2: offsetY + legend.swatchSize / 2,
4165
- stroke: entry.color,
4166
- "stroke-width": 2
4167
- });
4168
- entryG.appendChild(line);
4169
- const dot = createSVGElement("circle");
4170
- setAttrs(dot, {
4171
- cx: offsetX + legend.swatchSize / 2,
4172
- cy: offsetY + legend.swatchSize / 2,
4173
- r: 2.5,
4174
- fill: entry.color
4175
- });
4176
- entryG.appendChild(dot);
4177
- } else {
4178
- const rect = createSVGElement("rect");
4179
- setAttrs(rect, {
4180
- x: offsetX,
4181
- y: offsetY,
4182
- width: legend.swatchSize,
4183
- height: legend.swatchSize,
4184
- fill: entry.color,
4185
- rx: 2
4186
- });
4187
- entryG.appendChild(rect);
4188
- }
4189
- const label = createSVGElement("text");
4190
- setAttrs(label, {
4191
- x: offsetX + legend.swatchSize + legend.swatchGap,
4192
- y: offsetY + legend.swatchSize / 2,
4193
- "dominant-baseline": "central"
4194
- });
4195
- applyTextStyle(label, legend.labelStyle);
4196
- label.textContent = entry.label;
4197
- entryG.appendChild(label);
4198
- g.appendChild(entryG);
4199
- if (isHorizontal) {
4200
- const labelWidth = estimateTextWidth(
4201
- entry.label,
4202
- legend.labelStyle.fontSize,
4203
- legend.labelStyle.fontWeight
4204
- );
4205
- const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
4206
- offsetX += entryWidth;
4207
- } else {
4208
- offsetY += legend.swatchSize + legend.entryGap;
4209
- }
4210
- }
4211
- parent.appendChild(g);
4212
- }
4213
- var BRAND_URL = "https://tryopendata.ai";
4214
- var XLINK_NS = "http://www.w3.org/1999/xlink";
4215
- function renderBrand(parent, layout) {
4216
- if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
4217
- const { width } = layout.dimensions;
4218
- const padding = layout.theme.spacing.padding;
4219
- const rightEdge = width - padding;
4220
- const fill = layout.theme.colors.axis;
4221
- const { chrome } = layout;
4222
- const xAxisExtent = computeXAxisExtent(layout);
4223
- const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
4224
- const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
4225
- const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter;
4226
- const a2 = createSVGElement("a");
4227
- a2.setAttribute("href", BRAND_URL);
4228
- a2.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
4229
- a2.setAttribute("target", "_blank");
4230
- a2.setAttribute("rel", "noopener");
4231
- a2.setAttribute("class", "oc-chrome-ref");
4232
- const BRAND_LARGE = 16;
4233
- const text = createSVGElement("text");
4234
- setAttrs(text, {
4235
- x: rightEdge,
4236
- y: chromeY + BRAND_LARGE,
4237
- "dominant-baseline": "alphabetic",
4238
- "font-family": layout.theme.fonts.family,
4239
- "font-size": BRAND_FONT_SIZE2,
4240
- "text-anchor": "end",
4241
- "fill-opacity": 0.55
4242
- });
4243
- text.style.setProperty("fill", fill);
4244
- const trySpan = createSVGElement("tspan");
4245
- trySpan.setAttribute("font-weight", "500");
4246
- trySpan.textContent = "try";
4247
- text.appendChild(trySpan);
4248
- const openDataSpan = createSVGElement("tspan");
4249
- openDataSpan.setAttribute("font-weight", "600");
4250
- openDataSpan.setAttribute("font-size", String(BRAND_LARGE));
4251
- openDataSpan.textContent = "OpenData";
4252
- text.appendChild(openDataSpan);
4253
- const aiSpan = createSVGElement("tspan");
4254
- aiSpan.setAttribute("font-weight", "500");
4255
- aiSpan.textContent = ".ai";
4256
- text.appendChild(aiSpan);
4257
- a2.appendChild(text);
4258
- parent.appendChild(a2);
4259
- }
4275
+
4276
+ // src/svg-renderer.ts
4277
+ var EASE_VAR_MAP = {
4278
+ smooth: "var(--oc-ease-smooth)",
4279
+ snappy: "var(--oc-ease-snappy)"
4280
+ };
4260
4281
  function renderChartSVG(layout, container, opts) {
4261
4282
  const { width, height } = layout.dimensions;
4262
4283
  const animation = layout.animation;
4263
- currentAnimation = animation;
4264
4284
  const svg = createSVGElement("svg");
4265
4285
  setAttrs(svg, {
4266
4286
  viewBox: `0 0 ${width} ${height}`,
@@ -4321,38 +4341,41 @@ function renderChartSVG(layout, container, opts) {
4321
4341
  });
4322
4342
  clipPath.appendChild(clipRect);
4323
4343
  defs.appendChild(clipPath);
4324
- currentGradientMap = buildGradientDefs(layout.marks, defs);
4344
+ const gradientMap = buildGradientDefs(layout.marks, defs);
4325
4345
  svg.appendChild(defs);
4326
- renderAxes(svg, layout);
4327
- const clippedGroup = createSVGElement("g");
4328
- clippedGroup.setAttribute("clip-path", `url(#${clipId})`);
4329
- renderMarks(clippedGroup, layout);
4330
- const hasLineOrAreaWithDataPoints = layout.marks.some(
4331
- (m2) => (m2.type === "line" || m2.type === "area") && m2.dataPoints && m2.dataPoints.length > 0
4332
- );
4333
- const hasPointMarks = layout.marks.some((m2) => m2.type === "point");
4334
- if (hasLineOrAreaWithDataPoints && !hasPointMarks) {
4335
- const overlay = createSVGElement("rect");
4336
- setAttrs(overlay, {
4337
- x: layout.area.x,
4338
- y: layout.area.y,
4339
- width: layout.area.width,
4340
- height: layout.area.height,
4341
- fill: "transparent"
4342
- });
4343
- overlay.setAttribute("class", "oc-voronoi-overlay");
4344
- overlay.setAttribute("data-voronoi-overlay", "true");
4345
- clippedGroup.appendChild(overlay);
4346
- }
4347
- svg.appendChild(clippedGroup);
4348
- renderAnnotations(svg, layout);
4349
- renderLegend(svg, layout.legend);
4350
- renderChrome(svg, layout);
4351
- if (layout.watermark) {
4352
- renderBrand(svg, layout);
4346
+ setMarkRenderState({ animation, gradientMap });
4347
+ try {
4348
+ renderAxes(svg, layout);
4349
+ const clippedGroup = createSVGElement("g");
4350
+ clippedGroup.setAttribute("clip-path", `url(#${clipId})`);
4351
+ renderMarks(clippedGroup, layout);
4352
+ const hasLineOrAreaWithDataPoints = layout.marks.some(
4353
+ (m2) => (m2.type === "line" || m2.type === "area") && m2.dataPoints && m2.dataPoints.length > 0
4354
+ );
4355
+ const hasPointMarks = layout.marks.some((m2) => m2.type === "point");
4356
+ if (hasLineOrAreaWithDataPoints && !hasPointMarks) {
4357
+ const overlay = createSVGElement("rect");
4358
+ setAttrs(overlay, {
4359
+ x: layout.area.x,
4360
+ y: layout.area.y,
4361
+ width: layout.area.width,
4362
+ height: layout.area.height,
4363
+ fill: "transparent"
4364
+ });
4365
+ overlay.setAttribute("class", "oc-voronoi-overlay");
4366
+ overlay.setAttribute("data-voronoi-overlay", "true");
4367
+ clippedGroup.appendChild(overlay);
4368
+ }
4369
+ svg.appendChild(clippedGroup);
4370
+ renderAnnotations(svg, layout);
4371
+ renderLegend(svg, layout.legend);
4372
+ renderChrome(svg, layout);
4373
+ if (layout.watermark) {
4374
+ renderBrand(svg, layout);
4375
+ }
4376
+ } finally {
4377
+ resetMarkRenderState();
4353
4378
  }
4354
- currentAnimation = void 0;
4355
- currentGradientMap = /* @__PURE__ */ new Map();
4356
4379
  container.appendChild(svg);
4357
4380
  return svg;
4358
4381
  }
@@ -6482,7 +6505,7 @@ import { compileSankey } from "@opendata-ai/openchart-engine";
6482
6505
  import {
6483
6506
  BRAND_FONT_SIZE as BRAND_FONT_SIZE3,
6484
6507
  BRAND_MIN_WIDTH as BRAND_MIN_WIDTH3,
6485
- estimateTextWidth as estimateTextWidth2,
6508
+ estimateTextWidth as estimateTextWidth4,
6486
6509
  wrapText as wrapText2
6487
6510
  } from "@opendata-ai/openchart-core";
6488
6511
  import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
@@ -6641,7 +6664,7 @@ function renderLegend2(parent, legend) {
6641
6664
  for (let i = 0; i < legend.entries.length; i++) {
6642
6665
  const entry = legend.entries[i];
6643
6666
  if (isHorizontal && i > 0) {
6644
- const labelWidth = estimateTextWidth2(
6667
+ const labelWidth = estimateTextWidth4(
6645
6668
  entry.label,
6646
6669
  legend.labelStyle.fontSize,
6647
6670
  legend.labelStyle.fontWeight
@@ -6692,7 +6715,7 @@ function renderLegend2(parent, legend) {
6692
6715
  entryG.appendChild(label);
6693
6716
  g.appendChild(entryG);
6694
6717
  if (isHorizontal) {
6695
- const labelWidth = estimateTextWidth2(
6718
+ const labelWidth = estimateTextWidth4(
6696
6719
  entry.label,
6697
6720
  legend.labelStyle.fontSize,
6698
6721
  legend.labelStyle.fontWeight