@opendata-ai/openchart-engine 8.9.0 → 8.9.1

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
@@ -1,3 +1,6 @@
1
+ // src/compile.ts
2
+ import { mergeThemeConfig as mergeThemeConfig6 } from "@opendata-ai/openchart-core";
3
+
1
4
  // src/design/diagnostics.ts
2
5
  import { computeLabelBounds, contrastRatio, isOpaqueColor } from "@opendata-ai/openchart-core";
3
6
 
@@ -76,6 +79,18 @@ function linkedEntityKeys(row, source, target) {
76
79
  }
77
80
 
78
81
  // src/design/diagnostics.ts
82
+ function noteCapacityDiagnostic(layout) {
83
+ if ((layout.chrome.noteBandHeight ?? 0) <= 0 || layout.area.height > 0) return void 0;
84
+ return {
85
+ code: "layout.note-capacity",
86
+ path: "/annotations",
87
+ origin: "author",
88
+ status: "failed",
89
+ severity: "error",
90
+ viewport: layout.dimensions,
91
+ message: 'Reserved annotation note capacity leaves no plotting space. Increase the requested height, explicitly choose chromeLayout: "grow", or reduce annotation text/count.'
92
+ };
93
+ }
79
94
  function diagnoseChart(spec, layout) {
80
95
  const diagnostics = [];
81
96
  const viewport = layout.dimensions;
@@ -176,7 +191,9 @@ function diagnoseChart(spec, layout) {
176
191
  "author"
177
192
  );
178
193
  }
179
- if (layout.area.width < 80 || layout.area.height < 80)
194
+ const exhaustedNotes = noteCapacityDiagnostic(layout);
195
+ if (exhaustedNotes) diagnostics.push(exhaustedNotes);
196
+ else if (layout.area.width < 80 || layout.area.height < 80)
180
197
  emit(
181
198
  "layout.constrained",
182
199
  "/chromeLayout",
@@ -205,17 +222,17 @@ function diagnoseChart(spec, layout) {
205
222
  import {
206
223
  adaptTheme as adaptTheme6,
207
224
  computeLabelBounds as computeLabelBounds2,
208
- footnoteBandHeight,
225
+ computeNoteBandHeight as computeNoteBandHeight2,
209
226
  generateAltText,
210
227
  generateDataTable,
211
- getBreakpoint as getBreakpoint3,
212
- getHeightClass as getHeightClass3,
213
- getLayoutStrategy,
228
+ getBreakpoint as getBreakpoint4,
229
+ getHeightClass as getHeightClass4,
230
+ getLayoutStrategy as getLayoutStrategy2,
214
231
  isAxislessMark as isAxislessMark3,
215
232
  MAX_SR_TABLE_ROWS,
216
233
  resolveChromeEconomy,
217
234
  resolveTheme as resolveTheme7,
218
- wrapText as wrapText3
235
+ wrapText as wrapText4
219
236
  } from "@opendata-ai/openchart-core";
220
237
 
221
238
  // src/annotations/collisions.ts
@@ -408,7 +425,7 @@ function estimateLabelBounds(label, measure = heuristicMeasure) {
408
425
  return computeTextBlockBounds(
409
426
  label.x,
410
427
  label.y,
411
- label.text,
428
+ label.lines?.join("\n") ?? label.text,
412
429
  {
413
430
  fontSize,
414
431
  fontWeight,
@@ -436,19 +453,38 @@ function computeArrowheadPoints(tipX, tipY, tangentX, tangentY, length = ARROWHE
436
453
  function connectorPullbackGap(markerRadius, arrow) {
437
454
  return arrow ? Math.max(markerRadius + 2, 2) : Math.max(markerRadius + 3, 4);
438
455
  }
439
- function refreshConnector(connector, bounds, markerRadius) {
456
+ function automaticControlPoint(from, to, exit, bend = 0) {
457
+ const dx = to.x - from.x;
458
+ const dy = to.y - from.y;
459
+ const vertical = exit ? exit === "vertical" : Math.abs(dy) >= Math.abs(dx);
460
+ const x = from.x + dx * (vertical ? 0.2 : 0.6);
461
+ const y = from.y + dy * (vertical ? 0.6 : 0.2);
462
+ return bend === 0 ? { x, y } : { x: x + bend * dy, y: y - bend * dx };
463
+ }
464
+ function refreshConnector(connector, bounds, markerRadius, keepVisible = false) {
440
465
  if (!connector.endpoint) return void 0;
441
466
  const endpoint = connector.endpoint;
442
467
  if (connector.style === "drop-line") {
443
468
  const gap2 = connectorPullbackGap(markerRadius, false);
444
469
  return {
445
470
  ...connector,
446
- from: { x: endpoint.x, y: bounds.y - DROP_LINE_TOP_GAP },
471
+ from: {
472
+ x: endpoint.x + (connector.fromOffset?.dx ?? 0),
473
+ y: bounds.y - DROP_LINE_TOP_GAP + (connector.fromOffset?.dy ?? 0)
474
+ },
447
475
  to: { x: endpoint.x, y: endpoint.y - gap2 },
448
476
  exit: "vertical"
449
477
  };
450
478
  }
451
- const exit = connectorExit(bounds, endpoint.x, endpoint.y);
479
+ const exit = (connector.labelAttachment === "left-center" ? {
480
+ x: bounds.x - BOX_INFLATE - CONNECTOR_STANDOFF,
481
+ y: bounds.y + bounds.height / 2,
482
+ exit: "horizontal"
483
+ } : connectorExit(bounds, endpoint.x, endpoint.y)) ?? (keepVisible ? {
484
+ x: bounds.x + bounds.width / 2,
485
+ y: bounds.y + bounds.height / 2,
486
+ exit: connector.exit ?? "horizontal"
487
+ } : null);
452
488
  if (!exit) return void 0;
453
489
  const from = {
454
490
  x: exit.x + (connector.fromOffset?.dx ?? 0),
@@ -461,8 +497,15 @@ function refreshConnector(connector, bounds, markerRadius) {
461
497
  const to = dist > 0 ? { x: endpoint.x - cdx / dist * gap, y: endpoint.y - cdy / dist * gap } : { ...endpoint };
462
498
  const lx = to.x - from.x;
463
499
  const ly = to.y - from.y;
464
- if (!connectorIsDrawable(Math.sqrt(lx * lx + ly * ly), connector.arrow)) return void 0;
465
- return { ...connector, from, to, exit: exit.exit };
500
+ if (!keepVisible && !connectorIsDrawable(Math.sqrt(lx * lx + ly * ly), connector.arrow))
501
+ return void 0;
502
+ return {
503
+ ...connector,
504
+ from,
505
+ to,
506
+ exit: exit.exit,
507
+ ...connector.style === "curve" ? { control: automaticControlPoint(from, to, exit.exit, connector.bend) } : {}
508
+ };
466
509
  }
467
510
 
468
511
  // src/annotations/position.ts
@@ -542,12 +585,76 @@ function resolvePositionEdge(value, scale, edge) {
542
585
  return center;
543
586
  }
544
587
 
588
+ // src/annotations/wrap-text.ts
589
+ function wrapAnnotationText(text, maxWidth, style, measure) {
590
+ const width = (chars) => {
591
+ let total = 0;
592
+ let run = "";
593
+ let bold = false;
594
+ const flush = () => {
595
+ total += measure(run, {
596
+ fontSize: style.fontSize,
597
+ fontWeight: bold ? BOLD_SPAN_FONT_WEIGHT : Number(style.fontWeight) || 400
598
+ });
599
+ };
600
+ for (const char of chars) {
601
+ if (char.bold !== bold) {
602
+ flush();
603
+ run = "";
604
+ bold = char.bold;
605
+ }
606
+ run += char.text;
607
+ }
608
+ flush();
609
+ return total;
610
+ };
611
+ const serialize = (chars) => {
612
+ let result = "";
613
+ let bold = false;
614
+ for (const char of chars) {
615
+ if (char.bold !== bold) {
616
+ result += "**";
617
+ bold = char.bold;
618
+ }
619
+ result += char.text;
620
+ }
621
+ return result + (bold ? "**" : "");
622
+ };
623
+ return text.split("\n").flatMap((paragraph) => {
624
+ const chars = parseAnnotationSpans(paragraph).flatMap(
625
+ (span) => Array.from(span.text, (text2) => ({ text: text2, bold: span.bold }))
626
+ );
627
+ const lines = [];
628
+ let pending = [];
629
+ for (const char of chars) {
630
+ pending.push(char);
631
+ if (width(pending) <= maxWidth || pending.length === 1) continue;
632
+ let split = -1;
633
+ for (let i = pending.length - 1; i > 0; i--)
634
+ if (/\s/.test(pending[i].text)) {
635
+ split = i;
636
+ break;
637
+ }
638
+ if (split > 0) {
639
+ lines.push(serialize(pending.slice(0, split)));
640
+ pending = pending.slice(split + 1);
641
+ } else {
642
+ const last = pending.pop();
643
+ lines.push(serialize(pending));
644
+ pending = [last];
645
+ }
646
+ }
647
+ if (pending.length || !lines.length) lines.push(serialize(pending));
648
+ return lines;
649
+ });
650
+ }
651
+
545
652
  // src/annotations/resolve-text.ts
546
- function defaultConnectorStroke(hasArrow, isDark, textInk) {
653
+ function defaultConnectorStroke(hasArrow, isDark, textInk, colors) {
547
654
  if (hasArrow) return textInk;
548
- return isDark ? DARK_CONNECTOR_STROKE : LIGHT_CONNECTOR_STROKE;
655
+ return colors?.annotationLeader ?? (isDark ? DARK_CONNECTOR_STROKE : LIGHT_CONNECTOR_STROKE);
549
656
  }
550
- function resolveDot(dotSpec, hasConnector, hasArrow, x, y, isDark, connectorStroke) {
657
+ function resolveDot(dotSpec, hasConnector, hasArrow, x, y, isDark, connectorStroke, colors) {
551
658
  const useDefault = dotSpec === void 0 && hasConnector && !hasArrow;
552
659
  if (!dotSpec && !useDefault) return void 0;
553
660
  const dotConfig = typeof dotSpec === "object" ? dotSpec : {};
@@ -555,7 +662,7 @@ function resolveDot(dotSpec, hasConnector, hasArrow, x, y, isDark, connectorStro
555
662
  x,
556
663
  y,
557
664
  radius: dotConfig.radius ?? DEFAULT_DOT_RADIUS,
558
- fill: dotConfig.fill ?? (isDark ? DARK_DOT_FILL : LIGHT_DOT_FILL),
665
+ fill: dotConfig.fill ?? colors?.annotationMarkerFill ?? (isDark ? DARK_DOT_FILL : LIGHT_DOT_FILL),
559
666
  stroke: dotConfig.stroke ?? connectorStroke,
560
667
  strokeWidth: dotConfig.strokeWidth ?? DEFAULT_DOT_STROKE_WIDTH
561
668
  };
@@ -568,15 +675,50 @@ function resolveConnectorConfig(connector) {
568
675
  if (typeof connector === "object" && connector !== null && "type" in connector) {
569
676
  const cfg = connector;
570
677
  const arrow = cfg.type === "drop-line" ? false : cfg.arrow ?? cfg.type === "curve";
571
- return { type: cfg.type, arrow };
678
+ return {
679
+ type: cfg.type,
680
+ arrow,
681
+ ...cfg.type === "curve" && cfg.bend !== void 0 ? { bend: cfg.bend } : {}
682
+ };
572
683
  }
573
684
  if (connector === "curve") return { type: "curve", arrow: true };
574
685
  if (connector === "drop-line") return { type: "drop-line", arrow: false };
575
686
  return { type: "straight", arrow: false };
576
687
  }
577
- function resolveLabelBackground(background, isDark) {
688
+ function textConnectorSeed(annotation, scales, isDark, colors) {
689
+ const config = resolveConnectorConfig(annotation.connector);
690
+ const x = resolvePosition(annotation.x, scales.x);
691
+ const y = resolvePosition(annotation.y, scales.y);
692
+ if (!config || x === null || y === null) return void 0;
693
+ const endpoint = {
694
+ x: x + (annotation.connectorOffset?.to?.dx ?? 0),
695
+ y: y + (annotation.connectorOffset?.to?.dy ?? 0)
696
+ };
697
+ return {
698
+ from: { ...endpoint },
699
+ to: { ...endpoint },
700
+ endpoint,
701
+ style: config.type,
702
+ arrow: config.arrow,
703
+ stroke: annotation.stroke ?? defaultConnectorStroke(
704
+ config.arrow,
705
+ isDark,
706
+ annotation.fill ?? colors?.annotationLabel ?? (isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL),
707
+ colors
708
+ ),
709
+ ...config.bend !== void 0 ? { bend: config.bend } : {},
710
+ ...annotation.connectorOffset?.from ? {
711
+ fromOffset: {
712
+ dx: annotation.connectorOffset.from.dx ?? 0,
713
+ dy: annotation.connectorOffset.from.dy ?? 0
714
+ }
715
+ } : {}
716
+ };
717
+ }
718
+ function resolveLabelBackground(background, isDark, colors) {
578
719
  if (background === void 0) return void 0;
579
- if (background === true) return isDark ? DARK_LABEL_BACKGROUND : LIGHT_LABEL_BACKGROUND;
720
+ if (background === true)
721
+ return colors?.annotationPlate ?? (isDark ? DARK_LABEL_BACKGROUND : LIGHT_LABEL_BACKGROUND);
580
722
  return background;
581
723
  }
582
724
  var DROP_LINE_LABEL_GAP = 8;
@@ -595,12 +737,58 @@ function resolveLedeFontWeight(annotation) {
595
737
  if (annotation.fontWeight !== void 0) return annotation.fontWeight;
596
738
  return annotation.subtitle ? LEDE_FONT_WEIGHT : void 0;
597
739
  }
598
- function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily) {
740
+ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily, colors) {
741
+ if (annotation.maxWidth == null)
742
+ return resolveTextAnnotationDisplay(
743
+ annotation,
744
+ scales,
745
+ chartArea,
746
+ isDark,
747
+ measure,
748
+ fontFamily,
749
+ colors
750
+ );
751
+ const fontSize = annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
752
+ const lines = wrapAnnotationText(
753
+ annotation.text,
754
+ annotation.maxWidth,
755
+ { fontSize, fontWeight: resolveLedeFontWeight(annotation) },
756
+ measure
757
+ );
758
+ const subLines = annotation.subtitle ? wrapAnnotationText(
759
+ annotation.subtitle,
760
+ annotation.maxWidth,
761
+ {
762
+ fontSize: annotation.subtitleFontSize ?? subtitleFontSize(fontSize),
763
+ fontWeight: SUBTITLE_FONT_WEIGHT
764
+ },
765
+ measure
766
+ ) : void 0;
767
+ const resolved = resolveTextAnnotationDisplay(
768
+ { ...annotation, text: lines.join("\n"), subtitle: subLines?.join("\n") },
769
+ scales,
770
+ chartArea,
771
+ isDark,
772
+ measure,
773
+ fontFamily,
774
+ colors
775
+ );
776
+ if (resolved?.label) {
777
+ resolved.label.text = annotation.text;
778
+ resolved.label.lines = lines;
779
+ }
780
+ if (resolved?.subtitle) {
781
+ resolved.subtitle.text = annotation.subtitle;
782
+ resolved.subtitle.lines = subLines;
783
+ }
784
+ return resolved;
785
+ }
786
+ function resolveTextAnnotationDisplay(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily, colors) {
599
787
  const px = resolvePosition(annotation.x, scales.x);
600
788
  const py = resolvePosition(annotation.y, scales.y);
601
789
  if (px === null || py === null) return null;
602
- const defaultTextFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
603
- const labelBackground = resolveLabelBackground(annotation.background, isDark);
790
+ const defaultTextFill = colors?.annotationLabel ?? (isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL);
791
+ const labelBackground = resolveLabelBackground(annotation.background, isDark, colors);
604
792
  const labelStyle = makeAnnotationLabelStyle(
605
793
  annotation.fontSize,
606
794
  resolveLedeFontWeight(annotation),
@@ -618,7 +806,8 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
618
806
  labelStyle,
619
807
  labelBackground,
620
808
  isDark,
621
- measure
809
+ measure,
810
+ colors
622
811
  );
623
812
  }
624
813
  labelStyle.textAnchor = annotation.anchor === "left" ? "end" : "start";
@@ -640,7 +829,7 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
640
829
  annotation.subtitle,
641
830
  {
642
831
  ...labelStyle,
643
- fontSize: subtitleFontSize(fontSize),
832
+ fontSize: annotation.subtitleFontSize ?? subtitleFontSize(fontSize),
644
833
  fontWeight: SUBTITLE_FONT_WEIGHT,
645
834
  textAnchor: "start"
646
835
  },
@@ -658,10 +847,10 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
658
847
  let annotationBounds = labelBounds;
659
848
  if (annotation.subtitle) {
660
849
  const primaryLineCount = annotation.text.split("\n").length;
661
- const mutedFill = isDark ? DARK_MUTED_TEXT_FILL : LIGHT_MUTED_TEXT_FILL;
850
+ const mutedFill = colors?.annotationMuted ?? (isDark ? DARK_MUTED_TEXT_FILL : LIGHT_MUTED_TEXT_FILL);
662
851
  const subtitleStyle = {
663
852
  ...labelStyle,
664
- fontSize: subtitleFontSize(fontSize),
853
+ fontSize: annotation.subtitleFontSize ?? subtitleFontSize(fontSize),
665
854
  fontWeight: SUBTITLE_FONT_WEIGHT,
666
855
  fill: mutedFill
667
856
  };
@@ -685,7 +874,7 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
685
874
  x: px + (annotation.connectorOffset?.to?.dx ?? 0),
686
875
  y: py + (annotation.connectorOffset?.to?.dy ?? 0)
687
876
  };
688
- const connectorStroke = annotation.stroke ?? defaultConnectorStroke(connectorArrow, isDark, labelStyle.fill ?? defaultTextFill);
877
+ const connectorStroke = annotation.stroke ?? defaultConnectorStroke(connectorArrow, isDark, labelStyle.fill ?? defaultTextFill, colors);
689
878
  const dot = resolveDot(
690
879
  annotation.dot,
691
880
  showConnector,
@@ -693,7 +882,8 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
693
882
  endpoint.x,
694
883
  endpoint.y,
695
884
  isDark,
696
- connectorStroke
885
+ connectorStroke,
886
+ colors
697
887
  );
698
888
  let connector;
699
889
  if (showConnector) {
@@ -722,7 +912,11 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
722
912
  stroke: connectorStroke,
723
913
  style: connectorStyle,
724
914
  arrow: connectorArrow,
725
- exit: exit.exit
915
+ exit: exit.exit,
916
+ ...connectorStyle === "curve" ? {
917
+ control: automaticControlPoint(from, to, exit.exit, connectorConfig?.bend),
918
+ ...connectorConfig?.bend !== void 0 ? { bend: connectorConfig.bend } : {}
919
+ } : {}
726
920
  };
727
921
  }
728
922
  }
@@ -751,12 +945,12 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
751
945
  bounds: annotationBounds
752
946
  };
753
947
  }
754
- function resolveDropLineAnnotation(annotation, px, py, chartArea, labelStyle, labelBackground, isDark, measure = heuristicMeasure) {
948
+ function resolveDropLineAnnotation(annotation, px, py, chartArea, labelStyle, labelBackground, isDark, measure = heuristicMeasure, colors) {
755
949
  const fontSize = annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
756
950
  const fontWeight = Number(labelStyle.fontWeight) || DEFAULT_ANNOTATION_FONT_WEIGHT;
757
951
  const lines = annotation.text.split("\n");
758
952
  const subtitleLines = annotation.subtitle ? annotation.subtitle.split("\n") : [];
759
- const subFontSize = subtitleFontSize(fontSize);
953
+ const subFontSize = annotation.subtitleFontSize ?? subtitleFontSize(fontSize);
760
954
  const estimatedWidth = Math.max(
761
955
  0,
762
956
  ...lines.map((line4) => measureRichLine(line4, { fontSize, fontWeight }, measure)),
@@ -774,21 +968,37 @@ function resolveDropLineAnnotation(annotation, px, py, chartArea, labelStyle, la
774
968
  } else if (side === "right" && !fitsRight) {
775
969
  side = fitsLeft || spaceLeft > spaceRight ? "left" : "right";
776
970
  }
777
- const labelX = side === "left" ? px - DROP_LINE_LABEL_GAP : px + DROP_LINE_LABEL_GAP;
971
+ const labelX = (side === "left" ? px - DROP_LINE_LABEL_GAP : px + DROP_LINE_LABEL_GAP) + (annotation.offset?.dx ?? 0);
778
972
  const textAnchor = side === "left" ? "end" : "start";
779
973
  const lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
780
974
  const subtitleHeight = annotation.subtitle ? SUBTITLE_GAP + subFontSize * DEFAULT_LINE_HEIGHT * subtitleLines.length : 0;
781
975
  const totalHeight = lineHeight * lines.length + subtitleHeight;
782
976
  const desiredLabelTopY = py - totalHeight - 12;
783
977
  const minLabelTopY = chartArea.y + 4;
784
- const labelTopY = Math.max(desiredLabelTopY, minLabelTopY);
978
+ const labelTopY = Math.max(desiredLabelTopY, minLabelTopY) + (annotation.offset?.dy ?? 0);
785
979
  const labelBaselineY = labelTopY + fontSize;
786
980
  const resolvedStyle = { ...labelStyle, textAnchor };
787
- const connectorStroke = annotation.stroke ?? (isDark ? DARK_CONNECTOR_STROKE : LIGHT_CONNECTOR_STROKE);
788
- const dot = resolveDot(annotation.dot, true, false, px, py, isDark, connectorStroke);
981
+ const connectorStroke = annotation.stroke ?? colors?.annotationLeader ?? (isDark ? DARK_CONNECTOR_STROKE : LIGHT_CONNECTOR_STROKE);
982
+ const endpoint = {
983
+ x: px + (annotation.connectorOffset?.to?.dx ?? 0),
984
+ y: py + (annotation.connectorOffset?.to?.dy ?? 0)
985
+ };
986
+ const dot = resolveDot(
987
+ annotation.dot,
988
+ true,
989
+ false,
990
+ endpoint.x,
991
+ endpoint.y,
992
+ isDark,
993
+ connectorStroke,
994
+ colors
995
+ );
789
996
  const bottomGap = connectorPullbackGap(markerClearance(dot), false);
790
- const from = { x: px, y: labelTopY - DROP_LINE_TOP_GAP };
791
- const to = { x: px, y: py - bottomGap };
997
+ const from = {
998
+ x: endpoint.x + (annotation.connectorOffset?.from?.dx ?? 0),
999
+ y: labelTopY - DROP_LINE_TOP_GAP + (annotation.connectorOffset?.from?.dy ?? 0)
1000
+ };
1001
+ const to = { x: endpoint.x, y: endpoint.y - bottomGap };
792
1002
  const labelBounds = computeTextBlockBounds(
793
1003
  labelX,
794
1004
  labelBaselineY,
@@ -803,7 +1013,7 @@ function resolveDropLineAnnotation(annotation, px, py, chartArea, labelStyle, la
803
1013
  ...resolvedStyle,
804
1014
  fontSize: subFontSize,
805
1015
  fontWeight: SUBTITLE_FONT_WEIGHT,
806
- fill: isDark ? DARK_MUTED_TEXT_FILL : LIGHT_MUTED_TEXT_FILL
1016
+ fill: colors?.annotationMuted ?? (isDark ? DARK_MUTED_TEXT_FILL : LIGHT_MUTED_TEXT_FILL)
807
1017
  };
808
1018
  const subtitleY = subtitleBaselineY(labelBaselineY, lineHeight, lines.length);
809
1019
  subtitle = {
@@ -826,7 +1036,13 @@ function resolveDropLineAnnotation(annotation, px, py, chartArea, labelStyle, la
826
1036
  connector: {
827
1037
  from,
828
1038
  to,
829
- endpoint: { x: px, y: py },
1039
+ endpoint,
1040
+ ...annotation.connectorOffset?.from ? {
1041
+ fromOffset: {
1042
+ dx: annotation.connectorOffset.from.dx ?? 0,
1043
+ dy: annotation.connectorOffset.from.dy ?? 0
1044
+ }
1045
+ } : {},
830
1046
  stroke: connectorStroke,
831
1047
  style: "drop-line",
832
1048
  arrow: false,
@@ -860,7 +1076,7 @@ function annotationBlockBounds(annotation, label, measure) {
860
1076
  const subBounds = computeTextBlockBounds(
861
1077
  sub.x,
862
1078
  sub.y,
863
- sub.text,
1079
+ sub.lines?.join("\n") ?? sub.text,
864
1080
  {
865
1081
  // Every resolver stamps all three, so these fallbacks are unreachable today.
866
1082
  // They still come from the constants: a hand-rolled `?? 10` here is a second
@@ -1033,9 +1249,6 @@ function sampleAreaObstacles(topPoints, bottomPoints, bucketWidth = 24, pad = 3)
1033
1249
  }
1034
1250
  return [...lineObs, ...fillObs];
1035
1251
  }
1036
- function normalizeObstacles(rects, kind = "mark") {
1037
- return rects.map((r) => ({ ...r, kind }));
1038
- }
1039
1252
  var OBSTACLE_WEIGHTS = {
1040
1253
  annotation: 30,
1041
1254
  line: 20,
@@ -1293,7 +1506,7 @@ function findBestPlacement(anchorX, anchorY, text, style, subtitleText, subtitle
1293
1506
  };
1294
1507
  }
1295
1508
  function isAutoPlacement(annotation) {
1296
- if (annotation.offset) return false;
1509
+ if (annotation.offset !== void 0) return false;
1297
1510
  if (annotation.anchor && annotation.anchor !== "auto") return false;
1298
1511
  if (annotation.connector === "drop-line") return false;
1299
1512
  if (typeof annotation.connector === "object" && annotation.connector !== null && annotation.connector.type === "drop-line")
@@ -1318,8 +1531,7 @@ function generateNudgeCandidates(selfBounds, obstacles, padding) {
1318
1531
  return candidates;
1319
1532
  }
1320
1533
  function hasExplicitOffset(annotation) {
1321
- const moved = (annotation.offset?.dx ?? 0) !== 0 || (annotation.offset?.dy ?? 0) !== 0;
1322
- return moved && !isAutoPlacement(annotation);
1534
+ return annotation.offset !== void 0 && !isAutoPlacement(annotation);
1323
1535
  }
1324
1536
  function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, chartArea, obstacles, measure = heuristicMeasure) {
1325
1537
  if (annotation.type !== "text" || !annotation.label) return false;
@@ -1530,8 +1742,310 @@ function collectPinnedOverlapWarnings(annotations, specs, obstacles, measure = h
1530
1742
  return warnings;
1531
1743
  }
1532
1744
 
1745
+ // src/annotations/edit-context.ts
1746
+ import { annotationSource } from "@opendata-ai/openchart-core";
1747
+ var geometry = /* @__PURE__ */ new WeakMap();
1748
+ function rememberAnnotationGeometry(annotation, authored, context) {
1749
+ geometry.set(annotation, { context, authored });
1750
+ }
1751
+ var snapshots = /* @__PURE__ */ new WeakMap();
1752
+ function createAnnotationEditContext(layout, spec) {
1753
+ const source = annotationSource(spec);
1754
+ const entries = [];
1755
+ const panels = layout.facet ? layout.facet.panels.map((p) => ({ annotations: p.annotations, facet: p.key })) : [{ annotations: layout.annotations, facet: void 0 }];
1756
+ for (const panel of panels) {
1757
+ for (const resolved of panel.annotations) {
1758
+ if (resolved.type !== "text" || !resolved.label || !resolved.bounds) continue;
1759
+ const metadata = geometry.get(resolved);
1760
+ if (!metadata || !source) continue;
1761
+ const index = resolved.specIndex;
1762
+ if (index === void 0) continue;
1763
+ const authored = source.annotations[index];
1764
+ if (!authored || authored.type !== "text" || authored.id !== resolved.id) continue;
1765
+ const context = metadata.context;
1766
+ const anchor = resolved.label.style.textAnchor === "end" ? "left" : "right";
1767
+ const base = resolveTextAnnotation(
1768
+ { ...metadata.authored, anchor, offset: void 0 },
1769
+ context.scales,
1770
+ context.chartArea,
1771
+ context.isDark,
1772
+ context.measure,
1773
+ context.fontFamily,
1774
+ context.colors
1775
+ );
1776
+ if (!base) continue;
1777
+ entries.push({
1778
+ ref: {
1779
+ type: "annotation",
1780
+ index,
1781
+ id: authored.id,
1782
+ path: `${source.path}/${index}`,
1783
+ ...panel.facet !== void 0 ? { facetInstance: panel.facet } : {}
1784
+ },
1785
+ resolved: structuredClone(resolved),
1786
+ base,
1787
+ anchor,
1788
+ connectorSeed: textConnectorSeed(
1789
+ metadata.authored,
1790
+ context.scales,
1791
+ context.isDark,
1792
+ context.colors
1793
+ ),
1794
+ context: {
1795
+ ...context,
1796
+ chartArea: { ...context.chartArea },
1797
+ svg: { ...context.svg },
1798
+ obstacles: []
1799
+ }
1800
+ });
1801
+ }
1802
+ }
1803
+ const snapshot = Object.freeze({ kind: "annotation-edit-context" });
1804
+ snapshots.set(snapshot, entries);
1805
+ return snapshot;
1806
+ }
1807
+ function entryFor(ctx, ref) {
1808
+ if (ref.type !== "annotation") throw new Error("Expected an annotation reference");
1809
+ const matches = (snapshots.get(ctx) ?? []).filter(
1810
+ (entry) => entry.ref.index === ref.index && (ref.id === void 0 || entry.ref.id === ref.id) && (ref.path === void 0 || ref.path === entry.ref.path || ref.path.startsWith(`${entry.ref.path}/`)) && (ref.facetInstance === void 0 || ref.facetInstance === entry.ref.facetInstance)
1811
+ );
1812
+ if (matches.length !== 1)
1813
+ throw new Error("Annotation reference is missing or ambiguous in this edit context");
1814
+ return matches[0];
1815
+ }
1816
+ function inverseLabelPlacement(ctx, ref, renderedBox) {
1817
+ const entry = entryFor(ctx, ref);
1818
+ const bounds = entry.base.bounds;
1819
+ return {
1820
+ anchor: entry.anchor,
1821
+ offset: {
1822
+ dx: Math.round((renderedBox.x - bounds.x) * 1e10) / 1e10,
1823
+ dy: Math.round((renderedBox.y - bounds.y) * 1e10) / 1e10
1824
+ },
1825
+ responsive: false
1826
+ };
1827
+ }
1828
+ function previewAnnotationMove(ctx, ref, delta) {
1829
+ const entry = entryFor(ctx, ref);
1830
+ const annotation = structuredClone(entry.resolved);
1831
+ const context = entry.context;
1832
+ if (!annotation.label.connector && entry.connectorSeed)
1833
+ annotation.label.connector = structuredClone(entry.connectorSeed);
1834
+ moveAnnotationBy(annotation, annotation.label, delta.dx, delta.dy, context.measure);
1835
+ clampAnnotationsToBounds(
1836
+ [annotation],
1837
+ context.svg.width,
1838
+ context.svg.height,
1839
+ context.measure,
1840
+ context.topBound
1841
+ );
1842
+ return annotation;
1843
+ }
1844
+ function previewConnectorBend(ctx, ref, bend) {
1845
+ if (!Number.isFinite(bend) || bend < -1 || bend > 1)
1846
+ throw new RangeError("Connector bend must be between -1 and 1");
1847
+ const annotation = structuredClone(entryFor(ctx, ref).resolved);
1848
+ const connector = annotation.label?.connector;
1849
+ if (connector?.style === "curve")
1850
+ annotation.label.connector = refreshConnector(
1851
+ { ...connector, bend },
1852
+ annotation.bounds,
1853
+ markerClearance(annotation.dot)
1854
+ );
1855
+ return annotation;
1856
+ }
1857
+ function previewConnectorOffset(ctx, ref, end, delta) {
1858
+ const entry = entryFor(ctx, ref);
1859
+ const annotation = structuredClone(entry.resolved);
1860
+ const connector = structuredClone(annotation.label?.connector ?? entry.connectorSeed);
1861
+ if (!connector?.endpoint) return annotation;
1862
+ if (end === "from")
1863
+ connector.fromOffset = {
1864
+ dx: (connector.fromOffset?.dx ?? 0) + delta.dx,
1865
+ dy: (connector.fromOffset?.dy ?? 0) + delta.dy
1866
+ };
1867
+ else {
1868
+ connector.endpoint.x += delta.dx;
1869
+ connector.endpoint.y += delta.dy;
1870
+ if (annotation.dot) {
1871
+ annotation.dot.x += delta.dx;
1872
+ annotation.dot.y += delta.dy;
1873
+ }
1874
+ }
1875
+ annotation.label.connector = refreshConnector(
1876
+ connector,
1877
+ annotation.bounds,
1878
+ markerClearance(annotation.dot)
1879
+ );
1880
+ return annotation;
1881
+ }
1882
+
1883
+ // src/annotations/reference-layout.ts
1884
+ import { overlapArea as overlapArea3 } from "@opendata-ai/openchart-core";
1885
+ function referenceLabelReserve(annotations, width, measure) {
1886
+ const outside = annotations.filter(
1887
+ (a) => a.type === "refline" && a.labelPlacement === "outside" && !!a.label
1888
+ );
1889
+ const labelWidth = Math.max(24, Math.min(160, width * 0.25));
1890
+ const vertical = outside.filter((a) => a.y === void 0);
1891
+ const top = vertical.reduce(
1892
+ (height, a) => height + wrapAnnotationText(
1893
+ a.label,
1894
+ labelWidth,
1895
+ { fontSize: a.fontSize ?? 11, fontWeight: a.fontWeight ?? 400 },
1896
+ measure
1897
+ ).length * (a.fontSize ?? 11) * 1.3 + 8,
1898
+ 0
1899
+ );
1900
+ return {
1901
+ right: outside.some((a) => a.y !== void 0) ? labelWidth + 32 : 0,
1902
+ top: top ? top + 24 : 0,
1903
+ labelWidth
1904
+ };
1905
+ }
1906
+ function placeReferenceLabel(annotation, resolved, area3, container, reserve, lanes, measure, colors, warn, obstacles = []) {
1907
+ const label = resolved.label;
1908
+ const line4 = resolved.line;
1909
+ if (!label || !line4 || !annotation.labelPlacement) return;
1910
+ const horizontal = annotation.y !== void 0;
1911
+ const initial = { x: label.x, y: label.y };
1912
+ const bounds = () => estimateLabelBounds(label, measure);
1913
+ const fits = (box) => box.x >= container.x && box.y >= container.y && box.x + box.width <= container.x + container.width && box.y + box.height <= container.y + container.height;
1914
+ let target = horizontal ? line4.end : line4.start;
1915
+ if (annotation.labelOffset !== void 0) {
1916
+ if (!fits(bounds()))
1917
+ warn?.(
1918
+ "[openchart] Reference labelOffset pins an overflowing label; remove labelOffset to allow automatic containment."
1919
+ );
1920
+ } else if (annotation.labelPlacement === "outside") {
1921
+ label.lines = wrapAnnotationText(
1922
+ label.text,
1923
+ reserve.labelWidth,
1924
+ { fontSize: label.style.fontSize, fontWeight: label.style.fontWeight },
1925
+ measure
1926
+ );
1927
+ label.style = { ...label.style, textAnchor: "start" };
1928
+ const size = bounds();
1929
+ if (horizontal) {
1930
+ label.x = container.x + container.width - reserve.right + 24;
1931
+ const previous = lanes.rightLabels?.at(-1)?.bounds;
1932
+ const top = Math.max(
1933
+ area3.y,
1934
+ line4.end.y - size.height / 2,
1935
+ previous ? previous.y + previous.height + 8 : area3.y
1936
+ );
1937
+ label.y += top - bounds().y;
1938
+ resolved.bounds = bounds();
1939
+ (lanes.rightLabels ??= []).push(resolved);
1940
+ const bottom = Math.min(container.y + container.height, area3.y + area3.height);
1941
+ let nextTop = bottom + 8;
1942
+ for (const item of [...lanes.rightLabels].reverse()) {
1943
+ const box = estimateLabelBounds(item.label, measure);
1944
+ const dy = Math.min(0, nextTop - 8 - box.y - box.height);
1945
+ item.label.y += dy;
1946
+ item.bounds = item.label.bounds = estimateLabelBounds(item.label, measure);
1947
+ nextTop = item.bounds.y;
1948
+ if (item.label.connector)
1949
+ item.label.connector = refreshConnector(item.label.connector, item.bounds, 0);
1950
+ }
1951
+ if (nextTop < area3.y)
1952
+ warn?.(
1953
+ "[openchart] Outside reference labels exceed their available lane; use shorter labels or a taller figure."
1954
+ );
1955
+ } else {
1956
+ label.x = Math.min(
1957
+ container.x + container.width - size.width,
1958
+ Math.max(area3.x, line4.start.x - size.width / 2)
1959
+ );
1960
+ const top = lanes.topBottom ?? area3.y - reserve.top;
1961
+ label.y += top - bounds().y;
1962
+ lanes.topBottom = top + size.height + 8;
1963
+ }
1964
+ } else if (annotation.labelPlacement === "auto") {
1965
+ const original = { x: label.x, y: label.y, anchor: label.style.textAnchor };
1966
+ const candidates = horizontal ? [
1967
+ { x: line4.end.x - 4, y: label.y, anchor: "end" },
1968
+ { x: line4.start.x + 4, y: label.y, anchor: "start" }
1969
+ ] : [
1970
+ { x: label.x, y: line4.start.y + 14, anchor: label.style.textAnchor },
1971
+ { x: label.x, y: line4.end.y - 4, anchor: label.style.textAnchor }
1972
+ ];
1973
+ const relevant = obstacles.filter((o) => o.kind !== "area-fill");
1974
+ const clear = (box) => fits(box) && relevant.every((o) => overlapArea3(box, o) === 0);
1975
+ let found = false;
1976
+ for (const candidate of candidates) {
1977
+ Object.assign(label, { x: candidate.x, y: candidate.y });
1978
+ label.style = { ...label.style, textAnchor: candidate.anchor };
1979
+ if (clear(bounds())) {
1980
+ found = true;
1981
+ break;
1982
+ }
1983
+ }
1984
+ if (!found) {
1985
+ Object.assign(label, { x: original.x, y: original.y });
1986
+ label.style = { ...label.style, textAnchor: original.anchor };
1987
+ const box = bounds();
1988
+ const shifts = horizontal ? [
1989
+ container.x - box.x,
1990
+ container.x + container.width - box.x - box.width,
1991
+ ...relevant.flatMap((o) => [o.x - box.x - box.width - 4, o.x + o.width - box.x + 4])
1992
+ ] : [
1993
+ container.y - box.y,
1994
+ container.y + container.height - box.y - box.height,
1995
+ ...relevant.flatMap((o) => [o.y - box.y - box.height - 4, o.y + o.height - box.y + 4])
1996
+ ];
1997
+ for (const shift of [...new Set(shifts)].sort((a, b) => Math.abs(a) - Math.abs(b)).slice(0, 64)) {
1998
+ label.x = original.x + (horizontal ? shift : 0);
1999
+ label.y = original.y + (horizontal ? 0 : shift);
2000
+ if (clear(bounds())) {
2001
+ found = true;
2002
+ break;
2003
+ }
2004
+ }
2005
+ if (!found) {
2006
+ label.x = original.x;
2007
+ label.y = original.y;
2008
+ const shift = horizontal ? Math.max(
2009
+ container.x - box.x,
2010
+ Math.min(0, container.x + container.width - box.x - box.width)
2011
+ ) : Math.max(
2012
+ container.y - box.y,
2013
+ Math.min(0, container.y + container.height - box.y - box.height)
2014
+ );
2015
+ if (horizontal) label.x += shift;
2016
+ else label.y += shift;
2017
+ warn?.(
2018
+ '[openchart] Reference label has no clear contained position; use labelPlacement: "outside" or shorter text.'
2019
+ );
2020
+ }
2021
+ }
2022
+ }
2023
+ label.background = colors?.annotationPlate;
2024
+ label.bounds = bounds();
2025
+ resolved.bounds = label.bounds;
2026
+ if (annotation.labelPlacement === "outside" || Math.hypot(label.x - initial.x, label.y - initial.y) > 8) {
2027
+ if (annotation.labelPlacement !== "outside")
2028
+ target = horizontal ? { x: initial.x, y: line4.start.y } : { x: line4.start.x, y: initial.y };
2029
+ label.connector = refreshConnector(
2030
+ {
2031
+ from: { ...target },
2032
+ to: { ...target },
2033
+ endpoint: { ...target },
2034
+ stroke: resolved.stroke ?? "#666666",
2035
+ style: "straight",
2036
+ arrow: false,
2037
+ // The whole leader stays left of the right-gutter text, even when a
2038
+ // tall neighbor displaces this label. Persist through sweep/tween rebuilds.
2039
+ ...annotation.labelPlacement === "outside" && horizontal && annotation.labelOffset === void 0 ? { labelAttachment: "left-center" } : {}
2040
+ },
2041
+ label.bounds,
2042
+ 0
2043
+ );
2044
+ }
2045
+ }
2046
+
1533
2047
  // src/annotations/resolve-range.ts
1534
- function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamily) {
2048
+ function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamily, colors) {
1535
2049
  let x = chartArea.x;
1536
2050
  let y = chartArea.y;
1537
2051
  let width = chartArea.width;
@@ -1563,7 +2077,7 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamil
1563
2077
  const style = makeAnnotationLabelStyle(
1564
2078
  annotation.fontSize ?? 11,
1565
2079
  annotation.fontWeight ?? 500,
1566
- void 0,
2080
+ colors?.annotationLabel,
1567
2081
  isDark,
1568
2082
  fontFamily
1569
2083
  );
@@ -1587,7 +2101,7 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamil
1587
2101
  id: annotation.id,
1588
2102
  rect,
1589
2103
  label,
1590
- fill: annotation.fill ?? DEFAULT_RANGE_FILL,
2104
+ fill: annotation.fill ?? colors?.annotationRangeFill ?? DEFAULT_RANGE_FILL,
1591
2105
  opacity: annotation.opacity ?? defaultOpacity,
1592
2106
  stroke: annotation.stroke,
1593
2107
  zIndex: annotation.zIndex
@@ -1595,7 +2109,7 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamil
1595
2109
  }
1596
2110
 
1597
2111
  // src/annotations/resolve-refline.ts
1598
- function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily) {
2112
+ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily, colors) {
1599
2113
  let start;
1600
2114
  let end;
1601
2115
  if (annotation.y !== void 0) {
@@ -1669,8 +2183,24 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure
1669
2183
  textAnchor = "start";
1670
2184
  }
1671
2185
  }
2186
+ if (annotation.labelPlacement) {
2187
+ const atStart = annotation.labelPlacement === "start" || !isHorizontal && annotation.labelPlacement !== "end";
2188
+ if (isHorizontal) {
2189
+ labelX = atStart ? start.x : end.x;
2190
+ labelY = start.y;
2191
+ baseDx = atStart ? 4 : -4;
2192
+ baseDy = anchor === "bottom" ? 14 : -4;
2193
+ textAnchor = atStart ? "start" : "end";
2194
+ } else {
2195
+ labelX = start.x;
2196
+ labelY = atStart ? start.y : end.y;
2197
+ baseDx = anchor === "right" ? -4 : 4;
2198
+ baseDy = atStart ? 14 : -4;
2199
+ textAnchor = anchor === "right" ? "end" : "start";
2200
+ }
2201
+ }
1672
2202
  const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
1673
- const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
2203
+ const defaultStroke2 = colors?.annotationRule ?? (isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE);
1674
2204
  const style = makeAnnotationLabelStyle(
1675
2205
  annotation.fontSize ?? 11,
1676
2206
  annotation.fontWeight ?? 400,
@@ -1687,7 +2217,7 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure
1687
2217
  visible: true
1688
2218
  };
1689
2219
  }
1690
- const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
2220
+ const defaultStroke = colors?.annotationRule ?? (isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE);
1691
2221
  const resolved = {
1692
2222
  type: "refline",
1693
2223
  id: annotation.id,
@@ -1705,24 +2235,12 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure
1705
2235
  }
1706
2236
 
1707
2237
  // src/annotations/compute.ts
1708
- function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obstacles, svgDimensions) {
1709
- let ctx;
1710
- if ("measure" in scalesOrCtx) {
1711
- ctx = scalesOrCtx;
1712
- } else {
1713
- ctx = {
1714
- scales: scalesOrCtx,
1715
- chartArea,
1716
- strategy,
1717
- isDark: isDark ?? false,
1718
- obstacles: obstacles ? normalizeObstacles(obstacles) : [],
1719
- svg: svgDimensions ?? { width: 0, height: 0 },
1720
- measure: heuristicMeasure
1721
- };
1722
- }
2238
+ function computeAnnotations(spec, ctx) {
1723
2239
  const isCompact = ctx.strategy.annotationPosition === "tooltip-only" && !ctx.autoThin;
1724
2240
  const svgRect = { x: 0, y: 0, width: ctx.svg.width, height: ctx.svg.height };
1725
2241
  const annotations = [];
2242
+ const referenceReserve = ctx.referenceReserve ?? referenceLabelReserve(spec.annotations, ctx.svg.width, ctx.measure);
2243
+ const referenceLanes = ctx.referenceLanes ?? {};
1726
2244
  const autoQueue = [];
1727
2245
  for (const [specIndex, annotation] of spec.annotations.entries()) {
1728
2246
  if (isCompact && annotation.responsive !== false) {
@@ -1737,7 +2255,8 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1737
2255
  ctx.chartArea,
1738
2256
  ctx.isDark,
1739
2257
  ctx.measure,
1740
- ctx.fontFamily
2258
+ ctx.fontFamily,
2259
+ ctx.colors
1741
2260
  );
1742
2261
  if (resolved && ctx.svg.width > 0 && ctx.svg.height > 0 && isAutoPlacement(annotation)) {
1743
2262
  const px = resolvePosition(annotation.x, ctx.scales.x);
@@ -1762,7 +2281,8 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1762
2281
  ctx.scales,
1763
2282
  ctx.chartArea,
1764
2283
  ctx.isDark,
1765
- ctx.fontFamily
2284
+ ctx.fontFamily,
2285
+ ctx.colors
1766
2286
  );
1767
2287
  break;
1768
2288
  case "refline":
@@ -1772,7 +2292,8 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1772
2292
  ctx.chartArea,
1773
2293
  ctx.isDark,
1774
2294
  ctx.measure,
1775
- ctx.fontFamily
2295
+ ctx.fontFamily,
2296
+ ctx.colors
1776
2297
  );
1777
2298
  break;
1778
2299
  }
@@ -1787,7 +2308,30 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1787
2308
  ctx.measure
1788
2309
  );
1789
2310
  }
1790
- if (annotation.type === "refline" && ctx.obstacles.length > 0) {
2311
+ if (annotation.type === "refline" && annotation.labelPlacement)
2312
+ placeReferenceLabel(
2313
+ annotation,
2314
+ resolved,
2315
+ ctx.chartArea,
2316
+ ctx.referenceContainer ?? {
2317
+ x: 8,
2318
+ y: ctx.topBound ?? 8,
2319
+ width: ctx.svg.width - 16,
2320
+ height: ctx.svg.height - (ctx.topBound ?? 8) - 8
2321
+ },
2322
+ referenceReserve,
2323
+ referenceLanes,
2324
+ ctx.measure,
2325
+ ctx.colors,
2326
+ ctx.onWarn,
2327
+ [
2328
+ ...ctx.obstacles,
2329
+ ...annotations.flatMap(
2330
+ (a) => a.bounds ? [{ ...a.bounds, kind: "annotation" }] : []
2331
+ )
2332
+ ]
2333
+ );
2334
+ if (annotation.type === "refline" && !annotation.labelPlacement && ctx.obstacles.length > 0) {
1791
2335
  nudgeRefLineLabel(resolved, annotation, ctx.obstacles, ctx.chartArea, ctx.measure);
1792
2336
  }
1793
2337
  resolved.specIndex = specIndex;
@@ -1812,7 +2356,7 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1812
2356
  ctx.fontFamily
1813
2357
  );
1814
2358
  const subtitleStyle = annotation.subtitle ? {
1815
- fontSize: subtitleFontSize(labelStyle.fontSize),
2359
+ fontSize: annotation.subtitleFontSize ?? subtitleFontSize(labelStyle.fontSize),
1816
2360
  fontWeight: SUBTITLE_FONT_WEIGHT,
1817
2361
  lineHeight: labelStyle.lineHeight,
1818
2362
  fontFamily: labelStyle.fontFamily
@@ -1820,14 +2364,14 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1820
2364
  const result = findBestPlacement(
1821
2365
  anchorX,
1822
2366
  anchorY,
1823
- annotation.text,
2367
+ resolved.label?.lines?.join("\n") ?? annotation.text,
1824
2368
  {
1825
2369
  fontSize: labelStyle.fontSize,
1826
2370
  fontWeight: Number(labelStyle.fontWeight) || 400,
1827
2371
  lineHeight: labelStyle.lineHeight,
1828
2372
  fontFamily: labelStyle.fontFamily
1829
2373
  },
1830
- annotation.subtitle,
2374
+ resolved.subtitle?.lines?.join("\n") ?? annotation.subtitle,
1831
2375
  subtitleStyle,
1832
2376
  workingObstacles,
1833
2377
  ctx.chartArea,
@@ -1848,19 +2392,6 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1848
2392
  const placedBounds = resolved.bounds ?? result.bounds;
1849
2393
  workingObstacles.push({ ...placedBounds, kind: "annotation" });
1850
2394
  }
1851
- } else if (autoQueue.length > 0) {
1852
- for (const entry of autoQueue) {
1853
- if (ctx.obstacles.length > 0) {
1854
- nudgeAnnotationFromObstacles(
1855
- entry.resolved,
1856
- entry.annotation,
1857
- ctx.scales,
1858
- ctx.chartArea,
1859
- ctx.obstacles,
1860
- ctx.measure
1861
- );
1862
- }
1863
- }
1864
2395
  }
1865
2396
  const autoIndices = new Set(autoQueue.map((q) => q.index));
1866
2397
  resolveAnnotationCollisions(
@@ -1875,15 +2406,73 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
1875
2406
  clampAnnotationsToBounds(annotations, ctx.svg.width, ctx.svg.height, ctx.measure, ctx.topBound);
1876
2407
  }
1877
2408
  annotations.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
2409
+ for (const resolved of annotations) {
2410
+ const authored = spec.annotations[resolved.specIndex];
2411
+ if (authored?.type === "text") rememberAnnotationGeometry(resolved, authored, ctx);
2412
+ }
1878
2413
  return annotations;
1879
2414
  }
1880
2415
 
2416
+ // src/annotations/notes.ts
2417
+ import { estimateTextWidth as estimateTextWidth2, wrapText } from "@opendata-ai/openchart-core";
2418
+ function wrapAnnotationNotes(notes, width, theme, measureText) {
2419
+ const size = theme.fonts.sizes.small;
2420
+ const weight = theme.fonts.weights.normal;
2421
+ const maxWidth = Math.max(1, width - 2 * theme.spacing.padding);
2422
+ const measure = (text) => measureText?.(text, size, weight).width ?? estimateTextWidth2(text, size, weight);
2423
+ return notes.map((note) => {
2424
+ const text = `${note.index}. ${note.text}`;
2425
+ const lines = wrapText(text, size, weight, maxWidth, measureText).flatMap((line4) => {
2426
+ const chunks = [];
2427
+ let chunk = "";
2428
+ for (const character of line4) {
2429
+ if (chunk && measure(chunk + character) > maxWidth) {
2430
+ chunks.push(chunk);
2431
+ chunk = character;
2432
+ } else chunk += character;
2433
+ }
2434
+ chunks.push(chunk);
2435
+ return chunks;
2436
+ });
2437
+ return lines.length > 1 ? { ...note, lines } : note;
2438
+ });
2439
+ }
2440
+ function annotationNoteLineCount(notes) {
2441
+ return notes.reduce((count, note) => count + (note.lines?.length ?? 1), 0);
2442
+ }
2443
+ function annotationNoteCapacity(annotations, width, theme, measureText, includeSingle = false) {
2444
+ if (annotations.length <= 1 && !includeSingle) return 0;
2445
+ const texts = annotations.flatMap(
2446
+ (annotation, specIndex) => annotation.type === "text" ? [
2447
+ {
2448
+ specIndex,
2449
+ text: annotation.subtitle ? `${annotation.text}. ${annotation.subtitle}` : annotation.text
2450
+ }
2451
+ ] : []
2452
+ );
2453
+ const size = theme.fonts.sizes.small;
2454
+ const weight = theme.fonts.weights.normal;
2455
+ const measure = (text) => measureText?.(text, size, weight).width ?? estimateTextWidth2(text, size, weight);
2456
+ let widestIndex = 1;
2457
+ for (let index = 2; index <= texts.length; index++) {
2458
+ if (measure(`${index}. `) > measure(`${widestIndex}. `)) widestIndex = index;
2459
+ }
2460
+ return annotationNoteLineCount(
2461
+ wrapAnnotationNotes(
2462
+ texts.map((note) => ({ ...note, index: widestIndex })),
2463
+ width,
2464
+ theme,
2465
+ measureText
2466
+ )
2467
+ );
2468
+ }
2469
+
1881
2470
  // src/annotations/thinning.ts
1882
2471
  import { detectCollision as detectCollision2 } from "@opendata-ai/openchart-core";
1883
2472
  var COVERAGE_BUDGET = 0.1;
1884
2473
  var COLLISION_PADDING = 4;
1885
- function thinAnnotations(annotations, specs, measure, plotArea, containerBounds) {
1886
- if (annotations.length <= 1) {
2474
+ function thinAnnotations(annotations, specs, measure, plotArea, containerBounds, compactLimit) {
2475
+ if (annotations.length <= 1 && compactLimit == null) {
1887
2476
  return { annotations, footnotes: [] };
1888
2477
  }
1889
2478
  const entries = annotations.map((a, i) => {
@@ -1924,9 +2513,10 @@ function thinAnnotations(annotations, specs, measure, plotArea, containerBounds)
1924
2513
  const coverageBudget = plotAreaSize * COVERAGE_BUDGET;
1925
2514
  const containment = containerBounds ?? plotArea;
1926
2515
  let usedArea = 0;
1927
- for (const entry of candidates) {
2516
+ for (const [rank, entry] of candidates.entries()) {
2517
+ const overLimit = compactLimit != null && rank >= compactLimit;
1928
2518
  const bounds = entry.resolved.bounds ?? estimateLabelBounds(entry.resolved.label, measure);
1929
- if (bounds.width <= 0 || bounds.height <= 0) {
2519
+ if (!overLimit && (bounds.width <= 0 || bounds.height <= 0)) {
1930
2520
  placedBounds.push(bounds);
1931
2521
  continue;
1932
2522
  }
@@ -1941,11 +2531,13 @@ function thinAnnotations(annotations, specs, measure, plotArea, containerBounds)
1941
2531
  );
1942
2532
  const candidateArea = bounds.width * bounds.height;
1943
2533
  const overBudget = plotAreaSize > 0 && usedArea + candidateArea > coverageBudget;
1944
- if (overlaps || !fitsWithin(bounds, containment) || overBudget) {
2534
+ if (overLimit || overlaps || !fitsWithin(bounds, containment) || overBudget) {
1945
2535
  const footnoteIndex = footnotes.length + 1;
1946
2536
  footnotes.push({
2537
+ ...entry.resolved.specIndex !== void 0 ? { specIndex: entry.resolved.specIndex } : {},
2538
+ ...entry.resolved.id !== void 0 ? { id: entry.resolved.id } : {},
1947
2539
  index: footnoteIndex,
1948
- text: entry.resolved.label.text
2540
+ text: entry.spec?.subtitle ? `${entry.resolved.label.text}. ${entry.spec.subtitle}` : entry.resolved.label.text
1949
2541
  });
1950
2542
  entry.resolved.footnoteIndex = footnoteIndex;
1951
2543
  } else {
@@ -1960,8 +2552,62 @@ function fitsWithin(bounds, plotArea) {
1960
2552
  return bounds.x >= plotArea.x && bounds.y >= plotArea.y && bounds.x + bounds.width <= plotArea.x + plotArea.width && bounds.y + bounds.height <= plotArea.y + plotArea.height;
1961
2553
  }
1962
2554
 
2555
+ // src/compiler/mark-options.ts
2556
+ var APPLIES_TO = {
2557
+ point: ["line", "area", "density"],
2558
+ interpolate: ["line", "area", "density"],
2559
+ orient: ["bar", "histogram"],
2560
+ cornerRadius: ["bar", "rect", "histogram", "waffle"],
2561
+ innerRadius: ["arc"],
2562
+ outerRadius: ["arc"],
2563
+ startAngle: ["arc"],
2564
+ endAngle: ["arc"],
2565
+ centerLabel: ["arc"],
2566
+ binCount: ["histogram"],
2567
+ normalize: ["histogram"],
2568
+ bandwidth: ["density"],
2569
+ cumulative: ["density"],
2570
+ steps: ["density"],
2571
+ trendline: ["point"],
2572
+ dx: ["text"],
2573
+ dy: ["text"],
2574
+ align: ["text"],
2575
+ baseline: ["text"],
2576
+ fontSize: ["text"],
2577
+ style: ["range"],
2578
+ colorByDirection: ["range"],
2579
+ units: ["waffle"],
2580
+ columns: ["waffle"],
2581
+ other: ["waffle"],
2582
+ weekStart: ["calendar"],
2583
+ cellRadius: ["calendar"],
2584
+ shape: ["parliament"],
2585
+ seatRadius: ["parliament"],
2586
+ majorityLine: ["parliament"]
2587
+ };
2588
+ function collectMarkOptionWarnings(input, path = "") {
2589
+ if (!input || typeof input !== "object" || Array.isArray(input)) return [];
2590
+ const spec = input;
2591
+ const warnings = [];
2592
+ if (spec.mark && typeof spec.mark === "object" && !Array.isArray(spec.mark)) {
2593
+ const mark = spec.mark;
2594
+ for (const [key, allowed] of Object.entries(APPLIES_TO)) {
2595
+ if (mark[key] !== void 0 && !allowed.includes(mark.type)) {
2596
+ warnings.push(
2597
+ `[openchart] ${path}/mark/${key} has no effect on mark "${mark.type}". Remove ${key}, or use a supported mark: ${allowed.join(", ")}.`
2598
+ );
2599
+ }
2600
+ }
2601
+ }
2602
+ if (Array.isArray(spec.layer))
2603
+ spec.layer.forEach((child, i) => {
2604
+ warnings.push(...collectMarkOptionWarnings(child, `${path}/layer/${i}`));
2605
+ });
2606
+ return warnings;
2607
+ }
2608
+
1963
2609
  // src/endpoint-labels/compute.ts
1964
- import { estimateTextWidth as estimateTextWidth2, wrapText } from "@opendata-ai/openchart-core";
2610
+ import { estimateTextWidth as estimateTextWidth3, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
1965
2611
 
1966
2612
  // src/format/field-format.ts
1967
2613
  import {
@@ -2292,7 +2938,7 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpoint
2292
2938
  const formatted = formatEndpointValue(rawValue, endpointFormatter);
2293
2939
  const labelText = trailingContent ? composeEndpointText(trailingContent, seriesKey, formatted) : seriesKey;
2294
2940
  const value = trailingContent ? "" : formatted;
2295
- const labelLines = wrapText(
2941
+ const labelLines = wrapText2(
2296
2942
  labelText,
2297
2943
  ENDPOINT_LABEL_FONT_SIZE,
2298
2944
  ENDPOINT_LABEL_FONT_WEIGHT,
@@ -2300,10 +2946,10 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpoint
2300
2946
  );
2301
2947
  let entryWidth = 0;
2302
2948
  for (const line4 of labelLines) {
2303
- const w = estimateTextWidth2(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
2949
+ const w = estimateTextWidth3(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
2304
2950
  if (w > entryWidth) entryWidth = w;
2305
2951
  }
2306
- const valueWidth = value ? estimateTextWidth2(value, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT) : 0;
2952
+ const valueWidth = value ? estimateTextWidth3(value, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT) : 0;
2307
2953
  if (valueWidth > entryWidth) entryWidth = valueWidth;
2308
2954
  const labelHeight = labelLines.length * labelLineHeight;
2309
2955
  const valueHeight = value ? valueLineHeight : 0;
@@ -2375,7 +3021,7 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpoint
2375
3021
  const formatted = formatEndpointValue(rawValue, endpointFormatter);
2376
3022
  const labelText = leadingContent ? composeEndpointText(leadingContent, seriesKey, formatted) : seriesKey;
2377
3023
  const value = leadingContent ? "" : formatted;
2378
- const labelLines = wrapText(
3024
+ const labelLines = wrapText2(
2379
3025
  labelText,
2380
3026
  ENDPOINT_LABEL_FONT_SIZE,
2381
3027
  ENDPOINT_LABEL_FONT_WEIGHT,
@@ -2383,10 +3029,10 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpoint
2383
3029
  );
2384
3030
  let entryWidth = 0;
2385
3031
  for (const line4 of labelLines) {
2386
- const w = estimateTextWidth2(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
3032
+ const w = estimateTextWidth3(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
2387
3033
  if (w > entryWidth) entryWidth = w;
2388
3034
  }
2389
- const valueWidth = value ? estimateTextWidth2(value, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT) : 0;
3035
+ const valueWidth = value ? estimateTextWidth3(value, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT) : 0;
2390
3036
  if (valueWidth > entryWidth) entryWidth = valueWidth;
2391
3037
  const labelHeight = labelLines.length * labelLineHeight;
2392
3038
  const valueHeight = value ? valueLineHeight : 0;
@@ -3035,7 +3681,7 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
3035
3681
 
3036
3682
  // src/charts/bar/labels.ts
3037
3683
  import {
3038
- estimateTextWidth as estimateTextWidth3,
3684
+ estimateTextWidth as estimateTextWidth4,
3039
3685
  getRepresentativeColor as getRepresentativeColor2,
3040
3686
  pickLabelColor,
3041
3687
  resolveCollisions
@@ -3106,7 +3752,7 @@ function computeBarLabels(marks, chartArea, density = "auto", labelFormatter, la
3106
3752
  }
3107
3753
  if (labelPrefix) valuePart = labelPrefix + valuePart;
3108
3754
  if (labelSuffix) valuePart = valuePart + labelSuffix;
3109
- const textWidth = estimateTextWidth3(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
3755
+ const textWidth = estimateTextWidth4(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
3110
3756
  const textHeight = FONT_SIZE * 1.2;
3111
3757
  const isStacked2 = mark.stackGroup !== void 0;
3112
3758
  const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
@@ -4109,7 +4755,7 @@ function applyMarkDefOverrides2(marks, spec, bandwidth) {
4109
4755
 
4110
4756
  // src/charts/column/labels.ts
4111
4757
  import {
4112
- estimateTextWidth as estimateTextWidth4,
4758
+ estimateTextWidth as estimateTextWidth5,
4113
4759
  getRepresentativeColor as getRepresentativeColor3,
4114
4760
  pickLabelColor as pickLabelColor2,
4115
4761
  resolveCollisions as resolveCollisions2,
@@ -4150,7 +4796,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormatter
4150
4796
  const isNegative = Number.isFinite(numericValue) && numericValue < 0;
4151
4797
  if (labelPrefix) valuePart = labelPrefix + valuePart;
4152
4798
  if (labelSuffix) valuePart = valuePart + labelSuffix;
4153
- const textWidth = estimateTextWidth4(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT2);
4799
+ const textWidth = estimateTextWidth5(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT2);
4154
4800
  const textHeight = FONT_SIZE * 1.2;
4155
4801
  const isStacked2 = mark.stackGroup !== void 0;
4156
4802
  const anchorX = mark.x + mark.width / 2;
@@ -4424,7 +5070,7 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
4424
5070
 
4425
5071
  // src/charts/dot/labels.ts
4426
5072
  import {
4427
- estimateTextWidth as estimateTextWidth5,
5073
+ estimateTextWidth as estimateTextWidth6,
4428
5074
  getRepresentativeColor as getRepresentativeColor4,
4429
5075
  resolveCollisions as resolveCollisions3
4430
5076
  } from "@opendata-ai/openchart-core";
@@ -4454,7 +5100,7 @@ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix, labe
4454
5100
  }
4455
5101
  }
4456
5102
  if (labelPrefix) valuePart = labelPrefix + valuePart;
4457
- const textWidth = estimateTextWidth5(valuePart, LABEL_FONT_SIZE3, LABEL_FONT_WEIGHT3);
5103
+ const textWidth = estimateTextWidth6(valuePart, LABEL_FONT_SIZE3, LABEL_FONT_WEIGHT3);
4458
5104
  const textHeight = LABEL_FONT_SIZE3 * 1.2;
4459
5105
  candidates.push({
4460
5106
  text: valuePart,
@@ -5046,7 +5692,7 @@ function computeAreaMarks(spec, scales, chartArea, theme) {
5046
5692
  // src/charts/line/labels.ts
5047
5693
  import {
5048
5694
  EXTENDED_OFFSET_STRATEGIES,
5049
- estimateTextWidth as estimateTextWidth6,
5695
+ estimateTextWidth as estimateTextWidth7,
5050
5696
  resolveCollisions as resolveCollisions4
5051
5697
  } from "@opendata-ai/openchart-core";
5052
5698
  var LABEL_FONT_SIZE4 = 11;
@@ -5076,7 +5722,7 @@ function computeLineLabels(marks, strategy, density = "auto", labelOffsets, spec
5076
5722
  const labelText = mark.seriesKey ?? "";
5077
5723
  if (!labelText) continue;
5078
5724
  const lastPoint = mark.points[mark.points.length - 1];
5079
- const textWidth = estimateTextWidth6(labelText, LABEL_FONT_SIZE4, LABEL_FONT_WEIGHT4);
5725
+ const textWidth = estimateTextWidth7(labelText, LABEL_FONT_SIZE4, LABEL_FONT_WEIGHT4);
5080
5726
  const textHeight = LABEL_FONT_SIZE4 * 1.2;
5081
5727
  candidates.push({
5082
5728
  text: labelText,
@@ -5543,7 +6189,7 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false, th
5543
6189
  }
5544
6190
 
5545
6191
  // src/charts/pie/labels.ts
5546
- import { estimateTextWidth as estimateTextWidth7, resolveCollisions as resolveCollisions5 } from "@opendata-ai/openchart-core";
6192
+ import { estimateTextWidth as estimateTextWidth8, resolveCollisions as resolveCollisions5 } from "@opendata-ai/openchart-core";
5547
6193
  var LABEL_FONT_SIZE5 = 11;
5548
6194
  var LABEL_FONT_WEIGHT5 = 500;
5549
6195
  var LEADER_LINE_OFFSET = 12;
@@ -5571,7 +6217,7 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
5571
6217
  const rawValue = Number(rawValueText);
5572
6218
  const valueText = formatValue3 && Number.isFinite(rawValue) ? formatValue3(rawValue) : percentText;
5573
6219
  const labelText = valueText ? `${sliceName} ${valueText}` : sliceName;
5574
- const textWidth = estimateTextWidth7(labelText, LABEL_FONT_SIZE5, LABEL_FONT_WEIGHT5);
6220
+ const textWidth = estimateTextWidth8(labelText, LABEL_FONT_SIZE5, LABEL_FONT_WEIGHT5);
5575
6221
  const textHeight = LABEL_FONT_SIZE5 * 1.2;
5576
6222
  const midAngle = (mark.startAngle + mark.endAngle) / 2;
5577
6223
  const labelRadius = mark.outerRadius + LEADER_LINE_OFFSET;
@@ -5896,7 +6542,7 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
5896
6542
 
5897
6543
  // src/charts/range/labels.ts
5898
6544
  import {
5899
- estimateTextWidth as estimateTextWidth8,
6545
+ estimateTextWidth as estimateTextWidth9,
5900
6546
  getRepresentativeColor as getRepresentativeColor9,
5901
6547
  resolveCollisions as resolveCollisions6
5902
6548
  } from "@opendata-ai/openchart-core";
@@ -5910,7 +6556,7 @@ function formatEndpoint(value, formatter, prefix) {
5910
6556
  return prefix ? prefix + text : text;
5911
6557
  }
5912
6558
  function buildCandidate(mark, text, horizontal, outerSide, index) {
5913
- const textWidth = estimateTextWidth8(text, LABEL_FONT_SIZE6, LABEL_FONT_WEIGHT6);
6559
+ const textWidth = estimateTextWidth9(text, LABEL_FONT_SIZE6, LABEL_FONT_WEIGHT6);
5914
6560
  const textHeight = LABEL_FONT_SIZE6 * 1.2;
5915
6561
  let anchorX;
5916
6562
  let anchorY;
@@ -6628,10 +7274,11 @@ function registerBuiltinRenderers() {
6628
7274
  registerBuiltinRenderers();
6629
7275
 
6630
7276
  // src/barlist/compile-barlist.ts
7277
+ import { mergeThemeConfig } from "@opendata-ai/openchart-core";
6631
7278
  import {
6632
7279
  adaptTheme,
6633
7280
  computeChrome,
6634
- estimateTextWidth as estimateTextWidth9,
7281
+ estimateTextWidth as estimateTextWidth10,
6635
7282
  resolveTheme
6636
7283
  } from "@opendata-ai/openchart-core";
6637
7284
 
@@ -7299,34 +7946,33 @@ function expandLayerSugar(spec, warnings, inheritedData, parentEncoding) {
7299
7946
  }
7300
7947
  function expandValueFormatSugar(spec, warnings) {
7301
7948
  const type = spec.type;
7302
- if (type !== "sankey" && type !== "tilemap" && type !== "barlist") return spec;
7949
+ if (type !== "sankey" && type !== "tilemap" && type !== "barlist" && type !== "map") return spec;
7303
7950
  if (typeof spec.valueFormat !== "string") return spec;
7304
7951
  const encoding = spec.encoding;
7305
- const valueCh = encoding?.value;
7952
+ const channelKey = type === "map" ? "color" : "value";
7953
+ const valueCh = encoding?.[channelKey];
7306
7954
  if (!valueCh || typeof valueCh !== "object") {
7307
- warnings.push(
7308
- "[openchart] valueFormat is deprecated; set format on the encoding value channel instead (e.g. encoding.value.format)."
7309
- );
7955
+ if (type === "tilemap") return spec;
7956
+ warnings.push(`[openchart] valueFormat is deprecated; use encoding.${channelKey}.format.`);
7310
7957
  return spec;
7311
7958
  }
7312
7959
  if (valueCh.format !== void 0) {
7313
7960
  const { valueFormat: _vf, ...rest2 } = spec;
7314
7961
  warnings.push(
7315
- "[openchart] valueFormat is deprecated; the encoding.value.format field takes precedence and valueFormat was ignored."
7962
+ `[openchart] valueFormat is deprecated; encoding.${channelKey}.format takes precedence and valueFormat was ignored.`
7316
7963
  );
7317
7964
  return rest2;
7318
7965
  }
7319
- warnings.push(
7320
- "[openchart] valueFormat is deprecated; set format on the encoding value channel instead (e.g. encoding.value.format)."
7321
- );
7966
+ warnings.push(`[openchart] valueFormat is deprecated; use encoding.${channelKey}.format.`);
7322
7967
  const { valueFormat, ...rest } = spec;
7323
7968
  return {
7324
7969
  ...rest,
7325
- encoding: { ...encoding, value: { ...valueCh, format: valueFormat } }
7970
+ encoding: { ...encoding, [channelKey]: { ...valueCh, format: valueFormat } }
7326
7971
  };
7327
7972
  }
7328
- function expandSpecSugar(spec, warnings = []) {
7973
+ function expandSpecSugar(spec, warnings = [], markOptionWarnings = true) {
7329
7974
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) return spec;
7975
+ if (markOptionWarnings) warnings.push(...collectMarkOptionWarnings(spec));
7330
7976
  if (Array.isArray(spec.layer) && typeof spec.type !== "string") {
7331
7977
  return expandLayerSugar(spec, warnings);
7332
7978
  }
@@ -7335,16 +7981,35 @@ function expandSpecSugar(spec, warnings = []) {
7335
7981
  }
7336
7982
  return expandChartSugar(spec, warnings);
7337
7983
  }
7984
+ var emittedDeprecations = /* @__PURE__ */ new WeakMap();
7338
7985
  var defaultWarnSink = (message) => console.warn(message);
7339
7986
  function emitSpecWarnings(warnings, sink = defaultWarnSink) {
7340
7987
  if (warnings.length === 0) return;
7341
7988
  const seen = /* @__PURE__ */ new Set();
7989
+ const warningSink = sink === defaultWarnSink ? console.warn : sink;
7990
+ let deprecated = emittedDeprecations.get(warningSink);
7991
+ if (!deprecated) {
7992
+ deprecated = /* @__PURE__ */ new Set();
7993
+ emittedDeprecations.set(warningSink, deprecated);
7994
+ }
7342
7995
  for (const warning of warnings) {
7996
+ if (/deprecated|has no effect|removed in v8/i.test(warning)) {
7997
+ if (deprecated.has(warning)) continue;
7998
+ deprecated.add(warning);
7999
+ }
7343
8000
  if (seen.has(warning)) continue;
7344
8001
  seen.add(warning);
7345
8002
  sink(warning);
7346
8003
  }
7347
8004
  }
8005
+ function emitThemeWarnings(theme, sink) {
8006
+ if (theme?.chrome && Object.values(theme.chrome).some((value) => typeof value === "string")) {
8007
+ emitSpecWarnings(
8008
+ ["[openchart] theme.chrome string colors are deprecated; use { color } objects."],
8009
+ sink
8010
+ );
8011
+ }
8012
+ }
7348
8013
 
7349
8014
  // src/compiler/animation.ts
7350
8015
  import { FLUID_SPRING, springDuration } from "@opendata-ai/openchart-core";
@@ -7661,27 +8326,30 @@ function inferEncodingTypes(encoding, data, warnings) {
7661
8326
  }
7662
8327
  return result;
7663
8328
  }
7664
- function normalizeAnnotations(annotations) {
8329
+ function normalizeAnnotations(annotations, warnings) {
7665
8330
  if (!annotations || annotations.length === 0) return [];
7666
8331
  return annotations.map((ann) => {
7667
8332
  switch (ann.type) {
7668
8333
  case "text":
8334
+ if (ann.connector !== null && typeof ann.connector === "object" && ann.connector.type !== "curve" && ann.connector.bend !== void 0) {
8335
+ warnings.push("Annotation connector.bend is ignored unless connector.type is curve.");
8336
+ }
7669
8337
  return {
7670
8338
  ...ann,
7671
8339
  opacity: ann.opacity ?? 1
7672
8340
  };
8341
+ // Omitted ink resolves through theme tokens. Their 8.x defaults preserve
8342
+ // the colors historically stamped here; explicit authored ink stays intact.
7673
8343
  case "range":
7674
8344
  return {
7675
8345
  ...ann,
7676
- opacity: ann.opacity ?? 0.1,
7677
- fill: ann.fill ?? "#000000"
8346
+ opacity: ann.opacity ?? 0.1
7678
8347
  };
7679
8348
  case "refline":
7680
8349
  return {
7681
8350
  ...ann,
7682
8351
  style: ann.style ?? "dashed",
7683
8352
  strokeWidth: ann.strokeWidth ?? 1,
7684
- stroke: ann.stroke ?? "#666666",
7685
8353
  opacity: ann.opacity ?? 0.8
7686
8354
  };
7687
8355
  default:
@@ -7849,10 +8517,11 @@ function normalizeChartSpec(spec, warnings) {
7849
8517
  metrics: spec.metrics,
7850
8518
  seriesSearch: normalizeSeriesSearch(spec, encoding, warnings),
7851
8519
  youDrawIt: normalizeYouDrawIt(spec),
7852
- annotations: normalizeAnnotations(spec.annotations),
8520
+ annotations: normalizeAnnotations(spec.annotations, warnings),
7853
8521
  labels: normalizeLabels(spec.labels),
7854
8522
  legend: spec.legend,
7855
8523
  endpointLabels: spec.endpointLabels,
8524
+ compactAnnotationLimit: typeof spec.responsive === "object" ? spec.responsive.compactAnnotationLimit : void 0,
7856
8525
  responsive: typeof spec.responsive === "object" ? true : spec.responsive ?? true,
7857
8526
  autoThin: typeof spec.responsive === "object" ? spec.responsive.autoThin ?? true : spec.responsive ?? true,
7858
8527
  theme: spec.theme ?? {},
@@ -7879,16 +8548,15 @@ function normalizeChartSpec(spec, warnings) {
7879
8548
  }
7880
8549
  };
7881
8550
  }
7882
- function normalizeTableDensity(spec, warnings) {
8551
+ function normalizeTableDensity(spec) {
7883
8552
  if (spec.density) return spec.density;
7884
8553
  if (spec.compact) {
7885
- warnings.push('[openchart] table "compact" is deprecated; use density: "condensed" instead.');
7886
8554
  return "condensed";
7887
8555
  }
7888
8556
  return "regular";
7889
8557
  }
7890
- function normalizeTableSpec(spec, warnings) {
7891
- const density = normalizeTableDensity(spec, warnings);
8558
+ function normalizeTableSpec(spec, _warnings) {
8559
+ const density = normalizeTableDensity(spec);
7892
8560
  const totalRow = spec.totalRow === true ? { label: "Total" } : spec.totalRow && typeof spec.totalRow === "object" ? { label: spec.totalRow.label ?? "Total" } : void 0;
7893
8561
  return {
7894
8562
  type: "table",
@@ -7953,7 +8621,7 @@ function normalizeGraphSpec(spec, warnings) {
7953
8621
  nodeOverrides: spec.nodeOverrides,
7954
8622
  seedNode,
7955
8623
  chrome: normalizeChrome(spec.chrome),
7956
- annotations: normalizeAnnotations(spec.annotations),
8624
+ annotations: normalizeAnnotations(spec.annotations, warnings),
7957
8625
  theme: spec.theme ?? {},
7958
8626
  darkMode: spec.darkMode ?? "off",
7959
8627
  watermark: spec.watermark ?? true,
@@ -8057,6 +8725,32 @@ function normalizeBarListSpec(spec, _warnings) {
8057
8725
  };
8058
8726
  }
8059
8727
  function normalizeSpec(spec, warnings = []) {
8728
+ const authored = spec;
8729
+ if (authored.darkMode !== void 0)
8730
+ warnings.push(
8731
+ "[openchart] spec darkMode is deprecated and has no effect; use the mount darkMode option."
8732
+ );
8733
+ const themeChrome = authored.theme?.chrome;
8734
+ if (themeChrome && Object.values(themeChrome).some((value) => typeof value === "string"))
8735
+ warnings.push("[openchart] theme.chrome string colors are deprecated; use { color } objects.");
8736
+ if (authored.animation === "legacy" || authored.animation?.preset === "legacy")
8737
+ warnings.push("[openchart] animation legacy is deprecated; omit preset to retain v8 timing.");
8738
+ if (authored.labels?.offsets !== void 0)
8739
+ warnings.push(
8740
+ "[openchart] labels.offsets is deprecated; use endpointLabels. Migration drops manual offsets."
8741
+ );
8742
+ if (authored.type === "table" && "compact" in authored)
8743
+ warnings.push(
8744
+ '[openchart] table "compact" is deprecated; use density: "condensed" (true) or "regular" (false).'
8745
+ );
8746
+ const annotations = authored.annotations;
8747
+ if (Array.isArray(annotations)) {
8748
+ const ids = annotations.map((a) => a.id).filter((id) => id !== void 0);
8749
+ if (ids.length !== annotations.length || new Set(ids).size !== ids.length)
8750
+ warnings.push(
8751
+ "[openchart] Missing or duplicate annotation IDs use revision-local source indices; provide unique IDs for durable identity."
8752
+ );
8753
+ }
8060
8754
  if (isLayerSpec(spec)) {
8061
8755
  const leaves = flattenLayers(spec);
8062
8756
  if (leaves.length === 0) {
@@ -8991,8 +9685,8 @@ function validateGraphSpec(spec, errors) {
8991
9685
  const nodeIds = /* @__PURE__ */ new Set();
8992
9686
  const nodes = spec.nodes;
8993
9687
  for (let i = 0; i < nodes.length; i++) {
8994
- const node = nodes[i];
8995
- if (!node || typeof node !== "object") {
9688
+ const node2 = nodes[i];
9689
+ if (!node2 || typeof node2 !== "object") {
8996
9690
  errors.push({
8997
9691
  message: `Spec error: nodes[${i}] must be an object`,
8998
9692
  path: `nodes[${i}]`,
@@ -9001,7 +9695,7 @@ function validateGraphSpec(spec, errors) {
9001
9695
  });
9002
9696
  continue;
9003
9697
  }
9004
- if (typeof node.id !== "string" || node.id === "") {
9698
+ if (typeof node2.id !== "string" || node2.id === "") {
9005
9699
  errors.push({
9006
9700
  message: `Spec error: nodes[${i}] must have a non-empty string "id" field`,
9007
9701
  path: `nodes[${i}].id`,
@@ -9009,7 +9703,7 @@ function validateGraphSpec(spec, errors) {
9009
9703
  suggestion: 'Add a string id to the node, e.g. { id: "node1" }'
9010
9704
  });
9011
9705
  } else {
9012
- nodeIds.add(node.id);
9706
+ nodeIds.add(node2.id);
9013
9707
  }
9014
9708
  }
9015
9709
  if (spec.seedNode !== void 0) {
@@ -9096,9 +9790,9 @@ function validateGraphSpec(spec, errors) {
9096
9790
  if (spec.encoding && typeof spec.encoding === "object") {
9097
9791
  const encoding = spec.encoding;
9098
9792
  const nodeFields = /* @__PURE__ */ new Set();
9099
- for (const node of nodes) {
9100
- if (node && typeof node === "object") {
9101
- for (const key of Object.keys(node)) nodeFields.add(key);
9793
+ for (const node2 of nodes) {
9794
+ if (node2 && typeof node2 === "object") {
9795
+ for (const key of Object.keys(node2)) nodeFields.add(key);
9102
9796
  }
9103
9797
  }
9104
9798
  const edgeFields = /* @__PURE__ */ new Set();
@@ -9882,7 +10576,55 @@ function validateFieldMetadata(obj, errors) {
9882
10576
  }
9883
10577
  }
9884
10578
  }
9885
- function validateSpec(spec) {
10579
+ function validateAnnotationLayout(spec, errors, prefix = "") {
10580
+ const responsive = spec.responsive;
10581
+ const limit = responsive && typeof responsive === "object" ? responsive.compactAnnotationLimit : void 0;
10582
+ if (limit != null && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 0)) {
10583
+ errors.push({
10584
+ code: "INVALID_VALUE",
10585
+ path: `${prefix}responsive.compactAnnotationLimit`,
10586
+ message: "compactAnnotationLimit must be a nonnegative integer or null",
10587
+ suggestion: "Use null for unlimited inline annotations."
10588
+ });
10589
+ }
10590
+ if (Array.isArray(spec.annotations)) {
10591
+ for (const [index, annotation] of spec.annotations.entries()) {
10592
+ for (const field of ["maxWidth", "subtitleFontSize"]) {
10593
+ const value = annotation?.[field];
10594
+ if (value === void 0 || field === "maxWidth" && value === null) continue;
10595
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
10596
+ errors.push({
10597
+ code: "INVALID_VALUE",
10598
+ path: `${prefix}annotations[${index}].${field}`,
10599
+ message: `${field} must be a positive finite number`,
10600
+ suggestion: field === "maxWidth" ? "Use null for unlimited width." : "Omit subtitleFontSize to use the default ratio."
10601
+ });
10602
+ }
10603
+ if (annotation?.labelPlacement !== void 0 && !["auto", "start", "end", "outside"].includes(annotation.labelPlacement))
10604
+ errors.push({
10605
+ code: "INVALID_VALUE",
10606
+ path: `${prefix}annotations[${index}].labelPlacement`,
10607
+ message: "Unknown reference label placement",
10608
+ suggestion: "Use auto, start, end or outside."
10609
+ });
10610
+ const bend = annotation?.connector?.bend;
10611
+ if (bend !== void 0 && (typeof bend !== "number" || !Number.isFinite(bend) || bend < -1 || bend > 1)) {
10612
+ errors.push({
10613
+ code: "INVALID_VALUE",
10614
+ path: `${prefix}annotations[${index}].connector.bend`,
10615
+ message: "Connector bend must be a finite number between -1 and 1",
10616
+ suggestion: "Omit bend for the automatic curve, or use a number in [-1, 1]."
10617
+ });
10618
+ }
10619
+ }
10620
+ }
10621
+ if (Array.isArray(spec.layer))
10622
+ for (const [index, child] of spec.layer.entries()) {
10623
+ if (child && typeof child === "object" && !Array.isArray(child))
10624
+ validateAnnotationLayout(child, errors, `${prefix}layer[${index}].`);
10625
+ }
10626
+ }
10627
+ function validateSpecShape(spec) {
9886
10628
  const errors = [];
9887
10629
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
9888
10630
  return {
@@ -9898,6 +10640,7 @@ function validateSpec(spec) {
9898
10640
  };
9899
10641
  }
9900
10642
  const obj = spec;
10643
+ validateAnnotationLayout(obj, errors);
9901
10644
  validateFieldMetadata(obj, errors);
9902
10645
  if (obj.chrome !== void 0) {
9903
10646
  const chrome = obj.chrome;
@@ -10027,16 +10770,21 @@ function validateSpec(spec) {
10027
10770
  normalized: spec
10028
10771
  };
10029
10772
  }
10773
+ function validateSpec(spec) {
10774
+ const result = validateSpecShape(spec);
10775
+ const warnings = collectMarkOptionWarnings(spec);
10776
+ return warnings.length ? { ...result, warnings } : result;
10777
+ }
10030
10778
 
10031
10779
  // src/compiler/index.ts
10032
- function compile(spec) {
10780
+ function compile(spec, options) {
10033
10781
  const validation = validateSpec(spec);
10034
10782
  if (!validation.valid || !validation.normalized) {
10035
10783
  const errorMessages = validation.errors.map((e) => e.message).join("\n");
10036
10784
  throw new Error(`Invalid spec:
10037
10785
  ${errorMessages}`);
10038
10786
  }
10039
- const warnings = [];
10787
+ const warnings = options?.markOptionWarnings === false ? [] : [...validation.warnings ?? []];
10040
10788
  const normalized = normalizeSpec(validation.normalized, warnings);
10041
10789
  return { spec: normalized, warnings };
10042
10790
  }
@@ -10068,9 +10816,6 @@ function bottomMargin(bottomHeight, padding, xAxisHeight) {
10068
10816
  }
10069
10817
  var INLINE_TICK_OVERHANG_PAD = 6;
10070
10818
  function resolveChromeLayout(spec, options) {
10071
- const encoding = spec?.encoding;
10072
- const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
10073
- if (isFaceted) return "subtract";
10074
10819
  return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
10075
10820
  }
10076
10821
 
@@ -10099,7 +10844,8 @@ function compileBarList(spec, options) {
10099
10844
  const barlistSpec = normalized;
10100
10845
  const rawWatermark = spec.watermark;
10101
10846
  const watermark = rawWatermark !== void 0 ? barlistSpec.watermark : options.watermark ?? true;
10102
- const mergedThemeConfig = options.theme ? { ...barlistSpec.theme, ...options.theme } : barlistSpec.theme;
10847
+ emitThemeWarnings(options.theme, options.onWarn);
10848
+ const mergedThemeConfig = mergeThemeConfig(barlistSpec.theme, options.theme);
10103
10849
  const lightTheme = resolveTheme(mergedThemeConfig);
10104
10850
  let theme = lightTheme;
10105
10851
  if (options.darkMode) {
@@ -10168,7 +10914,7 @@ function compileBarList(spec, options) {
10168
10914
  values: validRows.map((r) => r[valueField])
10169
10915
  });
10170
10916
  const measureText = options.measureText ?? ((text, fontSize) => ({
10171
- width: estimateTextWidth9(text, fontSize),
10917
+ width: estimateTextWidth10(text, fontSize),
10172
10918
  height: fontSize
10173
10919
  }));
10174
10920
  const perRowLabelWidths = /* @__PURE__ */ new Map();
@@ -10376,7 +11122,7 @@ function emptyLayout2(chrome, theme, options, watermark) {
10376
11122
  height: options.height,
10377
11123
  watermark,
10378
11124
  animation: void 0,
10379
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth9(text, fontSize), height: fontSize }))
11125
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth10(text, fontSize), height: fontSize }))
10380
11126
  };
10381
11127
  }
10382
11128
 
@@ -10540,7 +11286,7 @@ import {
10540
11286
  import {
10541
11287
  computeFieldFormatContext as computeFieldFormatContext2,
10542
11288
  defaultNumberFormatter as defaultNumberFormatter5,
10543
- estimateTextWidth as estimateTextWidth10,
11289
+ estimateTextWidth as estimateTextWidth11,
10544
11290
  resolveNumberFormatter as resolveNumberFormatter2,
10545
11291
  resolveSchemeName as resolveSchemeName3
10546
11292
  } from "@opendata-ai/openchart-core";
@@ -10733,7 +11479,7 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
10733
11479
  const rowEnds = [];
10734
11480
  const rowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
10735
11481
  for (const tick of ticks2) {
10736
- const width = estimateTextWidth10(
11482
+ const width = estimateTextWidth11(
10737
11483
  tick.label,
10738
11484
  theme.fonts.sizes.small,
10739
11485
  theme.fonts.weights.normal
@@ -10988,8 +11734,12 @@ import {
10988
11734
  axisTitleOffset as axisTitleOffset3,
10989
11735
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
10990
11736
  computeFieldFormatContext as computeFieldFormatContext5,
11737
+ computeNoteBandHeight,
10991
11738
  defaultNumberFormatter as defaultNumberFormatter7,
10992
- estimateTextWidth as estimateTextWidth18,
11739
+ estimateTextWidth as estimateTextWidth19,
11740
+ getBreakpoint,
11741
+ getHeightClass as getHeightClass2,
11742
+ getLayoutStrategy,
10993
11743
  resolveNumberFormatter as resolveNumberFormatter5,
10994
11744
  resolveTheme as resolveTheme2,
10995
11745
  TICK_LABEL_OFFSET
@@ -11007,7 +11757,7 @@ import {
11007
11757
  computeFieldFormatContext as computeFieldFormatContext4,
11008
11758
  computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
11009
11759
  defaultNumberFormatter as defaultNumberFormatter6,
11010
- estimateTextWidth as estimateTextWidth17,
11760
+ estimateTextWidth as estimateTextWidth18,
11011
11761
  getHeightClass,
11012
11762
  HPAD_COMPACT_FRACTION,
11013
11763
  HPAD_COMPACT_MIN,
@@ -11024,7 +11774,7 @@ import {
11024
11774
  } from "@opendata-ai/openchart-core";
11025
11775
 
11026
11776
  // src/endpoint-labels/predict.ts
11027
- import { estimateTextWidth as estimateTextWidth11, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
11777
+ import { estimateTextWidth as estimateTextWidth12, wrapText as wrapText3 } from "@opendata-ai/openchart-core";
11028
11778
  function predictEndpointLabelsWidth(spec, _theme) {
11029
11779
  if (spec.endpointLabels === false) return 0;
11030
11780
  if (spec.markType !== "line" && spec.markType !== "area") return 0;
@@ -11067,14 +11817,14 @@ function predictEndpointLabelsWidth(spec, _theme) {
11067
11817
  let maxWidth = 0;
11068
11818
  for (const name of seriesNames) {
11069
11819
  const text = content ? composeEndpointText(content, name, sample) : name;
11070
- const lines = wrapText2(text, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
11820
+ const lines = wrapText3(text, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
11071
11821
  for (const line4 of lines) {
11072
- const w = estimateTextWidth11(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
11822
+ const w = estimateTextWidth12(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
11073
11823
  if (w > maxWidth) maxWidth = w;
11074
11824
  }
11075
11825
  }
11076
11826
  if (!content && sample) {
11077
- const w = estimateTextWidth11(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
11827
+ const w = estimateTextWidth12(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
11078
11828
  if (w > maxWidth) maxWidth = w;
11079
11829
  }
11080
11830
  return maxWidth;
@@ -11091,7 +11841,7 @@ function isEndsBoth(spec) {
11091
11841
  }
11092
11842
 
11093
11843
  // src/legend/compute.ts
11094
- import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth13 } from "@opendata-ai/openchart-core";
11844
+ import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth14 } from "@opendata-ai/openchart-core";
11095
11845
 
11096
11846
  // src/layout/scales.ts
11097
11847
  import { computeFieldFormatContext as computeFieldFormatContext3 } from "@opendata-ai/openchart-core";
@@ -11679,7 +12429,7 @@ function computeScales(spec, chartArea, data) {
11679
12429
  }
11680
12430
 
11681
12431
  // src/legend/wrap.ts
11682
- import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth12 } from "@opendata-ai/openchart-core";
12432
+ import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth13 } from "@opendata-ai/openchart-core";
11683
12433
  var SWATCH_SIZE = 12;
11684
12434
  var SWATCH_GAP = 6;
11685
12435
  var ENTRY_GAP = 16;
@@ -11693,7 +12443,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
11693
12443
  if (entries.length === 0) {
11694
12444
  return { rowCount: 0, fittingCount: 0, rowWidths: [], placements: [] };
11695
12445
  }
11696
- const measureWidth = measure ?? estimateTextWidth12;
12446
+ const measureWidth = measure ?? estimateTextWidth13;
11697
12447
  let rowCount = 1;
11698
12448
  let rowWidth = 0;
11699
12449
  const rowWidths = [];
@@ -11839,7 +12589,7 @@ function emptyContent(position, theme) {
11839
12589
  };
11840
12590
  }
11841
12591
  function computeLegendContent(spec, strategy, theme, availableWidth, availableHeight, watermark = true, measure, endpointLabelsDemoted) {
11842
- const measureWidth = measure ?? estimateTextWidth13;
12592
+ const measureWidth = measure ?? estimateTextWidth14;
11843
12593
  const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
11844
12594
  if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
11845
12595
  return emptyContent("top", theme);
@@ -11851,7 +12601,7 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11851
12601
  if (!continuous) {
11852
12602
  return emptyContent(continuousPosition, theme);
11853
12603
  }
11854
- const labelStyle2 = {
12604
+ const labelStyle = {
11855
12605
  fontFamily: theme.fonts.family,
11856
12606
  fontSize: theme.fonts.sizes.small,
11857
12607
  fontWeight: theme.fonts.weights.normal,
@@ -11859,13 +12609,13 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11859
12609
  lineHeight: 1.3,
11860
12610
  fontVariant: "tabular-nums"
11861
12611
  };
11862
- const labelRowHeight = Math.ceil(labelStyle2.fontSize * labelStyle2.lineHeight);
12612
+ const labelRowHeight = Math.ceil(labelStyle.fontSize * labelStyle.lineHeight);
11863
12613
  const height = compact ? Math.max(continuous.barHeight, labelRowHeight) : continuous.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight * (continuous.labelRows ?? 1);
11864
12614
  const blockWidth = continuous.blockWidth ?? continuous.barWidth;
11865
12615
  return {
11866
12616
  entries: [],
11867
12617
  position: continuousPosition,
11868
- labelStyle: labelStyle2,
12618
+ labelStyle,
11869
12619
  rowCount: 1,
11870
12620
  totalWidth: blockWidth,
11871
12621
  height,
@@ -11903,6 +12653,18 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11903
12653
  if (spec.markType === "arc" && everySliceLabeled && uncrowded && !spec.userExplicit?.legend) {
11904
12654
  entries = [];
11905
12655
  }
12656
+ return measureCategoricalLegendContent(
12657
+ entries,
12658
+ spec.legend,
12659
+ strategy,
12660
+ theme,
12661
+ availableWidth,
12662
+ availableHeight,
12663
+ watermark,
12664
+ measureWidth
12665
+ );
12666
+ }
12667
+ function measureCategoricalLegendContent(entries, legend, strategy, theme, availableWidth, availableHeight, watermark = true, measureWidth = estimateTextWidth14) {
11906
12668
  const labelStyle = {
11907
12669
  fontFamily: theme.fonts.family,
11908
12670
  fontSize: theme.fonts.sizes.small,
@@ -11913,12 +12675,12 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11913
12675
  // of entries from shimmying.
11914
12676
  fontVariant: "tabular-nums"
11915
12677
  };
11916
- const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
12678
+ const specPosition = legend?.position === "top-left" ? "top" : legend?.position;
11917
12679
  const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
11918
12680
  if (entries.length === 0) {
11919
12681
  return emptyContent(resolvedPosition, theme);
11920
12682
  }
11921
- const offset = spec.legend?.offset;
12683
+ const offset = legend?.offset;
11922
12684
  if (resolvedPosition === "right" || resolvedPosition === "bottom-right") {
11923
12685
  const maxLabelWidth = Math.max(
11924
12686
  ...entries.map((e) => measureWidth(e.label, labelStyle.fontSize, labelStyle.fontWeight))
@@ -11934,7 +12696,7 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11934
12696
  1,
11935
12697
  Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
11936
12698
  );
11937
- const maxEntries = spec.legend?.symbolLimit != null ? Math.max(1, spec.legend.symbolLimit) : maxFromSpace;
12699
+ const maxEntries = legend?.symbolLimit != null ? Math.max(1, legend.symbolLimit) : maxFromSpace;
11938
12700
  if (entries.length > maxEntries) {
11939
12701
  entries = truncateEntries(entries, maxEntries);
11940
12702
  }
@@ -11975,13 +12737,13 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
11975
12737
  const effectivePadding = isCompact ? 2 : LEGEND_PADDING;
11976
12738
  const effectiveEntryGap = isCompact ? ENTRY_GAP_COMPACT : ENTRY_GAP;
11977
12739
  const contentWidth = availableWidth - effectivePadding * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH : 0);
11978
- if (spec.legend?.symbolLimit != null) {
11979
- const limit = Math.max(1, spec.legend.symbolLimit);
12740
+ if (legend?.symbolLimit != null) {
12741
+ const limit = Math.max(1, legend.symbolLimit);
11980
12742
  if (limit < entries.length) {
11981
12743
  entries = truncateEntries(entries, limit);
11982
12744
  }
11983
12745
  }
11984
- const maxRows = spec.legend?.maxRows != null ? Math.max(1, spec.legend.maxRows) : spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
12746
+ const maxRows = legend?.maxRows != null ? Math.max(1, legend.maxRows) : legend?.columns != null ? Math.ceil(entries.length / legend.columns) : TOP_LEGEND_MAX_ROWS;
11985
12747
  const { fittingCount } = measureLegendWrap(
11986
12748
  entries,
11987
12749
  contentWidth,
@@ -12117,7 +12879,7 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, ax
12117
12879
  import {
12118
12880
  abbreviateNumber,
12119
12881
  buildD3Formatter,
12120
- estimateTextWidth as estimateTextWidth14,
12882
+ estimateTextWidth as estimateTextWidth15,
12121
12883
  formatNumber as formatNumber3
12122
12884
  } from "@opendata-ai/openchart-core";
12123
12885
  import { scaleLinear as scaleLinear3, scaleSqrt as scaleSqrt3 } from "d3-scale";
@@ -12233,11 +12995,11 @@ function computeSizeLegendContent(sizeEncoding, data, theme, options) {
12233
12995
  };
12234
12996
  });
12235
12997
  const widestLabel = Math.max(
12236
- ...circles.map((c) => estimateTextWidth14(c.label, fontSize, theme.fonts.weights.normal))
12998
+ ...circles.map((c) => estimateTextWidth15(c.label, fontSize, theme.fonts.weights.normal))
12237
12999
  );
12238
13000
  const lastLabelBottom = (circles[circles.length - 1]?.labelY ?? 0) + fontSize * 0.5;
12239
13001
  const circleBlockWidth = maxRadius * 2 + LABEL_GAP + widestLabel;
12240
- const titleWidth = title ? estimateTextWidth14(title, fontSize, theme.fonts.weights.medium) : 0;
13002
+ const titleWidth = title ? estimateTextWidth15(title, fontSize, theme.fonts.weights.medium) : 0;
12241
13003
  return {
12242
13004
  circles,
12243
13005
  width: Math.max(circleBlockWidth, titleWidth),
@@ -12281,7 +13043,7 @@ function placeSizeLegend(content, chartArea, theme, colorLegend) {
12281
13043
  import {
12282
13044
  axisTitleOffset,
12283
13045
  computeXAxisExtentFromLabels,
12284
- estimateTextWidth as estimateTextWidth16,
13046
+ estimateTextWidth as estimateTextWidth17,
12285
13047
  truncateRotatedLabel,
12286
13048
  X_AXIS_TITLE_BAND,
12287
13049
  X_AXIS_TITLE_BAND_ROTATED
@@ -12326,11 +13088,11 @@ function bandLabelStride(maxLabelWidth, angleDeg, fontSize, spacing) {
12326
13088
  }
12327
13089
 
12328
13090
  // src/layout/axes/thinning.ts
12329
- import { estimateTextWidth as estimateTextWidth15 } from "@opendata-ai/openchart-core";
13091
+ import { estimateTextWidth as estimateTextWidth16 } from "@opendata-ai/openchart-core";
12330
13092
  var MIN_TICK_GAP_FACTOR = 0.5;
12331
13093
  var MIN_TICK_COUNT = 2;
12332
13094
  function measureLabel(text, fontSize, fontWeight, measureText) {
12333
- return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth15(text, fontSize, fontWeight);
13095
+ return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth16(text, fontSize, fontWeight);
12334
13096
  }
12335
13097
  function ticksOverlap(ticks, fontSize, fontWeight, measureText, orientation = "horizontal") {
12336
13098
  if (ticks.length < 2) return false;
@@ -12985,7 +13747,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
12985
13747
  const isRight = result.y.orient === "right";
12986
13748
  if (isRight) {
12987
13749
  const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
12988
- const w = estimateTextWidth16(
13750
+ const w = estimateTextWidth17(
12989
13751
  t.label,
12990
13752
  result.y.tickLabelStyle.fontSize,
12991
13753
  result.y.tickLabelStyle.fontWeight ?? 400
@@ -13000,7 +13762,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
13000
13762
  };
13001
13763
  } else {
13002
13764
  const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
13003
- const w = estimateTextWidth16(
13765
+ const w = estimateTextWidth17(
13004
13766
  t.label,
13005
13767
  result.y.tickLabelStyle.fontSize,
13006
13768
  result.y.tickLabelStyle.fontWeight ?? 400
@@ -13028,7 +13790,7 @@ function createMeasureFn(measureText) {
13028
13790
  if (measureText) {
13029
13791
  return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
13030
13792
  }
13031
- return estimateTextWidth17;
13793
+ return estimateTextWidth18;
13032
13794
  }
13033
13795
  function sampleLabelFromContext(maxAbsVal, yAxisFormat, ctx) {
13034
13796
  if (yAxisFormat) {
@@ -13480,7 +14242,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
13480
14242
  const yEnc = encoding.y;
13481
14243
  const yField = yEnc.field;
13482
14244
  if (!yField) return 40;
13483
- const m = measure ?? estimateTextWidth18;
14245
+ const m = measure ?? estimateTextWidth19;
13484
14246
  const yType = yEnc.type;
13485
14247
  if (yType === "nominal" || yType === "ordinal") {
13486
14248
  let maxWidth = 0;
@@ -13563,10 +14325,77 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
13563
14325
  leaf1WithUnionX = withYDomain(leaf1WithUnionX, aligned.domain1);
13564
14326
  }
13565
14327
  const primary0 = buildPrimarySpec([leaf0WithUnionX], layerSpec);
13566
- const layout0 = compileChart2(primary0, optionsWithReserve);
14328
+ let layout0 = compileChart2(primary0, optionsWithReserve);
13567
14329
  const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
13568
14330
  primary1.annotations = [];
13569
- const layout1 = compileChart2(primary1, optionsWithReserve);
14331
+ let layout1 = compileChart2(primary1, optionsWithReserve);
14332
+ const columns = [layout0.endpointLabels, layout1.endpointLabels].filter(
14333
+ (column) => !!column
14334
+ );
14335
+ let leadingColumnX;
14336
+ if (columns.length) {
14337
+ const pad = layout0.theme.spacing.padding;
14338
+ const leadingWidth = Math.max(0, ...columns.map((column) => column.leadingBounds?.width ?? 0));
14339
+ const leftAxisReserve = Math.max(
14340
+ 0,
14341
+ layout0.area.x - pad - (layout0.endpointLabels?.leadingBounds ? layout0.endpointLabels.leadingBounds.width + 16 : 0)
14342
+ );
14343
+ const x = Math.max(
14344
+ layout0.area.x,
14345
+ layout1.area.x,
14346
+ leadingWidth ? pad + leadingWidth + ENDPOINT_COLUMN_GAP + leftAxisReserve : 0
14347
+ );
14348
+ leadingColumnX = leadingWidth ? pad : void 0;
14349
+ const y = Math.max(layout0.area.y, layout1.area.y);
14350
+ const endpointWidth = Math.max(...columns.map((column) => column.bounds.width));
14351
+ const rightLegends = [...layout0.legends, ...layout1.legends].filter(isRightLegend);
14352
+ const legendReserve = rightLegends.length ? Math.max(...rightLegends.map((legend) => legend.bounds.width)) + 8 : 0;
14353
+ const referenceReserve = referenceLabelReserve(
14354
+ primary0.annotations ?? [],
14355
+ options.width,
14356
+ options.measureText ? (text, font) => options.measureText(text, font.fontSize, font.fontWeight).width : heuristicMeasure
14357
+ );
14358
+ const right = options.width - pad - rightReserve - ENDPOINT_COLUMN_GAP - endpointWidth - legendReserve - referenceReserve.right;
14359
+ const grow = (primary0.chromeLayout ?? options.chromeLayout) === "grow";
14360
+ const bottom = grow ? layout0.area.y + layout0.area.height : Math.min(layout0.area.y + layout0.area.height, layout1.area.y + layout1.area.height);
14361
+ const area3 = {
14362
+ ...options.frozenChartArea ?? {
14363
+ x,
14364
+ y,
14365
+ width: Math.max(1, right - x),
14366
+ height: Math.max(1, bottom - y)
14367
+ }
14368
+ };
14369
+ let reserve = noteReserve(layout0);
14370
+ for (let pass = 0; pass <= (primary0.annotations?.length ?? 0) + 1; pass++) {
14371
+ const alignedOptions = {
14372
+ ...optionsWithReserve,
14373
+ frozenChartArea: area3,
14374
+ footnoteReserve: reserve
14375
+ };
14376
+ layout0 = compileChart2(primary0, alignedOptions);
14377
+ layout1 = compileChart2(primary1, alignedOptions);
14378
+ const nextReserve = noteReserve(layout0);
14379
+ if (nextReserve <= reserve) break;
14380
+ const delta = nextReserve - reserve;
14381
+ reserve = nextReserve;
14382
+ if (grow) continue;
14383
+ if (options.frozenChartArea) break;
14384
+ area3.height = Math.max(1, area3.height - delta);
14385
+ }
14386
+ const legendShift = rightReserve + ENDPOINT_COLUMN_GAP + endpointWidth;
14387
+ const localColumnReserve = (layout) => layout.endpointLabels?.entries.length ? ENDPOINT_COLUMN_GAP + layout.endpointLabels.bounds.width : 0;
14388
+ layout0 = moveRightLegends(layout0, legendShift - localColumnReserve(layout0));
14389
+ layout1 = moveRightLegends(layout1, legendShift - localColumnReserve(layout1));
14390
+ }
14391
+ if (!columns.length && noteReserve(layout0) > 0) {
14392
+ const reserve = noteReserve(layout0);
14393
+ layout1 = compileChart2(primary1, {
14394
+ ...optionsWithReserve,
14395
+ frozenChartArea: layout0.area,
14396
+ footnoteReserve: reserve
14397
+ });
14398
+ }
13570
14399
  const y2Axis = layout1.axes.y ? {
13571
14400
  ...layout1.axes.y,
13572
14401
  orient: "right",
@@ -13658,6 +14487,43 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
13658
14487
  }
13659
14488
  return tagged;
13660
14489
  });
14490
+ const endpointColumns = [layout0, layout1].flatMap((layout, index) => {
14491
+ const column = layout.endpointLabels;
14492
+ if (!column) return [];
14493
+ const sourceMarks = index === 0 ? adjustedMarks0 : taggedMarks1;
14494
+ const remap = (entry, leading = false) => {
14495
+ const line4 = sourceMarks.find(
14496
+ (mark) => mark.type === "line" && mark.seriesKey === entry.seriesKey
14497
+ );
14498
+ if (!entry.marker || line4?.type !== "line") return entry;
14499
+ const points = line4.dataPoints?.length ? line4.dataPoints : line4.points;
14500
+ const point = leading ? points[0] : points.at(-1);
14501
+ if (!point) return entry;
14502
+ return {
14503
+ ...entry,
14504
+ marker: {
14505
+ ...entry.marker,
14506
+ dataX: point.x,
14507
+ x: point.x + (leading ? -1 : 1) * entry.marker.radius
14508
+ }
14509
+ };
14510
+ };
14511
+ const endpointConfig = (index === 0 ? primary0 : primary1).endpointLabels;
14512
+ return [
14513
+ {
14514
+ ...column,
14515
+ leadersEnabled: typeof endpointConfig === "object" && endpointConfig.showLeader === true,
14516
+ entries: column.entries.map((entry) => remap(entry)),
14517
+ leading: column.leading?.map((entry) => remap(entry, true))
14518
+ }
14519
+ ];
14520
+ });
14521
+ const endpointLabels = mergeEndpointColumns(
14522
+ endpointColumns,
14523
+ layout0.area,
14524
+ rightReserve,
14525
+ leadingColumnX
14526
+ );
13661
14527
  const seenLabels = /* @__PURE__ */ new Set();
13662
14528
  const l0Legend = layout0.legend;
13663
14529
  const l1Legend = layout1.legend;
@@ -13681,12 +14547,43 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
13681
14547
  for (const layer of layers) {
13682
14548
  appendLayerMarks(layer.layout, layer.leafIndex, marks, mergedTooltips, layer.sourceMarks);
13683
14549
  }
13684
- const mergedLegend = {
14550
+ let mergedLegend = {
13685
14551
  ...layout0.legend,
13686
14552
  ..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
13687
14553
  };
14554
+ if (endpointLabels) {
14555
+ const owner = l0Legend.bounds.width > 0 ? layout0 : layout1;
14556
+ if (isRightLegend(owner.legend) && "entries" in owner.legend) {
14557
+ const ownerSpec = owner === layout0 ? primary0 : primary1;
14558
+ const normalized = normalizeSpec(
14559
+ expandSpecSugar(ownerSpec)
14560
+ );
14561
+ const content = measureCategoricalLegendContent(
14562
+ mergedLegendEntries,
14563
+ normalized.legend,
14564
+ getLayoutStrategy(getBreakpoint(options.width), getHeightClass2(options.height)),
14565
+ owner.theme,
14566
+ options.width,
14567
+ layout0.area.height,
14568
+ owner.watermark,
14569
+ measureFn
14570
+ );
14571
+ mergedLegend = placeLegend(
14572
+ content,
14573
+ {
14574
+ ...layout0.area,
14575
+ width: layout0.area.width + rightReserve + ENDPOINT_COLUMN_GAP + endpointLabels.bounds.width
14576
+ },
14577
+ options.width,
14578
+ owner.theme,
14579
+ 0
14580
+ // Right legends do not depend on the x-axis extent.
14581
+ );
14582
+ }
14583
+ }
13688
14584
  return {
13689
14585
  ...layout0,
14586
+ ...endpointLabels ? { endpointLabels } : {},
13690
14587
  axes: {
13691
14588
  x: layout0.axes.x,
13692
14589
  y: layout0.axes.y,
@@ -13700,6 +14597,92 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
13700
14597
  tooltipDescriptors: mergedTooltips
13701
14598
  };
13702
14599
  }
14600
+ function noteReserve(layout) {
14601
+ return layout.chrome.noteBandHeight ?? computeNoteBandHeight(
14602
+ annotationNoteLineCount(layout.chrome.footnotes ?? []) + (layout.chrome.seriesSummary?.reduce((count, entry) => count + entry.lines.length, 0) ?? 0),
14603
+ layout.theme
14604
+ );
14605
+ }
14606
+ function isRightLegend(legend) {
14607
+ return (legend.position === "right" || legend.position === "bottom-right") && legend.bounds.width > 0;
14608
+ }
14609
+ function moveRightLegends(layout, dx) {
14610
+ const move = (legend) => {
14611
+ if (!isRightLegend(legend)) return legend;
14612
+ const shifted = { ...legend, bounds: { ...legend.bounds, x: legend.bounds.x + dx } };
14613
+ if ("entryPositions" in legend)
14614
+ return {
14615
+ ...shifted,
14616
+ entryPositions: legend.entryPositions?.map((position) => ({
14617
+ ...position,
14618
+ x: position.x + dx,
14619
+ labelX: position.labelX + dx
14620
+ }))
14621
+ };
14622
+ if (legend.type === "continuous")
14623
+ return {
14624
+ ...shifted,
14625
+ bar: { ...legend.bar, x: legend.bar.x + dx },
14626
+ bins: legend.bins.map((bin) => ({ ...bin, x: bin.x + dx })),
14627
+ ticks: legend.ticks.map((tick) => ({ ...tick, x: tick.x + dx }))
14628
+ };
14629
+ return shifted;
14630
+ };
14631
+ return { ...layout, legend: move(layout.legend), legends: layout.legends.map(move) };
14632
+ }
14633
+ function mergeEndpointColumns(columns, area3, rightReserve, leadingColumnX) {
14634
+ if (!columns.length) return void 0;
14635
+ const { leadersEnabled: _leadersEnabled, ...first } = columns[0];
14636
+ const sweep = (entries) => {
14637
+ const size = first.labelStyle.fontSize;
14638
+ const tops = bidirectionalSweep(
14639
+ entries.map((entry, index) => ({
14640
+ naturalTop: entry.dataY - size / 2,
14641
+ height: entry.labelLines.length * size * (first.labelStyle.lineHeight ?? 1.25) + (entry.value ? first.valueGap + first.valueStyle.fontSize * (first.valueStyle.lineHeight ?? 1.25) : 0) + ENDPOINT_ENTRY_GAP,
14642
+ index
14643
+ })),
14644
+ area3.y,
14645
+ area3.y + area3.height
14646
+ );
14647
+ return entries.map(({ leadersEnabled, ...entry }, index) => ({
14648
+ ...entry,
14649
+ labelY: tops[index],
14650
+ showLeader: leadersEnabled && Math.abs(tops[index] + size / 2 - entry.dataY) > ENDPOINT_LEADER_THRESHOLD
14651
+ }));
14652
+ };
14653
+ const leadingColumns = columns.filter((column) => column.leadingBounds);
14654
+ const leadingWidth = Math.max(0, ...leadingColumns.map((column) => column.leadingBounds.width));
14655
+ return {
14656
+ ...first,
14657
+ entries: sweep(
14658
+ columns.flatMap(
14659
+ (column) => column.entries.map((entry) => ({ ...entry, leadersEnabled: column.leadersEnabled }))
14660
+ )
14661
+ ),
14662
+ bounds: {
14663
+ x: area3.x + area3.width + rightReserve + ENDPOINT_COLUMN_GAP,
14664
+ y: area3.y,
14665
+ width: Math.max(...columns.map((column) => column.bounds.width)),
14666
+ height: area3.height
14667
+ },
14668
+ ...leadingColumns.length ? {
14669
+ leading: sweep(
14670
+ leadingColumns.flatMap(
14671
+ (column) => (column.leading ?? []).map((entry) => ({
14672
+ ...entry,
14673
+ leadersEnabled: column.leadersEnabled
14674
+ }))
14675
+ )
14676
+ ),
14677
+ leadingBounds: {
14678
+ x: leadingColumnX ?? area3.x - ENDPOINT_COLUMN_GAP - leadingWidth,
14679
+ y: area3.y,
14680
+ width: leadingWidth,
14681
+ height: area3.height
14682
+ }
14683
+ } : {}
14684
+ };
14685
+ }
13703
14686
  function withXDomain(leaf, domain) {
13704
14687
  if (!leaf.encoding?.x) return leaf;
13705
14688
  return {
@@ -14168,11 +15151,12 @@ import {
14168
15151
  buildD3Formatter as buildD3Formatter2,
14169
15152
  CATEGORICAL_PALETTE as CATEGORICAL_PALETTE2,
14170
15153
  computeChrome as computeChrome3,
14171
- estimateTextWidth as estimateTextWidth19,
15154
+ estimateTextWidth as estimateTextWidth20,
14172
15155
  formatNumber as formatNumber4,
14173
- getBreakpoint,
15156
+ getBreakpoint as getBreakpoint2,
14174
15157
  HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
14175
15158
  HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
15159
+ mergeThemeConfig as mergeThemeConfig2,
14176
15160
  resolveTheme as resolveTheme3
14177
15161
  } from "@opendata-ai/openchart-core";
14178
15162
  import { geoArea, geoPath } from "d3-geo";
@@ -14412,10 +15396,10 @@ function categoricalLegendLabelStyle(theme) {
14412
15396
  };
14413
15397
  }
14414
15398
  function categoricalEntryWidth(label, labelStyle) {
14415
- return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP + estimateTextWidth19(label, labelStyle.fontSize, labelStyle.fontWeight) + CATEGORICAL_ENTRY_GAP;
15399
+ return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP + estimateTextWidth20(label, labelStyle.fontSize, labelStyle.fontWeight) + CATEGORICAL_ENTRY_GAP;
14416
15400
  }
14417
15401
  function noDataBlockWidth(labelStyle) {
14418
- return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP + estimateTextWidth19("No data", labelStyle.fontSize, labelStyle.fontWeight);
15402
+ return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP + estimateTextWidth20("No data", labelStyle.fontSize, labelStyle.fontWeight);
14419
15403
  }
14420
15404
  function wrapCategoricalEntries(widths, legendWidth) {
14421
15405
  const placed = [];
@@ -14490,13 +15474,15 @@ function compileGeoMap(spec, options) {
14490
15474
  const topology = validateGeoFeatures(mapSpec.geo);
14491
15475
  const rawWatermark = spec.watermark;
14492
15476
  const watermark = rawWatermark !== void 0 ? mapSpec.watermark : options.watermark ?? true;
14493
- const mergedThemeConfig = options.theme ? { ...mapSpec.theme, ...options.theme } : mapSpec.theme;
15477
+ emitThemeWarnings(options.theme, options.onWarn);
15478
+ const mergedThemeConfig = mergeThemeConfig2(mapSpec.theme, options.theme);
14494
15479
  const lightTheme = resolveTheme3(mergedThemeConfig);
14495
15480
  let theme = lightTheme;
14496
15481
  if (options.darkMode) {
14497
15482
  theme = adaptTheme2(theme);
14498
15483
  }
14499
15484
  const isDarkMode = options.darkMode;
15485
+ const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
14500
15486
  const chrome = computeChrome3(
14501
15487
  {
14502
15488
  title: mapSpec.chrome.title,
@@ -14509,10 +15495,9 @@ function compileGeoMap(spec, options) {
14509
15495
  options.width,
14510
15496
  options.measureText,
14511
15497
  "full",
14512
- void 0,
15498
+ padding,
14513
15499
  watermark
14514
15500
  );
14515
- const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
14516
15501
  const chromeLayout = resolveChromeLayout(
14517
15502
  spec,
14518
15503
  options
@@ -14728,7 +15713,7 @@ function compileGeoMap(spec, options) {
14728
15713
  const labelY = bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize;
14729
15714
  const hasUnmatched = featureMarks.some((m) => m.noData);
14730
15715
  const noDataX = bar.x + bar.width + NO_DATA_SWATCH_GAP;
14731
- const noDataLabelWidth = estimateTextWidth19(
15716
+ const noDataLabelWidth = estimateTextWidth20(
14732
15717
  "No data",
14733
15718
  labelStyle.fontSize,
14734
15719
  labelStyle.fontWeight
@@ -15141,7 +16126,7 @@ function compileGeoMap(spec, options) {
15141
16126
  height: contentHeight,
15142
16127
  animation: resolvedAnimation,
15143
16128
  watermark,
15144
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
16129
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize })),
15145
16130
  warnings: compileWarnings,
15146
16131
  mapSize: { width: fullArea.width, height: mapAreaHeight },
15147
16132
  focus: resolvedFocus
@@ -15517,7 +16502,7 @@ function emptyLayout3(chrome, theme, options, watermark) {
15517
16502
  height: options.height,
15518
16503
  animation: void 0,
15519
16504
  watermark,
15520
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
16505
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize })),
15521
16506
  warnings: [],
15522
16507
  mapSize: { width: 0, height: 0 },
15523
16508
  focus: null
@@ -15525,6 +16510,7 @@ function emptyLayout3(chrome, theme, options, watermark) {
15525
16510
  }
15526
16511
 
15527
16512
  // src/graphs/compile-graph.ts
16513
+ import { mergeThemeConfig as mergeThemeConfig3 } from "@opendata-ai/openchart-core";
15528
16514
  import {
15529
16515
  adaptTheme as adaptTheme3,
15530
16516
  computeChrome as computeChrome4,
@@ -15624,8 +16610,8 @@ function hexWithOpacity(hex, opacity) {
15624
16610
  }
15625
16611
  function computeDegrees(nodes, edges) {
15626
16612
  const degrees = /* @__PURE__ */ new Map();
15627
- for (const node of nodes) {
15628
- degrees.set(node.id, 0);
16613
+ for (const node2 of nodes) {
16614
+ degrees.set(node2.id, 0);
15629
16615
  }
15630
16616
  for (const edge of edges) {
15631
16617
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
@@ -15687,8 +16673,8 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
15687
16673
  const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
15688
16674
  const range = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
15689
16675
  const colorScale = scaleLinear5().domain(domain).range(range);
15690
- colorFn = (node) => {
15691
- const val = Number(node[field]);
16676
+ colorFn = (node2) => {
16677
+ const val = Number(node2[field]);
15692
16678
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
15693
16679
  };
15694
16680
  } else {
@@ -15699,7 +16685,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
15699
16685
  );
15700
16686
  const range = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
15701
16687
  const ordinalScale = scaleOrdinal2().domain(domain).range(range);
15702
- colorFn = (node) => ordinalScale(String(node[field] ?? ""));
16688
+ colorFn = (node2) => ordinalScale(String(node2[field] ?? ""));
15703
16689
  }
15704
16690
  }
15705
16691
  const defaultColor = theme.colors.categorical[0];
@@ -15726,48 +16712,48 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
15726
16712
  const range = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
15727
16713
  labelPriorityScale = scaleLinear5().domain(domain).range(range);
15728
16714
  }
15729
- return nodes.map((node) => {
16715
+ return nodes.map((node2) => {
15730
16716
  let radius = DEFAULT_NODE_RADIUS;
15731
16717
  if (sizeScale && encoding.nodeSize?.field) {
15732
- const val = Number(node[encoding.nodeSize.field]);
16718
+ const val = Number(node2[encoding.nodeSize.field]);
15733
16719
  if (Number.isFinite(val)) {
15734
16720
  radius = sizeScale(val);
15735
16721
  }
15736
16722
  }
15737
16723
  let opacity = 1;
15738
16724
  if (opacityScale && encoding.nodeOpacity?.field) {
15739
- const val = Number(node[encoding.nodeOpacity.field]);
16725
+ const val = Number(node2[encoding.nodeOpacity.field]);
15740
16726
  if (Number.isFinite(val)) {
15741
16727
  opacity = opacityScale(val);
15742
16728
  }
15743
16729
  }
15744
- const fill = colorFn ? colorFn(node) : defaultColor;
16730
+ const fill = colorFn ? colorFn(node2) : defaultColor;
15745
16731
  const stroke = surface;
15746
16732
  let label;
15747
16733
  if (encoding.nodeLabel?.field) {
15748
- const labelVal = node[encoding.nodeLabel.field];
16734
+ const labelVal = node2[encoding.nodeLabel.field];
15749
16735
  label = labelVal != null ? String(labelVal) : void 0;
15750
16736
  } else {
15751
- label = node.id;
16737
+ label = node2.id;
15752
16738
  }
15753
16739
  let labelPriority;
15754
16740
  if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
15755
- const val = Number(node[encoding.nodeLabelPriority.field]);
16741
+ const val = Number(node2[encoding.nodeLabelPriority.field]);
15756
16742
  labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
15757
16743
  } else {
15758
- const degree = degrees.get(node.id) ?? 0;
16744
+ const degree = degrees.get(node2.id) ?? 0;
15759
16745
  labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
15760
16746
  }
15761
- const { id: _id, ...rest } = node;
15762
- const data = { id: node.id, ...rest };
15763
- const override = nodeOverrides?.[node.id];
16747
+ const { id: _id, ...rest } = node2;
16748
+ const data = { id: node2.id, ...rest };
16749
+ const override = nodeOverrides?.[node2.id];
15764
16750
  const finalFill = override?.fill ?? fill;
15765
16751
  const finalRadius = override?.radius ?? radius;
15766
16752
  const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH;
15767
16753
  const finalStroke = override?.stroke ?? stroke;
15768
16754
  const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
15769
16755
  return {
15770
- id: node.id,
16756
+ id: node2.id,
15771
16757
  radius: finalRadius,
15772
16758
  fill: finalFill,
15773
16759
  stroke: finalStroke,
@@ -15871,30 +16857,30 @@ function resolveEdgeVisuals(edges, encoding, theme, nodeFills) {
15871
16857
  // src/graphs/community.ts
15872
16858
  function assignCommunities(nodes, clusteringField) {
15873
16859
  if (!clusteringField) return;
15874
- for (const node of nodes) {
15875
- const value = node.data[clusteringField];
15876
- node.community = value != null ? String(value) : void 0;
16860
+ for (const node2 of nodes) {
16861
+ const value = node2.data[clusteringField];
16862
+ node2.community = value != null ? String(value) : void 0;
15877
16863
  }
15878
16864
  }
15879
16865
  function buildCommunityColorMap(nodes, theme) {
15880
16866
  const colorMap = /* @__PURE__ */ new Map();
15881
16867
  const palette = theme.colors.categorical;
15882
16868
  let colorIndex = 0;
15883
- for (const node of nodes) {
15884
- if (node.community != null && !colorMap.has(node.community)) {
15885
- colorMap.set(node.community, palette[colorIndex % palette.length]);
16869
+ for (const node2 of nodes) {
16870
+ if (node2.community != null && !colorMap.has(node2.community)) {
16871
+ colorMap.set(node2.community, palette[colorIndex % palette.length]);
15886
16872
  colorIndex++;
15887
16873
  }
15888
16874
  }
15889
16875
  return colorMap;
15890
16876
  }
15891
16877
  function applyCommunityColors(nodes, colorMap) {
15892
- for (const node of nodes) {
15893
- if (node.community != null) {
15894
- const communityColor = colorMap.get(node.community);
16878
+ for (const node2 of nodes) {
16879
+ if (node2.community != null) {
16880
+ const communityColor = colorMap.get(node2.community);
15895
16881
  if (communityColor) {
15896
- node.fill = communityColor;
15897
- node.stroke = darkenColor(communityColor);
16882
+ node2.fill = communityColor;
16883
+ node2.stroke = darkenColor(communityColor);
15898
16884
  }
15899
16885
  }
15900
16886
  }
@@ -15966,9 +16952,9 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
15966
16952
  let entries;
15967
16953
  if (hasCommunities && communityColorMap.size > 0) {
15968
16954
  const counts = /* @__PURE__ */ new Map();
15969
- for (const node of nodes) {
15970
- if (node.community != null) {
15971
- counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
16955
+ for (const node2 of nodes) {
16956
+ if (node2.community != null) {
16957
+ counts.set(node2.community, (counts.get(node2.community) ?? 0) + 1);
15972
16958
  }
15973
16959
  }
15974
16960
  entries = [...communityColorMap.entries()].map(([label, color]) => ({
@@ -15981,10 +16967,10 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
15981
16967
  } else if (nodeColorField) {
15982
16968
  const categoryColors = /* @__PURE__ */ new Map();
15983
16969
  const categoryCounts = /* @__PURE__ */ new Map();
15984
- for (const node of nodes) {
15985
- const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
16970
+ for (const node2 of nodes) {
16971
+ const category = String(node2.data[nodeColorField] ?? node2.label ?? node2.id);
15986
16972
  if (!categoryColors.has(category)) {
15987
- categoryColors.set(category, node.fill);
16973
+ categoryColors.set(category, node2.fill);
15988
16974
  }
15989
16975
  categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
15990
16976
  }
@@ -16035,16 +17021,16 @@ function buildEdgeLegend(edges, encoding) {
16035
17021
  }
16036
17022
  function buildGraphTooltips(nodes) {
16037
17023
  const descriptors = /* @__PURE__ */ new Map();
16038
- for (const node of nodes) {
17024
+ for (const node2 of nodes) {
16039
17025
  const fields = [];
16040
- if (node.community != null) {
17026
+ if (node2.community != null) {
16041
17027
  fields.push({
16042
17028
  label: "Community",
16043
- value: node.community,
16044
- color: node.fill
17029
+ value: node2.community,
17030
+ color: node2.fill
16045
17031
  });
16046
17032
  }
16047
- for (const [key, value] of Object.entries(node.data)) {
17033
+ for (const [key, value] of Object.entries(node2.data)) {
16048
17034
  if (key === "id") continue;
16049
17035
  if (value == null) continue;
16050
17036
  fields.push({
@@ -16052,8 +17038,8 @@ function buildGraphTooltips(nodes) {
16052
17038
  value: typeof value === "number" ? graphNumberFormatter(value) : String(value)
16053
17039
  });
16054
17040
  }
16055
- descriptors.set(node.id, {
16056
- title: node.label ?? node.id,
17041
+ descriptors.set(node2.id, {
17042
+ title: node2.label ?? node2.id,
16057
17043
  fields
16058
17044
  });
16059
17045
  }
@@ -16096,7 +17082,8 @@ function compileGraph(spec, options) {
16096
17082
  }
16097
17083
  const rawWatermark = spec.watermark;
16098
17084
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
16099
- const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
17085
+ emitThemeWarnings(options.theme, options.onWarn);
17086
+ const mergedThemeConfig = mergeThemeConfig3(graphSpec.theme, options.theme);
16100
17087
  let theme = resolveTheme4(mergedThemeConfig);
16101
17088
  if (options.darkMode) {
16102
17089
  theme = adaptTheme3(theme);
@@ -16262,30 +17249,19 @@ var DEFAULT_COLLISION_PADDING = 5;
16262
17249
 
16263
17250
  // src/layout/dimensions.ts
16264
17251
  import {
16265
- AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD3,
16266
- axisTitleOffset as axisTitleOffset4,
16267
17252
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX3,
16268
17253
  computeChrome as computeChrome5,
16269
- computeFieldFormatContext as computeFieldFormatContext6,
16270
- defaultNumberFormatter as defaultNumberFormatter9,
16271
- estimateTextWidth as estimateTextWidth21,
16272
- getHeightClass as getHeightClass2,
17254
+ estimateTextWidth as estimateTextWidth22,
17255
+ getHeightClass as getHeightClass3,
16273
17256
  HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION3,
16274
17257
  HPAD_COMPACT_MIN as HPAD_COMPACT_MIN3,
16275
17258
  isAxislessMark as isAxislessMark2,
16276
- LABEL_GAP_COMPACT as LABEL_GAP_COMPACT2,
16277
- LABEL_GAP_DEFAULT as LABEL_GAP_DEFAULT2,
16278
- MAX_LEFT_LABEL_FRACTION_COMPACT as MAX_LEFT_LABEL_FRACTION_COMPACT2,
16279
- MAX_LEFT_LABEL_FRACTION_DEFAULT as MAX_LEFT_LABEL_FRACTION_DEFAULT2,
16280
- MAX_LEFT_LABEL_FRACTION_MEDIUM as MAX_LEFT_LABEL_FRACTION_MEDIUM2,
16281
- MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX as MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX2,
16282
17259
  NARROW_VIEWPORT_MAX as NARROW_VIEWPORT_MAX3,
16283
- resolveNumberFormatter as resolveNumberFormatter6,
16284
17260
  TOP_PAD_EXTRA_NARROW as TOP_PAD_EXTRA_NARROW2
16285
17261
  } from "@opendata-ai/openchart-core";
16286
17262
 
16287
17263
  // src/layout/metrics.ts
16288
- import { estimateTextWidth as estimateTextWidth20 } from "@opendata-ai/openchart-core";
17264
+ import { estimateTextWidth as estimateTextWidth21 } from "@opendata-ai/openchart-core";
16289
17265
  var DEFAULT_LABEL_FONT_SIZE = 10;
16290
17266
  var DEFAULT_VALUE_FONT_SIZE = 22;
16291
17267
  var LABEL_LINE_HEIGHT_RATIO = 1.4;
@@ -16318,7 +17294,7 @@ function computeMetricBar(metrics, metricsTopY, metricsArea, remainingChartHeigh
16318
17294
  const cellWidth = metricsArea.width / metrics.length;
16319
17295
  for (const metric of metrics) {
16320
17296
  const text = valueRunText(metric);
16321
- const measured = measureText ? measureText(text, fonts.value, 510).width : estimateTextWidth20(text, fonts.value, 510);
17297
+ const measured = measureText ? measureText(text, fonts.value, 510).width : estimateTextWidth21(text, fonts.value, 510);
16322
17298
  if (measured > cellWidth - CELL_INNER_PAD) return void 0;
16323
17299
  }
16324
17300
  const labelLine = fonts.label * LABEL_LINE_HEIGHT_RATIO;
@@ -16343,10 +17319,6 @@ function computeMetricBar(metrics, metricsTopY, metricsArea, remainingChartHeigh
16343
17319
  function metricFonts(theme) {
16344
17320
  return { label: theme.fonts.sizes.metricLabel, value: theme.fonts.sizes.metricValue };
16345
17321
  }
16346
- function legendLayoutHasContent(legendLayout) {
16347
- if (legendLayout.type === "continuous") return legendLayout.bounds.height > 0;
16348
- return "entries" in legendLayout && legendLayout.entries.length > 0;
16349
- }
16350
17322
  var MIN_CHART_WIDTH = 60;
16351
17323
  var MIN_CHART_HEIGHT2 = 40;
16352
17324
  var PLOT_SHARE_MAX = 0.4;
@@ -16370,7 +17342,7 @@ function getSparklinePad(spec) {
16370
17342
  bottom: hasDots ? Math.max(strokePad, dotPad) : strokePad
16371
17343
  };
16372
17344
  }
16373
- function computeDimensions(spec, options, legendLayout, theme, strategy, watermark = true, plan) {
17345
+ function computeDimensions(spec, options, legendLayout, theme, strategy, watermark, plan, reserveReferenceLabels = true) {
16374
17346
  const { width, height } = options;
16375
17347
  const padding = scalePadding(theme.spacing.padding, width, height);
16376
17348
  const hPad = width < BREAKPOINT_COMPACT_MAX3 ? Math.max(Math.round(padding * HPAD_COMPACT_FRACTION3), HPAD_COMPACT_MIN3) : padding;
@@ -16381,21 +17353,17 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16381
17353
  if (isSparkline && !userExplicit.chrome) {
16382
17354
  chromeMode = "hidden";
16383
17355
  }
16384
- const bottomLegendReservation = plan ? hasLegendContent(plan.legendContent) && plan.legendContent.position === "bottom" ? plan.legendContent.height + legendGap(width) : 0 : legendLayoutHasContent(legendLayout) && legendLayout.position === "bottom" ? legendLayout.bounds.height + legendGap(width) : 0;
16385
- const isCrampedHeight = getHeightClass2(height) === "cramped";
16386
- const chrome = plan?.chrome ?? computeChrome5(
16387
- chromeToInput(spec.chrome),
16388
- theme,
17356
+ const bottomLegendReservation = hasLegendContent(plan.legendContent) && plan.legendContent.position === "bottom" ? plan.legendContent.height + legendGap(width) : 0;
17357
+ const isCrampedHeight = getHeightClass3(height) === "cramped";
17358
+ const chrome = { ...plan.chrome };
17359
+ const referenceReserve = referenceLabelReserve(
17360
+ reserveReferenceLabels ? spec.annotations : [],
16389
17361
  width,
16390
- options.measureText,
16391
- chromeMode,
16392
- padding,
16393
- watermark,
16394
- bottomLegendReservation,
16395
- isCrampedHeight
17362
+ options.measureText ? (text, font) => options.measureText(text, font.fontSize, font.fontWeight).width : heuristicMeasure
16396
17363
  );
16397
17364
  const chromeLayout = resolveChromeLayout(spec, options);
16398
- const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
17365
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight + (options.footnoteReserve ?? 0) : height;
17366
+ if (chromeLayout === "grow") chrome.grownHeight = effectiveHeight - height;
16399
17367
  if (isSparkline) {
16400
17368
  const total2 = { x: 0, y: 0, width, height: effectiveHeight };
16401
17369
  const sparkPad = getSparklinePad(spec);
@@ -16404,10 +17372,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16404
17372
  const margins2 = {
16405
17373
  top: chrome.topHeight + sparkPad.top,
16406
17374
  right: sparkPad.right,
16407
- bottom: chrome.bottomHeight + sparkPad.bottom + xAxisSpace,
17375
+ bottom: chrome.bottomHeight + sparkPad.bottom + xAxisSpace + (options.footnoteReserve ?? 0),
16408
17376
  left: sparkPad.left + yAxisSpace
16409
17377
  };
16410
- if (userExplicit.legend && legendLayoutHasContent(legendLayout)) {
17378
+ if (userExplicit.legend && hasLegendContent(plan.legendContent)) {
16411
17379
  const gap = legendGap(width);
16412
17380
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
16413
17381
  margins2.right += legendLayout.bounds.width + 8;
@@ -16437,33 +17405,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16437
17405
  const total = { x: 0, y: 0, width, height: effectiveHeight };
16438
17406
  const isRadial = isAxislessMark2(spec.markType);
16439
17407
  const encoding = spec.encoding;
16440
- const xAxisSuppressed = encoding.x?.axis === false;
16441
- const xAxis = !xAxisSuppressed && encoding.x?.axis;
16442
- const hasXAxisLabel = !!xAxis?.title;
16443
- const xTickAngle = xAxis?.labelAngle;
16444
- let xAxisHeight;
16445
- if (plan) {
16446
- xAxisHeight = plan.xAxisExtent;
16447
- } else if (isRadial || xAxisSuppressed) {
16448
- xAxisHeight = 0;
16449
- } else if (xTickAngle && Math.abs(xTickAngle) > 10) {
16450
- const angleRad = Math.abs(xTickAngle) * (Math.PI / 180);
16451
- const xField = encoding.x?.field;
16452
- let maxLabelWidth = 40;
16453
- if (xField) {
16454
- for (const row of spec.data) {
16455
- const label = String(row[xField] ?? "");
16456
- const w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
16457
- if (w > maxLabelWidth) maxLabelWidth = w;
16458
- }
16459
- }
16460
- const rotatedHeight = maxLabelWidth * Math.sin(angleRad) + 6;
16461
- const labelHeight = Math.min(rotatedHeight, 120);
16462
- xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
16463
- } else {
16464
- const base = theme.spacing.xAxisHeight;
16465
- xAxisHeight = hasXAxisLabel ? base + 22 : base;
16466
- }
17408
+ const xAxisHeight = plan.xAxisExtent;
16467
17409
  const yIsInlinePre = yTickPositionIsInline(encoding.y, spec.markType);
16468
17410
  const inlineTickOverhang = yIsInlinePre ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
16469
17411
  const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
@@ -16473,7 +17415,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16473
17415
  const wantsSearch = !!spec.seriesSearch && chromeMode !== "hidden" && !isCrampedHeight;
16474
17416
  const searchReservation = wantsSearch ? SERIES_SEARCH_INPUT_HEIGHT + SERIES_SEARCH_GAP : 0;
16475
17417
  const margins = {
16476
- top: topPad + chrome.topHeight + tentativeMetricsHeight + searchReservation,
17418
+ top: topPad + chrome.topHeight + tentativeMetricsHeight + searchReservation + referenceReserve.top,
16477
17419
  right: hPad + (isRadial ? hPad : axisMargin),
16478
17420
  bottom: bottomMargin(chrome.bottomHeight, padding, xAxisHeight),
16479
17421
  left: hPad + (isRadial ? hPad : axisMargin)
@@ -16485,7 +17427,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16485
17427
  seriesCount,
16486
17428
  labelsHiddenByStrategy,
16487
17429
  labelsDensityNone: labelDensity === "none",
16488
- endpointLabelsDemoted: plan?.endpointLabelsDemoted
17430
+ endpointLabelsDemoted: plan.endpointLabelsDemoted
16489
17431
  });
16490
17432
  let endpointWidth = 0;
16491
17433
  if (sup.showEndpointLabels) {
@@ -16507,7 +17449,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16507
17449
  const label = String(row[colorField] ?? "");
16508
17450
  if (!seen.has(label)) {
16509
17451
  seen.add(label);
16510
- const w = estimateTextWidth21(label, 11, 600);
17452
+ const w = estimateTextWidth22(label, 11, 600);
16511
17453
  if (w > maxLabelWidth) maxLabelWidth = w;
16512
17454
  }
16513
17455
  }
@@ -16516,138 +17458,18 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16516
17458
  }
16517
17459
  }
16518
17460
  }
16519
- if (strategy?.annotationPosition !== "tooltip-only" && spec.annotations.length > 0 && encoding.x) {
16520
- const xField = encoding.x.field;
16521
- let maxX;
16522
- for (const row of spec.data) {
16523
- const v = row[xField];
16524
- if (v != null && (maxX == null || String(v) >= String(maxX))) maxX = v;
16525
- }
16526
- if (maxX != null) {
16527
- const maxXStr = String(maxX);
16528
- for (const ann of spec.annotations) {
16529
- if (ann.type === "text" && String(ann.x) === maxXStr) {
16530
- const textWidth = estimateTextWidth21(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
16531
- const dx = ann.offset?.dx ?? 0;
16532
- const anchor = ann.anchor ?? "auto";
16533
- const baseRightExtent = anchor === "right" ? textWidth : anchor === "left" ? 0 : textWidth / 2;
16534
- const rightOverflow = Math.max(0, baseRightExtent + dx);
16535
- if (rightOverflow > 0) {
16536
- margins.right = Math.max(margins.right, hPad + rightOverflow + 12);
16537
- }
16538
- }
16539
- }
16540
- }
16541
- }
16542
- const yAxisSuppressed = encoding.y?.axis === false;
16543
- const yIsInline = yIsInlinePre;
16544
- if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
16545
- const isBinnedBar = isBinnedBarEncoding(spec.markType, encoding);
16546
- if (!plan && !isBinnedBar && (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal")) {
16547
- const yField = encoding.y.field;
16548
- const yLabelField = encoding.y.axis?.labelField;
16549
- let maxLabelWidth = 0;
16550
- for (const row of spec.data) {
16551
- const label = String(row[yField] ?? "");
16552
- let w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
16553
- if (yLabelField) {
16554
- const subtitle = String(row[yLabelField] ?? "");
16555
- if (subtitle) {
16556
- const gap = theme.fonts.sizes.axisTick * 0.6;
16557
- const subtitleWidth = estimateTextWidth21(
16558
- subtitle,
16559
- theme.fonts.sizes.axisTick,
16560
- theme.fonts.weights.normal
16561
- );
16562
- w += gap + subtitleWidth;
16563
- }
16564
- }
16565
- if (w > maxLabelWidth) maxLabelWidth = w;
16566
- }
16567
- if (maxLabelWidth > 0) {
16568
- const labelGap = width < NARROW_VIEWPORT_MAX3 ? LABEL_GAP_COMPACT2 : LABEL_GAP_DEFAULT2;
16569
- const maxLeftFraction = width < BREAKPOINT_COMPACT_MAX3 ? MAX_LEFT_LABEL_FRACTION_COMPACT2 : width < MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX2 ? MAX_LEFT_LABEL_FRACTION_MEDIUM2 : MAX_LEFT_LABEL_FRACTION_DEFAULT2;
16570
- const maxLeftReserved = Math.floor(width * maxLeftFraction);
16571
- const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
16572
- margins.left = Math.max(margins.left, reserved);
16573
- }
16574
- } else if ((encoding.y.type === "quantitative" || encoding.y.type === "temporal") && !plan) {
16575
- const yField = encoding.y.field;
16576
- const yAxisFormat = encoding.y.axis?.format;
16577
- let maxAbsVal = 0;
16578
- for (const row of spec.data) {
16579
- const v = Number(row[yField]);
16580
- if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
16581
- }
16582
- const ctx = computeFieldFormatContext6(fieldIterable(spec.data, yField));
16583
- let sampleLabel;
16584
- if (yAxisFormat) {
16585
- const fmt2 = resolveNumberFormatter6(yAxisFormat, ctx);
16586
- sampleLabel = fmt2 ? fmt2(maxAbsVal) : String(maxAbsVal);
16587
- } else {
16588
- sampleLabel = defaultNumberFormatter9({ ...ctx, extent: ctx.extent ?? [0, maxAbsVal] })(
16589
- maxAbsVal
16590
- );
16591
- }
16592
- const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
16593
- const labelEst = negPrefix + sampleLabel;
16594
- const labelWidth = estimateTextWidth21(
16595
- labelEst,
16596
- theme.fonts.sizes.axisTick,
16597
- theme.fonts.weights.normal
16598
- );
16599
- margins.left = Math.max(margins.left, hPad + labelWidth + 10);
16600
- }
16601
- }
16602
- const yAxis = encoding.y?.axis;
16603
- if (yAxis && (yAxis.title || yAxis.label) && !isRadial && !plan) {
16604
- const yFieldForTitle = encoding.y?.field;
16605
- const yAxisFormatForTitle = yAxis?.format;
16606
- let estTickLabelWidth = 0;
16607
- if (yFieldForTitle && !yIsInlinePre && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
16608
- let maxAbsValForTitle = 0;
16609
- for (const row of spec.data) {
16610
- const v = Number(row[yFieldForTitle]);
16611
- if (Number.isFinite(v) && Math.abs(v) > maxAbsValForTitle) maxAbsValForTitle = Math.abs(v);
16612
- }
16613
- const ctxForTitle = computeFieldFormatContext6(fieldIterable(spec.data, yFieldForTitle));
16614
- let sampleLabelForTitle;
16615
- if (yAxisFormatForTitle) {
16616
- const fmt2 = resolveNumberFormatter6(yAxisFormatForTitle, ctxForTitle);
16617
- sampleLabelForTitle = fmt2 ? fmt2(maxAbsValForTitle) : String(maxAbsValForTitle);
16618
- } else {
16619
- sampleLabelForTitle = defaultNumberFormatter9({
16620
- ...ctxForTitle,
16621
- extent: ctxForTitle.extent ?? [0, maxAbsValForTitle]
16622
- })(maxAbsValForTitle);
16623
- }
16624
- const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
16625
- estTickLabelWidth = estimateTextWidth21(
16626
- negPrefixForTitle + sampleLabelForTitle,
16627
- theme.fonts.sizes.axisTick,
16628
- theme.fonts.weights.normal
16629
- );
16630
- }
16631
- const titleFontSize = theme.fonts.sizes.body;
16632
- const offset = axisTitleOffset4(estTickLabelWidth, titleFontSize, width, yIsInlinePre);
16633
- const halfGlyph = Math.ceil(titleFontSize / 2);
16634
- const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX3 ? 0 : AXIS_TITLE_TRAILING_PAD3);
16635
- margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
16636
- }
16637
- if (plan) {
16638
- margins.left = Math.max(margins.left, plan.leftGutter);
16639
- }
17461
+ margins.left = Math.max(margins.left, plan.leftGutter);
16640
17462
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
16641
17463
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
16642
17464
  }
16643
17465
  if (options.footnoteReserve && options.footnoteReserve > 0) {
16644
17466
  margins.bottom += options.footnoteReserve;
16645
17467
  }
16646
- const legendHasEntries = plan ? hasLegendContent(plan.legendContent) : legendLayoutHasContent(legendLayout);
16647
- const legendPos = plan ? plan.legendContent.position : legendLayout.position;
16648
- const legendHeight = plan ? plan.legendContent.height : legendLayout.bounds.height;
16649
- const legendBoundsWidth = plan ? plan.legendContent.legendWidth : legendLayout.bounds.width;
16650
- const sizeLegend = plan?.sizeLegendContent;
17468
+ const legendHasEntries = hasLegendContent(plan.legendContent);
17469
+ const legendPos = plan.legendContent.position;
17470
+ const legendHeight = plan.legendContent.height;
17471
+ const legendBoundsWidth = plan.legendContent.legendWidth;
17472
+ const sizeLegend = plan.sizeLegendContent;
16651
17473
  const colorLegendInGutter = legendHasEntries && (legendPos === "right" || legendPos === "bottom-right");
16652
17474
  const hasTopLegend = legendHasEntries && legendPos === "top";
16653
17475
  if (legendHasEntries) {
@@ -16658,7 +17480,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16658
17480
  }
16659
17481
  const colorReserve = colorLegendInGutter ? legendBoundsWidth + 8 : 0;
16660
17482
  const sizeReserve = sizeLegend ? sizeLegend.width + SIZE_LEGEND_GAP : 0;
16661
- margins.right += Math.max(colorReserve, sizeReserve);
17483
+ margins.right += Math.max(colorReserve, sizeReserve) + referenceReserve.right;
16662
17484
  const effectiveAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
16663
17485
  margins.top += effectiveAxisGap;
16664
17486
  let chartArea = {
@@ -16685,7 +17507,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
16685
17507
  const fallbackEffectiveAxisGap = hasTopLegend ? inlineTickOverhang : fallbackTopAxisGap;
16686
17508
  const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight + searchReservation;
16687
17509
  const topDelta = margins.top - newTop;
16688
- const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
17510
+ const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight) + (options.footnoteReserve ?? 0);
16689
17511
  const bottomDelta = margins.bottom - newBottom;
16690
17512
  if (topDelta > 0 || bottomDelta > 0) {
16691
17513
  const gap = legendGap(width);
@@ -16804,39 +17626,52 @@ function facetMinHeight(panelCount, columns) {
16804
17626
  const rows = Math.ceil(panelCount / columns);
16805
17627
  return rows * (MIN_PANEL_HEIGHT + HEADER_HEIGHT + X_AXIS_HEIGHT) + (rows - 1) * PANEL_GAP;
16806
17628
  }
16807
- function computeFacetGrid(facetValues, requestedColumns, totalArea, outerAxisReservation) {
17629
+ function computeFacetGrid(facetValues, requestedColumns, totalArea, outerAxisReservation, referenceInsets) {
16808
17630
  const n = facetValues.length;
16809
17631
  if (n === 0) return { panels: [], columns: 0, rows: 0 };
16810
17632
  const usableWidth = totalArea.width - outerAxisReservation.left;
16811
17633
  let columns = Math.min(requestedColumns ?? autoColumns(n, usableWidth), n);
16812
17634
  while (columns > 1) {
16813
17635
  const panelWidth2 = (usableWidth - (columns - 1) * PANEL_GAP) / columns;
16814
- if (panelWidth2 >= MIN_PANEL_WIDTH) break;
17636
+ const innerAxis = referenceInsets?.innerAxisWidth ?? 0;
17637
+ const right = referenceInsets?.measure(panelWidth2 - innerAxis).right ?? 0;
17638
+ if (panelWidth2 - innerAxis - right >= MIN_PANEL_WIDTH) break;
16815
17639
  columns--;
16816
17640
  }
16817
17641
  const rows = Math.ceil(n / columns);
16818
17642
  const panelWidth = Math.max(0, (usableWidth - (columns - 1) * PANEL_GAP) / columns);
16819
17643
  const xAxisPerRow = outerAxisReservation.bottom;
16820
17644
  const usableHeight = totalArea.height;
17645
+ const topInset = referenceInsets?.measure(panelWidth - (columns > 1 ? referenceInsets.innerAxisWidth : 0)).top ?? 0;
16821
17646
  const panelHeight = Math.max(
16822
17647
  0,
16823
- (usableHeight - rows * (HEADER_HEIGHT + xAxisPerRow) - (rows - 1) * PANEL_GAP) / rows
17648
+ (usableHeight - rows * (HEADER_HEIGHT + topInset + xAxisPerRow) - (rows - 1) * PANEL_GAP) / rows
16824
17649
  );
16825
- const rowStride = HEADER_HEIGHT + panelHeight + xAxisPerRow + PANEL_GAP;
17650
+ const rowStride = HEADER_HEIGHT + topInset + panelHeight + xAxisPerRow + PANEL_GAP;
16826
17651
  const panels = [];
16827
17652
  for (let i = 0; i < n; i++) {
16828
17653
  const col = i % columns;
16829
17654
  const row = Math.floor(i / columns);
16830
17655
  const x = totalArea.x + outerAxisReservation.left + col * (panelWidth + PANEL_GAP);
16831
17656
  const y = totalArea.y + row * rowStride;
17657
+ const leftInset = col > 0 ? referenceInsets?.innerAxisWidth ?? 0 : 0;
17658
+ const reserve = referenceInsets?.measure(panelWidth - leftInset);
16832
17659
  panels.push({
16833
17660
  key: facetValues[i],
16834
17661
  area: {
16835
- x,
16836
- y: y + HEADER_HEIGHT,
16837
- width: panelWidth,
17662
+ x: x + leftInset,
17663
+ y: y + HEADER_HEIGHT + topInset,
17664
+ width: Math.max(0, panelWidth - leftInset - (reserve?.right ?? 0)),
16838
17665
  height: panelHeight
16839
17666
  },
17667
+ ...referenceInsets ? {
17668
+ referenceContainer: {
17669
+ x: x + leftInset,
17670
+ y: y + HEADER_HEIGHT,
17671
+ width: panelWidth - leftInset,
17672
+ height: topInset + panelHeight
17673
+ }
17674
+ } : {},
16840
17675
  headerPos: {
16841
17676
  x: x + panelWidth / 2,
16842
17677
  y: y + HEADER_HEIGHT * 0.6
@@ -16845,7 +17680,14 @@ function computeFacetGrid(facetValues, requestedColumns, totalArea, outerAxisRes
16845
17680
  col
16846
17681
  });
16847
17682
  }
16848
- return { panels, columns, rows };
17683
+ return {
17684
+ panels,
17685
+ columns,
17686
+ rows,
17687
+ ...referenceInsets ? {
17688
+ minimumHeight: rows * (MIN_PANEL_HEIGHT + HEADER_HEIGHT + topInset + xAxisPerRow) + (rows - 1) * PANEL_GAP
17689
+ } : {}
17690
+ };
16849
17691
  }
16850
17692
 
16851
17693
  // src/layout/gridlines.ts
@@ -16878,12 +17720,13 @@ function computeGridlines(axes, chartArea, showVertical = false) {
16878
17720
  }
16879
17721
 
16880
17722
  // src/sankey/compile-sankey.ts
17723
+ import { mergeThemeConfig as mergeThemeConfig4 } from "@opendata-ai/openchart-core";
16881
17724
  import {
16882
17725
  adaptTheme as adaptTheme4,
16883
17726
  buildD3Formatter as buildD3Formatter3,
16884
17727
  computeChrome as computeChrome6,
16885
- defaultNumberFormatter as defaultNumberFormatter10,
16886
- estimateTextWidth as estimateTextWidth22,
17728
+ defaultNumberFormatter as defaultNumberFormatter9,
17729
+ estimateTextWidth as estimateTextWidth23,
16887
17730
  resolveTheme as resolveTheme5
16888
17731
  } from "@opendata-ai/openchart-core";
16889
17732
 
@@ -16977,12 +17820,12 @@ function buildNodeColorMap(nodes, palette, colorField, data, sourceField, target
16977
17820
  }
16978
17821
  const categoryIndex = /* @__PURE__ */ new Map();
16979
17822
  let nextIdx = 0;
16980
- for (const node of nodes) {
16981
- const category = nodeCategoryMap.get(node.id) ?? node.id;
17823
+ for (const node2 of nodes) {
17824
+ const category = nodeCategoryMap.get(node2.id) ?? node2.id;
16982
17825
  if (!categoryIndex.has(category)) {
16983
17826
  categoryIndex.set(category, nextIdx++);
16984
17827
  }
16985
- colorMap.set(node.id, pickColor(palette, categoryIndex.get(category)));
17828
+ colorMap.set(node2.id, pickColor(palette, categoryIndex.get(category)));
16986
17829
  }
16987
17830
  } else {
16988
17831
  for (let i = 0; i < nodes.length; i++) {
@@ -17012,11 +17855,11 @@ function resolveOtherConfig(other) {
17012
17855
  }
17013
17856
  function bucketSmallNodes(nodes, cfg) {
17014
17857
  const byDepth = /* @__PURE__ */ new Map();
17015
- for (const node of nodes) {
17016
- const depth = node.depth ?? 0;
17858
+ for (const node2 of nodes) {
17859
+ const depth = node2.depth ?? 0;
17017
17860
  const group = byDepth.get(depth);
17018
- if (group) group.push(node);
17019
- else byDepth.set(depth, [node]);
17861
+ if (group) group.push(node2);
17862
+ else byDepth.set(depth, [node2]);
17020
17863
  }
17021
17864
  const merged = /* @__PURE__ */ new Map();
17022
17865
  for (const [depth, group] of byDepth) {
@@ -17024,7 +17867,7 @@ function bucketSmallNodes(nodes, cfg) {
17024
17867
  if (total <= 0) continue;
17025
17868
  const small = group.filter((n) => (n.value ?? 0) / total < cfg.threshold);
17026
17869
  if (small.length < 2) continue;
17027
- for (const node of small) merged.set(node.id, `${OTHER_ID_PREFIX}${depth}`);
17870
+ for (const node2 of small) merged.set(node2.id, `${OTHER_ID_PREFIX}${depth}`);
17028
17871
  }
17029
17872
  return merged;
17030
17873
  }
@@ -17057,8 +17900,8 @@ function labelsLeftForDepth(depth, maxDepth, nodeLabelAlign) {
17057
17900
  if (depth === 0 && maxDepth > 0) return true;
17058
17901
  return false;
17059
17902
  }
17060
- function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "auto", containerWidth, padding) {
17061
- const depth = node.depth ?? 0;
17903
+ function computeNodeLabel(node2, maxDepth, theme, nodeWidth, nodeLabelAlign = "auto", containerWidth, padding) {
17904
+ const depth = node2.depth ?? 0;
17062
17905
  const placeLeft = labelsLeftForDepth(depth, maxDepth, nodeLabelAlign);
17063
17906
  const style = {
17064
17907
  fontFamily: theme.fonts.family,
@@ -17067,10 +17910,10 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
17067
17910
  fill: theme.colors.text,
17068
17911
  lineHeight: 1.3
17069
17912
  };
17070
- const x0 = node.x0 ?? 0;
17071
- const x1 = node.x1 ?? nodeWidth;
17072
- const y0 = node.y0 ?? 0;
17073
- const y1 = node.y1 ?? 0;
17913
+ const x0 = node2.x0 ?? 0;
17914
+ const x1 = node2.x1 ?? nodeWidth;
17915
+ const y0 = node2.y0 ?? 0;
17916
+ const y1 = node2.y1 ?? 0;
17074
17917
  const midY = (y0 + y1) / 2;
17075
17918
  const pad = padding ?? 0;
17076
17919
  let maxWidth;
@@ -17084,7 +17927,7 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
17084
17927
  }
17085
17928
  if (placeLeft) {
17086
17929
  return {
17087
- text: node.label ?? node.id,
17930
+ text: node2.label ?? node2.id,
17088
17931
  x: x0 - LABEL_GAP3,
17089
17932
  y: midY,
17090
17933
  style: { ...style, textAnchor: "end", dominantBaseline: "central" },
@@ -17093,7 +17936,7 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
17093
17936
  };
17094
17937
  }
17095
17938
  return {
17096
- text: node.label ?? node.id,
17939
+ text: node2.label ?? node2.id,
17097
17940
  x: x1 + LABEL_GAP3,
17098
17941
  y: midY,
17099
17942
  style: { ...style, textAnchor: "start", dominantBaseline: "central" },
@@ -17115,7 +17958,8 @@ function compileSankey(spec, options) {
17115
17958
  const resolvedValueFormat = sankeySpec.encoding.value.format ?? sankeySpec.valueFormat;
17116
17959
  const rawWatermark = spec.watermark;
17117
17960
  const watermark = rawWatermark !== void 0 ? sankeySpec.watermark : options.watermark ?? true;
17118
- const mergedThemeConfig = options.theme ? { ...sankeySpec.theme, ...options.theme } : sankeySpec.theme;
17961
+ emitThemeWarnings(options.theme, options.onWarn);
17962
+ const mergedThemeConfig = mergeThemeConfig4(sankeySpec.theme, options.theme);
17119
17963
  const lightTheme = resolveTheme5(mergedThemeConfig);
17120
17964
  let theme = lightTheme;
17121
17965
  if (options.darkMode) {
@@ -17260,18 +18104,18 @@ function compileSankey(spec, options) {
17260
18104
  }
17261
18105
  const applyOtherLabels = (list) => {
17262
18106
  if (otherLabels.size === 0) return;
17263
- for (const node of list) {
17264
- const label = otherLabels.get(node.id);
17265
- if (label) node.label = label;
18107
+ for (const node2 of list) {
18108
+ const label = otherLabels.get(node2.id);
18109
+ if (label) node2.label = label;
17266
18110
  }
17267
18111
  };
17268
18112
  applyOtherLabels(nodes);
17269
18113
  const nodeFmt = resolveNodeValueFormatter(nodes, resolvedValueFormat, flowFmt);
17270
- const labelBlockWidth = (node) => {
17271
- const name = node.label ?? node.id;
17272
- const nameWidth = estimateTextWidth22(name, labelFontSize, labelFontWeight);
17273
- const valueWidth = estimateTextWidth22(
17274
- formatFlowValue(node.value ?? 0, nodeFmt),
18114
+ const labelBlockWidth = (node2) => {
18115
+ const name = node2.label ?? node2.id;
18116
+ const nameWidth = estimateTextWidth23(name, labelFontSize, labelFontWeight);
18117
+ const valueWidth = estimateTextWidth23(
18118
+ formatFlowValue(node2.value ?? 0, nodeFmt),
17275
18119
  labelFontSize,
17276
18120
  valueFontWeight
17277
18121
  );
@@ -17282,14 +18126,14 @@ function compileSankey(spec, options) {
17282
18126
  const cap = area3.width * MAX_LABEL_GUTTER;
17283
18127
  let left = 0;
17284
18128
  let right = 0;
17285
- for (const node of list) {
17286
- const depth = node.depth ?? 0;
17287
- const width = labelBlockWidth(node) + LABEL_GAP3;
18129
+ for (const node2 of list) {
18130
+ const depth = node2.depth ?? 0;
18131
+ const width = labelBlockWidth(node2) + LABEL_GAP3;
17288
18132
  if (labelsLeftForDepth(depth, maxDepth2, nodeLabelAlign)) {
17289
- const need = width - ((node.x0 ?? 0) - area3.x);
18133
+ const need = width - ((node2.x0 ?? 0) - area3.x);
17290
18134
  if (need > left) left = need;
17291
18135
  } else {
17292
- const need = width - (area3.x + area3.width - (node.x1 ?? 0));
18136
+ const need = width - (area3.x + area3.width - (node2.x1 ?? 0));
17293
18137
  if (need > right) right = need;
17294
18138
  }
17295
18139
  }
@@ -17332,39 +18176,39 @@ function compileSankey(spec, options) {
17332
18176
  lineHeight: 1.3
17333
18177
  };
17334
18178
  const columnX0 = /* @__PURE__ */ new Map();
17335
- for (const node of nodes) {
17336
- const d = node.depth ?? 0;
17337
- const x0 = node.x0 ?? 0;
18179
+ for (const node2 of nodes) {
18180
+ const d = node2.depth ?? 0;
18181
+ const x0 = node2.x0 ?? 0;
17338
18182
  const current = columnX0.get(d);
17339
18183
  if (current === void 0 || x0 < current) columnX0.set(d, x0);
17340
18184
  }
17341
- const valueLabelFor = (node, depth, text) => {
18185
+ const valueLabelFor = (node2, depth, text) => {
17342
18186
  const placedLeft = labelsLeftForDepth(depth, maxDepth, nodeLabelAlign);
17343
18187
  if (placedLeft || depth === maxDepth) return { text, style: valueStyle };
17344
18188
  const nextColumn = columnX0.get(depth + 1);
17345
18189
  if (nextColumn === void 0) return { text, style: valueStyle };
17346
- const available = nextColumn - LABEL_GAP3 - ((node.x1 ?? 0) + LABEL_GAP3);
17347
- const needed = estimateTextWidth22(node.label ?? node.id, labelFontSize, labelFontWeight) + VALUE_GAP + estimateTextWidth22(text, labelFontSize, valueFontWeight);
18190
+ const available = nextColumn - LABEL_GAP3 - ((node2.x1 ?? 0) + LABEL_GAP3);
18191
+ const needed = estimateTextWidth23(node2.label ?? node2.id, labelFontSize, labelFontWeight) + VALUE_GAP + estimateTextWidth23(text, labelFontSize, valueFontWeight);
17348
18192
  return needed <= available ? { text, style: valueStyle } : void 0;
17349
18193
  };
17350
- const nodeMarks = nodes.map((node) => {
17351
- const fill = nodeColorMap.get(node.id) ?? theme.colors.categorical[0];
17352
- const depth = node.depth ?? 0;
17353
- const merged = otherMembers.get(node.id);
17354
- const valueText = formatFlowValue(node.value ?? 0, nodeFmt);
18194
+ const nodeMarks = nodes.map((node2) => {
18195
+ const fill = nodeColorMap.get(node2.id) ?? theme.colors.categorical[0];
18196
+ const depth = node2.depth ?? 0;
18197
+ const merged = otherMembers.get(node2.id);
18198
+ const valueText = formatFlowValue(node2.value ?? 0, nodeFmt);
17355
18199
  return {
17356
18200
  type: "sankeyNode",
17357
- x: node.x0 ?? 0,
17358
- y: node.y0 ?? 0,
17359
- width: (node.x1 ?? 0) - (node.x0 ?? 0),
17360
- height: (node.y1 ?? 0) - (node.y0 ?? 0),
18201
+ x: node2.x0 ?? 0,
18202
+ y: node2.y0 ?? 0,
18203
+ width: (node2.x1 ?? 0) - (node2.x0 ?? 0),
18204
+ height: (node2.y1 ?? 0) - (node2.y0 ?? 0),
17361
18205
  fill,
17362
18206
  // Explicit: a node is a solid block of flow, not an outlined box. Without
17363
18207
  // this the renderer inherits whatever stroke the SVG context carries.
17364
18208
  stroke: "none",
17365
18209
  cornerRadius: NODE_CORNER_RADIUS,
17366
18210
  label: computeNodeLabel(
17367
- node,
18211
+ node2,
17368
18212
  maxDepth,
17369
18213
  theme,
17370
18214
  sankeySpec.nodeWidth,
@@ -17372,14 +18216,14 @@ function compileSankey(spec, options) {
17372
18216
  options.width,
17373
18217
  padding
17374
18218
  ),
17375
- valueLabel: valueLabelFor(node, depth, valueText),
17376
- nodeId: node.id,
17377
- value: node.value ?? 0,
18219
+ valueLabel: valueLabelFor(node2, depth, valueText),
18220
+ nodeId: node2.id,
18221
+ value: node2.value ?? 0,
17378
18222
  depth,
17379
- data: merged ? { id: node.id, label: node.label, merged } : { id: node.id, label: node.label },
18223
+ data: merged ? { id: node2.id, label: node2.label, merged } : { id: node2.id, label: node2.label },
17380
18224
  aria: {
17381
18225
  role: "img",
17382
- label: `${node.label}: ${valueText}`
18226
+ label: `${node2.label}: ${valueText}`
17383
18227
  },
17384
18228
  animationIndex: 0
17385
18229
  // Reassigned below after sorting by depth
@@ -17529,22 +18373,22 @@ function resolveNodeValueFormatter(nodes, explicitFormat, fallback) {
17529
18373
  }
17530
18374
  return buildD3Formatter3(`,.${decimals}f`) ?? fallback;
17531
18375
  }
17532
- var defaultFmt3 = defaultNumberFormatter10();
18376
+ var defaultFmt3 = defaultNumberFormatter9();
17533
18377
  function formatFlowValue(value, formatter) {
17534
18378
  if (formatter) return formatter(value);
17535
18379
  return defaultFmt3(value);
17536
18380
  }
17537
18381
  function buildTooltipDescriptors(nodes, links, formatter, nodeFormatter) {
17538
18382
  const descriptors = /* @__PURE__ */ new Map();
17539
- for (const node of nodes) {
18383
+ for (const node2 of nodes) {
17540
18384
  const fields = [
17541
18385
  {
17542
18386
  label: "Total flow",
17543
- value: formatFlowValue(node.value, nodeFormatter ?? formatter)
18387
+ value: formatFlowValue(node2.value, nodeFormatter ?? formatter)
17544
18388
  }
17545
18389
  ];
17546
- descriptors.set(`node-${node.nodeId}`, {
17547
- title: node.label.text,
18390
+ descriptors.set(`node-${node2.nodeId}`, {
18391
+ title: node2.label.text,
17548
18392
  fields
17549
18393
  });
17550
18394
  }
@@ -17602,7 +18446,7 @@ function emptyLayout4(area3, chrome, theme, options, watermark) {
17602
18446
  }
17603
18447
 
17604
18448
  // src/tables/compile-table.ts
17605
- import { computeChrome as computeChrome7, estimateTextWidth as estimateTextWidth23 } from "@opendata-ai/openchart-core";
18449
+ import { computeChrome as computeChrome7, estimateTextWidth as estimateTextWidth24 } from "@opendata-ai/openchart-core";
17606
18450
 
17607
18451
  // src/tables/bar-column.ts
17608
18452
  var NEGATIVE_BAR_COLOR = "#c44e52";
@@ -17726,9 +18570,9 @@ function computeCategoryColors(data, column, theme, darkMode) {
17726
18570
  // src/tables/format-cells.ts
17727
18571
  import {
17728
18572
  buildD3Formatter as buildD3Formatter4,
17729
- defaultNumberFormatter as defaultNumberFormatter11,
18573
+ defaultNumberFormatter as defaultNumberFormatter10,
17730
18574
  formatDate as formatDate3,
17731
- resolveNumberFormatter as resolveNumberFormatter7
18575
+ resolveNumberFormatter as resolveNumberFormatter6
17732
18576
  } from "@opendata-ai/openchart-core";
17733
18577
  function isNumericValue(value) {
17734
18578
  if (typeof value === "number") return Number.isFinite(value);
@@ -17748,7 +18592,7 @@ function formatCell(value, column) {
17748
18592
  };
17749
18593
  }
17750
18594
  if (column.format && isNumericValue(value)) {
17751
- const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
18595
+ const formatter = resolveNumberFormatter6(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
17752
18596
  if (formatter) {
17753
18597
  return {
17754
18598
  value,
@@ -17760,7 +18604,7 @@ function formatCell(value, column) {
17760
18604
  if (isNumericValue(value)) {
17761
18605
  return {
17762
18606
  value,
17763
- formattedValue: defaultNumberFormatter11({ surface: "table" })(value),
18607
+ formattedValue: defaultNumberFormatter10({ surface: "table" })(value),
17764
18608
  style
17765
18609
  };
17766
18610
  }
@@ -17780,13 +18624,13 @@ function formatCell(value, column) {
17780
18624
  function formatValueForSearch(value, column) {
17781
18625
  if (value == null) return "";
17782
18626
  if (column.format && isNumericValue(value)) {
17783
- const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
18627
+ const formatter = resolveNumberFormatter6(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
17784
18628
  if (formatter) {
17785
18629
  return formatter(value);
17786
18630
  }
17787
18631
  }
17788
18632
  if (isNumericValue(value)) {
17789
- return defaultNumberFormatter11({ surface: "table" })(value);
18633
+ return defaultNumberFormatter10({ surface: "table" })(value);
17790
18634
  }
17791
18635
  return String(value);
17792
18636
  }
@@ -18075,13 +18919,13 @@ function estimateColumnWidth(col, data, fontSize, paddingX) {
18075
18919
  if (col.image) return (col.image.width ?? 24) + paddingX;
18076
18920
  if (col.flag) return 60;
18077
18921
  const label = col.label ?? col.key;
18078
- const headerWidth = estimateTextWidth23(label, fontSize, 600) + paddingX;
18922
+ const headerWidth = estimateTextWidth24(label, fontSize, 600) + paddingX;
18079
18923
  const sampleSize = Math.min(100, data.length);
18080
18924
  let maxDataWidth = 0;
18081
18925
  for (let i = 0; i < sampleSize; i++) {
18082
18926
  const val = data[i][col.key];
18083
18927
  const text = val == null ? "" : String(val);
18084
- const width = estimateTextWidth23(text, fontSize, 400) + paddingX;
18928
+ const width = estimateTextWidth24(text, fontSize, 400) + paddingX;
18085
18929
  if (width > maxDataWidth) maxDataWidth = width;
18086
18930
  }
18087
18931
  return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
@@ -18445,12 +19289,13 @@ function compileTableLayout(spec, options, theme) {
18445
19289
  }
18446
19290
 
18447
19291
  // src/tilemap/compile-tilemap.ts
19292
+ import { mergeThemeConfig as mergeThemeConfig5 } from "@opendata-ai/openchart-core";
18448
19293
  import {
18449
19294
  adaptTheme as adaptTheme5,
18450
19295
  CATEGORICAL_PALETTE as CATEGORICAL_PALETTE3,
18451
19296
  computeChrome as computeChrome8,
18452
- estimateTextWidth as estimateTextWidth24,
18453
- getBreakpoint as getBreakpoint2,
19297
+ estimateTextWidth as estimateTextWidth25,
19298
+ getBreakpoint as getBreakpoint3,
18454
19299
  HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION4,
18455
19300
  HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
18456
19301
  pickLabelColor as pickLabelColor4,
@@ -18496,7 +19341,8 @@ function compileTileMap(spec, options) {
18496
19341
  const tilemapSpec = normalized;
18497
19342
  const rawWatermark = spec.watermark;
18498
19343
  const watermark = rawWatermark !== void 0 ? tilemapSpec.watermark : options.watermark ?? true;
18499
- const mergedThemeConfig = options.theme ? { ...tilemapSpec.theme, ...options.theme } : tilemapSpec.theme;
19344
+ emitThemeWarnings(options.theme, options.onWarn);
19345
+ const mergedThemeConfig = mergeThemeConfig5(tilemapSpec.theme, options.theme);
18500
19346
  const lightTheme = resolveTheme6(mergedThemeConfig);
18501
19347
  let theme = lightTheme;
18502
19348
  if (options.darkMode) {
@@ -18518,7 +19364,7 @@ function compileTileMap(spec, options) {
18518
19364
  void 0,
18519
19365
  watermark
18520
19366
  );
18521
- const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
19367
+ const padding = getBreakpoint3(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
18522
19368
  const chromeLayout = resolveChromeLayout(
18523
19369
  spec,
18524
19370
  options
@@ -18631,7 +19477,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
18631
19477
  });
18632
19478
  const hasEmptyTiles = tiles.some((t) => !t.hasData);
18633
19479
  const noDataLabel = "No data";
18634
- const noDataLabelWidth = estimateTextWidth24(noDataLabel, 11);
19480
+ const noDataLabelWidth = estimateTextWidth25(noDataLabel, 11);
18635
19481
  const noDataBlockWidth2 = hasEmptyTiles ? NO_DATA_BAR_GAP + legendBarHeight + NO_DATA_LABEL_GAP2 + noDataLabelWidth : 0;
18636
19482
  const barWidth = Math.max(0, legendWidth - noDataBlockWidth2);
18637
19483
  const noDataX = legendX + barWidth + NO_DATA_BAR_GAP;
@@ -18688,7 +19534,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
18688
19534
  height: contentHeight,
18689
19535
  animation: resolvedAnimation,
18690
19536
  watermark,
18691
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
19537
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth25(text, fontSize), height: fontSize }))
18692
19538
  };
18693
19539
  }
18694
19540
  function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
@@ -18829,7 +19675,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
18829
19675
  height: contentHeight,
18830
19676
  animation: resolvedAnimation,
18831
19677
  watermark,
18832
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
19678
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth25(text, fontSize), height: fontSize }))
18833
19679
  };
18834
19680
  }
18835
19681
  function buildTileMark(opts) {
@@ -18955,7 +19801,7 @@ function countLegendRows(labels, availableWidth, fontSize) {
18955
19801
  let rows = 1;
18956
19802
  let offsetX = 0;
18957
19803
  for (let i = 0; i < labels.length; i++) {
18958
- const labelWidth = estimateTextWidth24(labels[i], fontSize);
19804
+ const labelWidth = estimateTextWidth25(labels[i], fontSize);
18959
19805
  const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
18960
19806
  if (i > 0 && offsetX + entryWidth > availableWidth) {
18961
19807
  rows++;
@@ -18999,14 +19845,14 @@ function emptyLayout5(chrome, theme, options, watermark) {
18999
19845
  height: options.height,
19000
19846
  watermark,
19001
19847
  animation: void 0,
19002
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
19848
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth25(text, fontSize), height: fontSize }))
19003
19849
  };
19004
19850
  }
19005
19851
 
19006
19852
  // src/tooltips/compute.ts
19007
19853
  import {
19008
19854
  buildTemporalFormatter as buildTemporalFormatter2,
19009
- defaultNumberFormatter as defaultNumberFormatter12,
19855
+ defaultNumberFormatter as defaultNumberFormatter11,
19010
19856
  formatDate as formatDate4,
19011
19857
  formatPercent as formatPercent4,
19012
19858
  getRepresentativeColor as getRepresentativeColor14
@@ -19020,7 +19866,7 @@ function formatValue2(value, fieldType, formatter) {
19020
19866
  }
19021
19867
  if (typeof value === "number") {
19022
19868
  if (formatter) return formatter(value);
19023
- return defaultNumberFormatter12()(value);
19869
+ return defaultNumberFormatter11()(value);
19024
19870
  }
19025
19871
  return String(value);
19026
19872
  }
@@ -19342,7 +20188,7 @@ function computeRangeTooltips(spec, marks) {
19342
20188
  const endVal = Number(row[endCh.field]);
19343
20189
  if (Number.isFinite(startVal) && Number.isFinite(endVal)) {
19344
20190
  const delta = endVal - startVal;
19345
- const formatted = startFmt ? startFmt(delta) : defaultNumberFormatter12()(delta);
20191
+ const formatted = startFmt ? startFmt(delta) : defaultNumberFormatter11()(delta);
19346
20192
  fields.push({ label: "Change", value: delta > 0 ? `+${formatted}` : formatted });
19347
20193
  }
19348
20194
  return { title, fields };
@@ -19470,8 +20316,8 @@ function computeBinnedBarTooltips(spec, marks) {
19470
20316
  const title = `${start} \u2013 ${end}`;
19471
20317
  if (encoding.tooltip) {
19472
20318
  const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
20319
+ if (channels.length === 0) continue;
19473
20320
  descriptors.set(`rect-${i}`, {
19474
- title,
19475
20321
  fields: buildExplicitTooltipFields(row, channels, fmtCache)
19476
20322
  });
19477
20323
  continue;
@@ -20198,7 +21044,7 @@ function applySpecSize(spec, options) {
20198
21044
  return {
20199
21045
  ...options,
20200
21046
  width: typeof sized.width === "number" ? sized.width : options.width,
20201
- height: typeof sized.height === "number" ? sized.height : options.height
21047
+ height: typeof sized.height === "number" && !(options?.facetHeightGrown && (sized.chromeLayout ?? options.chromeLayout) === "grow") ? sized.height : options.height
20202
21048
  };
20203
21049
  }
20204
21050
  var MAX_FOOTNOTE_PASSES = 3;
@@ -20262,10 +21108,16 @@ function resolveScaleInvert(resolved) {
20262
21108
  return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
20263
21109
  }
20264
21110
  function compileChart(spec, optionsInput) {
21111
+ emitSpecWarnings(collectMarkOptionWarnings(spec), optionsInput?.onWarn);
21112
+ return compileChartInternal(spec, optionsInput);
21113
+ }
21114
+ function compileChartInternal(spec, optionsInput) {
20265
21115
  const sugarWarnings = [];
20266
- const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
21116
+ const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings, false) : spec;
20267
21117
  const options = applySpecSize(expandedSpec, optionsInput);
20268
- const { spec: normalized, warnings: normalizeWarnings } = compile(expandedSpec);
21118
+ const { spec: normalized, warnings: normalizeWarnings } = compile(expandedSpec, {
21119
+ markOptionWarnings: false
21120
+ });
20269
21121
  emitSpecWarnings([...sugarWarnings, ...normalizeWarnings], options.onWarn);
20270
21122
  if ("type" in normalized && normalized.type === "table") {
20271
21123
  throw new Error("compileChart received a table spec. Use compileTable instead.");
@@ -20286,9 +21138,9 @@ function compileChart(spec, optionsInput) {
20286
21138
  if (rawTransforms && rawTransforms.length > 0) {
20287
21139
  chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
20288
21140
  }
20289
- const breakpoint = getBreakpoint3(options.width);
20290
- const heightClass = getHeightClass3(options.height);
20291
- let strategy = getLayoutStrategy(breakpoint, heightClass);
21141
+ const breakpoint = getBreakpoint4(options.width);
21142
+ const heightClass = getHeightClass4(options.height);
21143
+ let strategy = getLayoutStrategy2(breakpoint, heightClass);
20292
21144
  const economy = resolveChromeEconomy(options.width, options.height);
20293
21145
  const rawSpec = expandedSpec;
20294
21146
  const overrides = rawSpec.overrides;
@@ -20435,11 +21287,26 @@ function compileChart(spec, optionsInput) {
20435
21287
  watermark = false;
20436
21288
  chartSpec = { ...chartSpec, watermark: false };
20437
21289
  }
20438
- const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
21290
+ emitThemeWarnings(options.theme, options.onWarn);
21291
+ const mergedThemeConfig = mergeThemeConfig6(chartSpec.theme, options.theme);
20439
21292
  let theme = resolveTheme7(mergedThemeConfig);
20440
21293
  if (options.darkMode) {
20441
21294
  theme = adaptTheme6(theme);
20442
21295
  }
21296
+ const noteCapacity = chartSpec.autoThin ? annotationNoteCapacity(
21297
+ chartSpec.annotations,
21298
+ options.width,
21299
+ theme,
21300
+ options.measureText,
21301
+ options.width < 400 && options.height >= 200 && chartSpec.compactAnnotationLimit != null
21302
+ ) : 0;
21303
+ const stableNoteReserve = computeNoteBandHeight2(noteCapacity, theme);
21304
+ if (!options.frozenChartArea && stableNoteReserve > (options.footnoteReserve ?? 0)) {
21305
+ return compileChartInternal(spec, {
21306
+ ...optionsInput,
21307
+ footnoteReserve: stableNoteReserve
21308
+ });
21309
+ }
20443
21310
  if (chartSpec.display === "sparkline") {
20444
21311
  chartSpec = applySparklineDefaults(chartSpec, theme);
20445
21312
  }
@@ -20492,20 +21359,23 @@ function compileChart(spec, optionsInput) {
20492
21359
  }
20493
21360
  const facetChannel = renderSpec.encoding.facet;
20494
21361
  if (facetChannel?.field) {
20495
- return compileFaceted(
21362
+ const layout2 = compileFaceted(
20496
21363
  chartSpec,
20497
21364
  renderSpec,
20498
21365
  facetChannel,
20499
21366
  facetDirection,
20500
- options,
21367
+ { ...options, onDiagnostic: void 0 },
20501
21368
  theme,
20502
21369
  strategy,
20503
21370
  watermark,
20504
21371
  resolvedAnimation,
20505
21372
  crosshair,
20506
21373
  spec,
20507
- optionsInput
21374
+ { ...optionsInput, onDiagnostic: void 0 }
20508
21375
  );
21376
+ const diagnostic = noteCapacityDiagnostic(layout2);
21377
+ if (diagnostic) options.onDiagnostic?.(diagnostic);
21378
+ return layout2;
20509
21379
  }
20510
21380
  const measure = createMeasureFn(options.measureText);
20511
21381
  const plan = resolveLayoutPlan(
@@ -20526,16 +21396,6 @@ function compileChart(spec, optionsInput) {
20526
21396
  );
20527
21397
  const dims = computeDimensions(chartSpec, options, legendStub, theme, strategy, watermark, plan);
20528
21398
  const chartArea = options.frozenChartArea ?? dims.chartArea;
20529
- const finalLegend = placeLegend(
20530
- plan.legendContent,
20531
- chartArea,
20532
- options.width,
20533
- theme,
20534
- plan.xAxisExtent,
20535
- dims.effectiveAxisGap
20536
- );
20537
- const sizeLegend = plan.sizeLegendContent ? placeSizeLegend(plan.sizeLegendContent, chartArea, theme, finalLegend) : null;
20538
- const allLegends = [finalLegend, ...sizeLegend ? [sizeLegend] : []];
20539
21399
  let scaleArea = chartArea;
20540
21400
  if (plan.inlineYLabelInset > 0 && plan.inlineYLabelInset < chartArea.width) {
20541
21401
  scaleArea = {
@@ -20599,6 +21459,12 @@ function compileChart(spec, optionsInput) {
20599
21459
  );
20600
21460
  const renderer = getChartRenderer(rendererKey);
20601
21461
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
21462
+ if (marks.some((mark) => mark.type === "line" && mark.label?.visible)) {
21463
+ emitSpecWarnings(
21464
+ ["[openchart] Legacy end-of-line labels are deprecated; use endpointLabels."],
21465
+ options.onWarn
21466
+ );
21467
+ }
20602
21468
  applyFillPatterns(marks, chartSpec.markDef, theme);
20603
21469
  const markRenderModeWarnings = [];
20604
21470
  const markRenderMode = resolveMarkRenderMode(
@@ -20623,6 +21489,18 @@ function compileChart(spec, optionsInput) {
20623
21489
  strategy,
20624
21490
  plan.endpointLabelsDemoted
20625
21491
  );
21492
+ const finalLegend = placeLegend(
21493
+ plan.legendContent,
21494
+ // Dimensions reserve both columns; place the legend after the resolved
21495
+ // endpoint column instead of drawing both at the plot's right edge.
21496
+ (plan.legendContent.position === "right" || plan.legendContent.position === "bottom-right") && endpointLabels.entries.length > 0 ? { ...chartArea, width: chartArea.width + ENDPOINT_COLUMN_GAP + endpointLabels.bounds.width } : chartArea,
21497
+ options.width,
21498
+ theme,
21499
+ plan.xAxisExtent,
21500
+ dims.effectiveAxisGap
21501
+ );
21502
+ const sizeLegend = plan.sizeLegendContent ? placeSizeLegend(plan.sizeLegendContent, chartArea, theme, finalLegend) : null;
21503
+ const allLegends = [finalLegend, ...sizeLegend ? [sizeLegend] : []];
20626
21504
  const obstacles = [];
20627
21505
  if (finalLegend.bounds.width > 0) {
20628
21506
  obstacles.push({ ...finalLegend.bounds, kind: "legend" });
@@ -20658,6 +21536,8 @@ function compileChart(spec, optionsInput) {
20658
21536
  svg: { width: dims.total.width, height: dims.total.height },
20659
21537
  measure: annotationMeasure,
20660
21538
  fontFamily: theme.fonts.family,
21539
+ colors: theme.colors,
21540
+ onWarn: (message) => emitSpecWarnings([message], options.onWarn),
20661
21541
  autoThin: chartSpec.autoThin,
20662
21542
  // Hand-placed blocks skip every nudge pass, so the clamp is the only lever
20663
21543
  // that keeps an author's offset from parking a callout on the title.
@@ -20675,24 +21555,28 @@ function compileChart(spec, optionsInput) {
20675
21555
  );
20676
21556
  }
20677
21557
  let footnotes;
20678
- if (chartSpec.autoThin && annotations.length > 1) {
21558
+ if (chartSpec.autoThin && (annotations.length > 1 || chartSpec.compactAnnotationLimit != null)) {
20679
21559
  const result = thinAnnotations(
20680
21560
  annotations,
20681
21561
  chartSpec.annotations,
20682
21562
  annotationMeasure,
20683
21563
  chartArea,
20684
- { x: 0, y: 0, width: dims.total.width, height: dims.total.height }
21564
+ { x: 0, y: 0, width: dims.total.width, height: dims.total.height },
21565
+ options.width < 400 && options.height >= 200 ? chartSpec.compactAnnotationLimit : void 0
20685
21566
  );
20686
21567
  annotations = result.annotations;
20687
21568
  if (result.footnotes.length > 0) {
20688
- footnotes = result.footnotes;
20689
- const reserve = footnoteBandHeight(footnotes.length, theme);
21569
+ footnotes = wrapAnnotationNotes(
21570
+ result.footnotes,
21571
+ dims.total.width,
21572
+ theme,
21573
+ options.measureText
21574
+ );
21575
+ const reserve = computeNoteBandHeight2(annotationNoteLineCount(footnotes), theme);
20690
21576
  const currentReserve = options.footnoteReserve ?? 0;
20691
21577
  if (!options.frozenChartArea && reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
20692
- const grow = (spec.chromeLayout ?? options.chromeLayout) === "grow";
20693
- return compileChart(spec, {
21578
+ return compileChartInternal(spec, {
20694
21579
  ...optionsInput,
20695
- height: optionsInput.height + (grow ? reserve - currentReserve : 0),
20696
21580
  footnoteReserve: reserve,
20697
21581
  footnotePass: (options.footnotePass ?? 0) + 1
20698
21582
  });
@@ -20705,7 +21589,7 @@ function compileChart(spec, optionsInput) {
20705
21589
  (entry) => ({
20706
21590
  key: entry.seriesKey,
20707
21591
  text: `${entry.seriesKey} \xB7 ${entry.value}`,
20708
- lines: wrapText3(
21592
+ lines: wrapText4(
20709
21593
  `${entry.seriesKey} \xB7 ${entry.value}`,
20710
21594
  theme.fonts.sizes.small,
20711
21595
  500,
@@ -20729,16 +21613,14 @@ function compileChart(spec, optionsInput) {
20729
21613
  })
20730
21614
  ) : [];
20731
21615
  if (seriesSummary.length) {
20732
- const reserve = footnoteBandHeight(
20733
- (footnotes?.length ?? 0) + seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
21616
+ const reserve = computeNoteBandHeight2(
21617
+ Math.max(noteCapacity, annotationNoteLineCount(footnotes ?? [])) + seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
20734
21618
  theme
20735
21619
  );
20736
21620
  const previousReserve = options.footnoteReserve ?? 0;
20737
21621
  if (!options.frozenChartArea && reserve > previousReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
20738
- const grow = (spec.chromeLayout ?? options.chromeLayout) === "grow";
20739
- return compileChart(spec, {
21622
+ return compileChartInternal(spec, {
20740
21623
  ...optionsInput,
20741
- height: optionsInput.height + (grow ? reserve - previousReserve : 0),
20742
21624
  footnoteReserve: reserve,
20743
21625
  footnotePass: (options.footnotePass ?? 0) + 1
20744
21626
  });
@@ -20763,8 +21645,16 @@ function compileChart(spec, optionsInput) {
20763
21645
  chartSpec.data
20764
21646
  );
20765
21647
  assignAnimationIndices(marks, resolvedAnimation);
21648
+ const noteBandHeight = Math.max(
21649
+ options.footnoteReserve ?? 0,
21650
+ computeNoteBandHeight2(
21651
+ Math.max(noteCapacity, annotationNoteLineCount(footnotes ?? [])) + seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
21652
+ theme
21653
+ )
21654
+ );
20766
21655
  const chrome = {
20767
21656
  ...dims.chrome,
21657
+ ...noteBandHeight > 0 ? { noteBandHeight } : {},
20768
21658
  ...footnotes ? { footnotes } : {},
20769
21659
  ...seriesSummary.length ? { seriesSummary } : {}
20770
21660
  };
@@ -20859,7 +21749,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
20859
21749
  theme,
20860
21750
  strategy,
20861
21751
  watermark,
20862
- plan
21752
+ plan,
21753
+ false
20863
21754
  );
20864
21755
  const chartArea = dims.chartArea;
20865
21756
  const finalLegend = placeLegend(
@@ -20872,19 +21763,35 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
20872
21763
  );
20873
21764
  const bottomReservation = plan.xAxisExtent;
20874
21765
  const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
20875
- const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
20876
- left: 0,
20877
- bottom: bottomReservation
20878
- });
21766
+ const facetAnnotationMeasure = options.measureText ? (text, font) => options.measureText(text, font.fontSize, font.fontWeight).width : heuristicMeasure;
21767
+ const hasOutsideReferences = chartSpec.annotations.some(
21768
+ (a) => a.type === "refline" && a.labelPlacement === "outside" && !!a.label
21769
+ );
21770
+ const referenceInsets = hasOutsideReferences ? {
21771
+ measure: (width) => referenceLabelReserve(chartSpec.annotations, width, facetAnnotationMeasure),
21772
+ innerAxisWidth: (chartSpec.resolve?.scale?.y ?? (facetDirection === "row" ? "independent" : "shared")) === "independent" ? plan.leftGutter : 0
21773
+ } : void 0;
21774
+ const grid = computeFacetGrid(
21775
+ facetValues,
21776
+ effectiveColumns,
21777
+ chartArea,
21778
+ {
21779
+ left: 0,
21780
+ bottom: bottomReservation
21781
+ },
21782
+ referenceInsets
21783
+ );
20879
21784
  if (!optionsInput.facetHeightGrown && grid.panels.length > 0 && grid.panels[0].area.height < MIN_PANEL_HEIGHT) {
20880
- const neededChartHeight = facetMinHeight(facetValues.length, grid.columns);
21785
+ const neededChartHeight = grid.minimumHeight ?? facetMinHeight(facetValues.length, grid.columns);
20881
21786
  const heightGrowth = neededChartHeight - chartArea.height;
20882
21787
  if (heightGrowth > 0) {
20883
- return compileChart(rawSpec, {
21788
+ const grown = compileChartInternal(rawSpec, {
20884
21789
  ...optionsInput,
20885
21790
  height: options.height + heightGrowth,
20886
21791
  facetHeightGrown: true
20887
21792
  });
21793
+ if (grown.chrome.grownHeight !== void 0) grown.chrome.grownHeight += heightGrowth;
21794
+ return grown;
20888
21795
  }
20889
21796
  }
20890
21797
  const resolveConfig = chartSpec.resolve;
@@ -20948,7 +21855,9 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
20948
21855
  data: panelData,
20949
21856
  encoding: panelSpecWithDomains.encoding,
20950
21857
  markType: chartSpec.markType,
20951
- totalWidth: gridPanel.area.width
21858
+ // Outside-label cells use figure coordinates throughout, including
21859
+ // the tick-label clamp. Keep the legacy path unchanged in 8.x.
21860
+ totalWidth: gridPanel.referenceContainer ? gridPanel.area.x + gridPanel.area.width : gridPanel.area.width
20952
21861
  });
20953
21862
  if (!isRadial) {
20954
21863
  computeGridlines(panelAxes, gridPanel.area);
@@ -20984,22 +21893,31 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
20984
21893
  let panelAnnotations = computeAnnotations(panelSpecWithDomains, {
20985
21894
  scales: panelScales,
20986
21895
  chartArea: gridPanel.area,
21896
+ referenceContainer: gridPanel.referenceContainer,
21897
+ referenceReserve: gridPanel.referenceContainer ? referenceLabelReserve(
21898
+ chartSpec.annotations,
21899
+ gridPanel.referenceContainer.width,
21900
+ facetAnnotationMeasure
21901
+ ) : void 0,
20987
21902
  strategy,
20988
21903
  isDark: theme.isDark,
20989
21904
  obstacles: [],
20990
- svg: { width: options.width, height: options.height },
21905
+ svg: { width: dims.total.width, height: dims.total.height },
20991
21906
  measure: annotationMeasure,
20992
21907
  fontFamily: theme.fonts.family,
21908
+ colors: theme.colors,
21909
+ onWarn: (message) => emitSpecWarnings([message], options.onWarn),
20993
21910
  autoThin: panelSpecWithDomains.autoThin,
20994
21911
  topBound: dims.chrome.topHeight > 0 ? dims.topPad + dims.chrome.topHeight : void 0
20995
21912
  });
20996
- if (chartSpec.autoThin && panelAnnotations.length > 1) {
21913
+ if (chartSpec.autoThin && (panelAnnotations.length > 1 || chartSpec.compactAnnotationLimit != null)) {
20997
21914
  const thinned = thinAnnotations(
20998
21915
  panelAnnotations,
20999
21916
  chartSpec.annotations,
21000
21917
  annotationMeasure,
21001
21918
  gridPanel.area,
21002
- gridPanel.area
21919
+ gridPanel.area,
21920
+ options.width < 400 && options.height >= 200 ? chartSpec.compactAnnotationLimit : void 0
21003
21921
  );
21004
21922
  panelAnnotations = thinned.annotations;
21005
21923
  for (const a of panelAnnotations) {
@@ -21010,6 +21928,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
21010
21928
  num = allFootnotes.length + 1;
21011
21929
  footnoteNumberBySpecIndex.set(a.specIndex, num);
21012
21930
  allFootnotes.push({
21931
+ specIndex: a.specIndex,
21932
+ ...a.id !== void 0 ? { id: a.id } : {},
21013
21933
  index: num,
21014
21934
  text: thinned.footnotes[panelIndex - 1]?.text ?? ""
21015
21935
  });
@@ -21034,11 +21954,17 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
21034
21954
  }
21035
21955
  });
21036
21956
  }
21957
+ const wrappedFootnotes = wrapAnnotationNotes(
21958
+ allFootnotes,
21959
+ dims.total.width,
21960
+ theme,
21961
+ options.measureText
21962
+ );
21037
21963
  if (allFootnotes.length > 0 && !options.frozenChartArea) {
21038
- const reserve = footnoteBandHeight(allFootnotes.length, theme);
21964
+ const reserve = computeNoteBandHeight2(annotationNoteLineCount(wrappedFootnotes), theme);
21039
21965
  const currentReserve = options.footnoteReserve ?? 0;
21040
21966
  if (reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
21041
- return compileChart(rawSpec, {
21967
+ return compileChartInternal(rawSpec, {
21042
21968
  ...optionsInput,
21043
21969
  footnoteReserve: reserve,
21044
21970
  footnotePass: (options.footnotePass ?? 0) + 1
@@ -21046,6 +21972,12 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
21046
21972
  }
21047
21973
  }
21048
21974
  const tooltipDescriptors = computeTooltipDescriptors(panelChartSpec, allMarks);
21975
+ if (allMarks.some((mark) => mark.type === "line" && mark.label?.visible)) {
21976
+ emitSpecWarnings(
21977
+ ["[openchart] Legacy end-of-line labels are deprecated; use endpointLabels."],
21978
+ options.onWarn
21979
+ );
21980
+ }
21049
21981
  applyFillPatterns(allMarks, chartSpec.markDef, theme);
21050
21982
  const panelCount = facetValues.length;
21051
21983
  const baseAltText = generateAltText(
@@ -21082,7 +22014,14 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
21082
22014
  emitSpecWarnings(facetRenderModeWarnings, options.onWarn);
21083
22015
  return {
21084
22016
  area: chartArea,
21085
- chrome: allFootnotes.length > 0 ? { ...dims.chrome, footnotes: allFootnotes } : dims.chrome,
22017
+ chrome: {
22018
+ ...dims.chrome,
22019
+ noteBandHeight: Math.max(
22020
+ options.footnoteReserve ?? 0,
22021
+ computeNoteBandHeight2(annotationNoteLineCount(wrappedFootnotes), theme)
22022
+ ),
22023
+ ...allFootnotes.length > 0 ? { footnotes: wrappedFootnotes } : {}
22024
+ },
21086
22025
  metrics: dims.metrics,
21087
22026
  ...dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {},
21088
22027
  axes: { x: void 0, y: void 0 },
@@ -21102,7 +22041,7 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
21102
22041
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
21103
22042
  },
21104
22043
  theme,
21105
- dimensions: { width: options.width, height: options.height },
22044
+ dimensions: { width: dims.total.width, height: dims.total.height },
21106
22045
  animation: resolvedAnimation,
21107
22046
  watermark,
21108
22047
  display: chartSpec.display,
@@ -21125,7 +22064,19 @@ function compileLayer2(spec, options) {
21125
22064
  );
21126
22065
  emitSpecWarnings(sugarWarnings, options.onWarn);
21127
22066
  const resolvedOptions = applySpecSize(expanded, options);
21128
- return compileLayer(expanded, resolvedOptions, compileChart);
22067
+ const layout = compileLayer(
22068
+ expanded,
22069
+ {
22070
+ ...resolvedOptions,
22071
+ onDiagnostic: options.onDiagnostic ? (diagnostic2) => {
22072
+ if (diagnostic2.code !== "layout.note-capacity") options.onDiagnostic(diagnostic2);
22073
+ } : void 0
22074
+ },
22075
+ compileChartInternal
22076
+ );
22077
+ const diagnostic = noteCapacityDiagnostic(layout);
22078
+ if (diagnostic) options.onDiagnostic?.(diagnostic);
22079
+ return layout;
21129
22080
  }
21130
22081
  function compileTable(spec, options) {
21131
22082
  const { spec: normalized, warnings: normalizeWarnings } = compile(spec);
@@ -21135,7 +22086,8 @@ function compileTable(spec, options) {
21135
22086
  throw new Error(`compileTable received a non-table spec. Use compileChart instead.`);
21136
22087
  }
21137
22088
  const tableSpec = normalized;
21138
- const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
22089
+ emitThemeWarnings(options.theme, options.onWarn);
22090
+ const mergedThemeConfig = mergeThemeConfig6(tableSpec.theme, options.theme);
21139
22091
  let theme = resolveTheme7(mergedThemeConfig);
21140
22092
  if (options.darkMode) {
21141
22093
  theme = adaptTheme6(theme);
@@ -21160,6 +22112,58 @@ function compileBarList2(spec, options) {
21160
22112
  return compileBarList(spec, options);
21161
22113
  }
21162
22114
 
22115
+ // src/annotations/interpolate.ts
22116
+ function interpolateAnnotationGeometry(before, after, t) {
22117
+ if (t >= 1) return structuredClone(after);
22118
+ const result = structuredClone(after);
22119
+ const point = (a, b) => ({
22120
+ x: a.x + (b.x - a.x) * t,
22121
+ y: a.y + (b.y - a.y) * t
22122
+ });
22123
+ if (before.label && result.label) {
22124
+ const pos = point(before.label, result.label);
22125
+ const dx = pos.x - result.label.x;
22126
+ const dy = pos.y - result.label.y;
22127
+ result.label.x = pos.x;
22128
+ result.label.y = pos.y;
22129
+ if (result.label.bounds) {
22130
+ result.label.bounds.x += dx;
22131
+ result.label.bounds.y += dy;
22132
+ }
22133
+ if (result.bounds) {
22134
+ result.bounds.x += dx;
22135
+ result.bounds.y += dy;
22136
+ }
22137
+ if (result.subtitle) {
22138
+ result.subtitle.x += dx;
22139
+ result.subtitle.y += dy;
22140
+ }
22141
+ }
22142
+ if (before.dot && result.dot) Object.assign(result.dot, point(before.dot, result.dot));
22143
+ if (before.line && result.line) {
22144
+ result.line.start = point(before.line.start, result.line.start);
22145
+ result.line.end = point(before.line.end, result.line.end);
22146
+ }
22147
+ if (before.rect && result.rect) {
22148
+ const rect = result.rect;
22149
+ Object.assign(rect, point(before.rect, rect));
22150
+ rect.width = before.rect.width + (rect.width - before.rect.width) * t;
22151
+ rect.height = before.rect.height + (rect.height - before.rect.height) * t;
22152
+ }
22153
+ const connector = result.label?.connector;
22154
+ if (connector?.endpoint && result.bounds) {
22155
+ const start = before.label?.connector?.endpoint ?? before.dot ?? connector.endpoint;
22156
+ connector.endpoint = point(start, connector.endpoint);
22157
+ result.label.connector = refreshConnector(
22158
+ connector,
22159
+ result.bounds,
22160
+ markerClearance(result.dot),
22161
+ true
22162
+ );
22163
+ }
22164
+ return result;
22165
+ }
22166
+
21163
22167
  // src/design/patch.ts
21164
22168
  var unsafe = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
21165
22169
  function applySpecPatch(spec, patch) {
@@ -21329,6 +22333,246 @@ function recommendCompositions(data, question, roles) {
21329
22333
  }
21330
22334
  return { recommendations: recommendations.slice(0, 3), missingRoles: [] };
21331
22335
  }
22336
+
22337
+ // src/migration/v8.ts
22338
+ import { legacyEditorial } from "@opendata-ai/openchart-core";
22339
+ var node = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22340
+ var escapePointer = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1");
22341
+ function migrateSpecV8(input) {
22342
+ const repairs = [];
22343
+ const clone = (value) => Array.isArray(value) ? value.map(clone) : node(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) ? Object.fromEntries(
22344
+ Object.entries(value).map(([key, child]) => [key, clone(child)])
22345
+ ) : value;
22346
+ const spec = clone(input);
22347
+ function visit(current, path, inherited = {}) {
22348
+ function replace(owner, key, value, at, reason) {
22349
+ repairs.push({
22350
+ path: `${at}/${escapePointer(key)}`,
22351
+ from: clone(owner[key]),
22352
+ to: clone(value),
22353
+ reason
22354
+ });
22355
+ if (value === void 0) delete owner[key];
22356
+ else owner[key] = value;
22357
+ }
22358
+ const rewrite = (key, value, reason) => replace(current, key, value, path, reason);
22359
+ if ("darkMode" in current)
22360
+ rewrite("darkMode", void 0, "Spec darkMode has no effect; use the mount option.");
22361
+ if (current.type === "table" && "compact" in current) {
22362
+ if (current.density === void 0)
22363
+ rewrite("density", current.compact ? "condensed" : "regular", "Use table density.");
22364
+ rewrite("compact", void 0, "Replaced by density.");
22365
+ }
22366
+ const theme = node(current.theme);
22367
+ const chrome = node(theme?.chrome);
22368
+ if (chrome)
22369
+ for (const [key, value] of Object.entries(chrome)) {
22370
+ if (typeof value === "string")
22371
+ replace(
22372
+ chrome,
22373
+ key,
22374
+ { color: value },
22375
+ `${path}/theme/chrome`,
22376
+ "Use a chrome color object."
22377
+ );
22378
+ }
22379
+ if (current.animation === "legacy")
22380
+ rewrite("animation", true, "Omitting the preset retains v8 animation.");
22381
+ const animation = node(current.animation);
22382
+ if (animation?.preset === "legacy")
22383
+ replace(animation, "preset", void 0, `${path}/animation`, "Omit the legacy preset.");
22384
+ const mark = node(current.mark);
22385
+ if (mark && "render" in mark)
22386
+ replace(mark, "render", void 0, `${path}/mark`, "Use the renderer mount option.");
22387
+ const markType = typeof current.mark === "string" ? current.mark : mark?.type;
22388
+ const encoding = node(current.encoding);
22389
+ if (encoding) {
22390
+ for (const key of ["radius", "shape", "href", "order"]) {
22391
+ if (key in encoding)
22392
+ replace(
22393
+ encoding,
22394
+ key,
22395
+ void 0,
22396
+ `${path}/encoding`,
22397
+ "This channel has no rendering effect."
22398
+ );
22399
+ }
22400
+ if (["arc", "waffle", "parliament"].includes(String(markType)) && "y" in encoding) {
22401
+ if (encoding.theta === void 0)
22402
+ replace(
22403
+ encoding,
22404
+ "theta",
22405
+ encoding.y,
22406
+ `${path}/encoding`,
22407
+ "Use theta for angular values."
22408
+ );
22409
+ replace(encoding, "y", void 0, `${path}/encoding`, "Replaced by theta.");
22410
+ }
22411
+ }
22412
+ if (["sankey", "map", "barlist"].includes(String(current.type)) && typeof current.valueFormat === "string") {
22413
+ const key = current.type === "map" ? "color" : "value";
22414
+ const enc = node(current.encoding) ?? {};
22415
+ const channel = node(enc[key]);
22416
+ if (channel) {
22417
+ if (channel.format === void 0)
22418
+ replace(
22419
+ channel,
22420
+ "format",
22421
+ current.valueFormat,
22422
+ `${path}/encoding/${key}`,
22423
+ "Use channel format."
22424
+ );
22425
+ rewrite("valueFormat", void 0, "Replaced by channel format.");
22426
+ }
22427
+ }
22428
+ if (Array.isArray(current.annotations))
22429
+ for (const [index, value] of current.annotations.entries()) {
22430
+ const annotation = node(value);
22431
+ if (annotation?.type === "rule")
22432
+ replace(annotation, "type", "refline", `${path}/annotations/${index}`, "Use refline.");
22433
+ }
22434
+ if (Array.isArray(current.annotations))
22435
+ for (const [index, value] of current.annotations.entries()) {
22436
+ const annotation = node(value);
22437
+ if (!annotation) continue;
22438
+ const at = `${path}/annotations/${index}`;
22439
+ const setDefault = (key, value2) => {
22440
+ if (annotation[key] === void 0)
22441
+ replace(annotation, key, value2, at, "Preserve the v8 annotation default.");
22442
+ };
22443
+ if (annotation.type === "text") {
22444
+ setDefault("maxWidth", null);
22445
+ if (annotation.subtitle) {
22446
+ setDefault("fontWeight", 700);
22447
+ setDefault(
22448
+ "subtitleFontSize",
22449
+ Math.round(
22450
+ (typeof annotation.fontSize === "number" ? annotation.fontSize : 13) * 0.85
22451
+ )
22452
+ );
22453
+ }
22454
+ if (annotation.connector === "curve")
22455
+ replace(
22456
+ annotation,
22457
+ "connector",
22458
+ { type: "curve", arrow: true },
22459
+ at,
22460
+ "Preserve the v8 curve arrow."
22461
+ );
22462
+ const connector = node(annotation.connector);
22463
+ if (connector?.type === "curve" && connector.arrow === void 0)
22464
+ replace(connector, "arrow", true, `${at}/connector`, "Preserve the v8 curve arrow.");
22465
+ }
22466
+ if (annotation.type === "refline" && annotation.label && annotation.labelAnchor === void 0 && annotation.labelPlacement === void 0) {
22467
+ setDefault("labelAnchor", annotation.y !== void 0 ? "top" : "left");
22468
+ }
22469
+ }
22470
+ const isChart = current.mark !== void 0 || Array.isArray(current.layer);
22471
+ const isLayer = Array.isArray(current.layer);
22472
+ if (isChart && !isLayer && current.chromeLayout === void 0)
22473
+ rewrite("chromeLayout", "subtract", "Preserve the v8 fixed figure budget.");
22474
+ if (isChart && (!isLayer || current.responsive !== void 0 || typeof current.width === "number" && typeof current.height === "number")) {
22475
+ const responsive = current.responsive;
22476
+ if (responsive !== false) {
22477
+ if (responsive === void 0 && typeof current.width === "number" && typeof current.height === "number")
22478
+ rewrite("responsive", false, "Preserve the fixed-size responsive opt-out.");
22479
+ else if (node(responsive)?.compactAnnotationLimit === void 0)
22480
+ rewrite(
22481
+ "responsive",
22482
+ { ...node(responsive), compactAnnotationLimit: null },
22483
+ "Preserve unlimited v8 compact annotations."
22484
+ );
22485
+ }
22486
+ }
22487
+ if (!isLayer && (isChart || current.type !== void 0) && inherited.theme === void 0 || current.theme !== void 0) {
22488
+ const oldTheme = { ...node(current.theme) };
22489
+ const fonts = node(oldTheme.fonts) ?? {};
22490
+ const sizes = node(fonts.sizes) ?? {};
22491
+ const chromeDefaults = node(oldTheme.chrome) ?? {};
22492
+ const newSizes = { ...sizes };
22493
+ for (const [key, value] of Object.entries(legacyEditorial.fonts.sizes)) {
22494
+ const matching = key === "small" ? ["source", "byline", "footer"] : [key];
22495
+ if (newSizes[key] === void 0 && !matching.some((name) => node(chromeDefaults[name])?.fontSize !== void 0))
22496
+ newSizes[key] = value;
22497
+ }
22498
+ const newChrome = { ...chromeDefaults };
22499
+ for (const key of ["byline", "source", "footer"]) {
22500
+ const entry = node(newChrome[key]) ?? {};
22501
+ if (entry.fontSize === void 0)
22502
+ newChrome[key] = { ...entry, fontSize: sizes.small ?? 11 };
22503
+ }
22504
+ const materialized = { ...oldTheme, fonts: { ...fonts, sizes: newSizes }, chrome: newChrome };
22505
+ if (JSON.stringify(materialized) !== JSON.stringify(current.theme))
22506
+ rewrite(
22507
+ "theme",
22508
+ materialized,
22509
+ "Preserve v8 typography without overriding authored chrome sizes."
22510
+ );
22511
+ }
22512
+ const data = current.data ?? inherited.data;
22513
+ const effectiveEncoding = { ...node(inherited.encoding), ...encoding };
22514
+ const labels = node(current.labels);
22515
+ const endpoint = current.endpointLabels ?? inherited.endpointLabels;
22516
+ let legacyLabels = false;
22517
+ const effective = {
22518
+ ...inherited,
22519
+ ...current,
22520
+ data: data ?? [],
22521
+ encoding: effectiveEncoding
22522
+ };
22523
+ const layerSpec = effective;
22524
+ const leaves = Array.isArray(current.layer) ? flattenLayers(layerSpec) : [];
22525
+ const migrationSources = leaves.length ? layerSpec.resolve?.scale?.y === "independent" ? leaves.map((leaf) => buildPrimarySpec([leaf], layerSpec)) : [buildPrimarySpec(leaves, layerSpec)] : [effective];
22526
+ legacyLabels = migrationSources.some((migrationSource) => {
22527
+ const sourceMark = typeof migrationSource.mark === "string" ? migrationSource.mark : node(migrationSource.mark)?.type;
22528
+ if (!["line", "area"].includes(String(sourceMark))) return false;
22529
+ const normalized = normalizeSpec(
22530
+ expandSpecSugar(migrationSource)
22531
+ );
22532
+ return resolveSuppression(normalized, {
22533
+ seriesCount: countColorSeries(normalized),
22534
+ labelsHiddenByStrategy: false,
22535
+ labelsDensityNone: normalized.labels.density === "none"
22536
+ }).showEndOfLineLabels;
22537
+ });
22538
+ if (legacyLabels)
22539
+ rewrite(
22540
+ "endpointLabels",
22541
+ { content: "label", showMarker: false },
22542
+ "Replace legacy end-of-line labels with the endpoint column; label positions may change."
22543
+ );
22544
+ const offsets = node(labels?.offsets);
22545
+ if (labels && offsets) {
22546
+ for (const key of Object.keys(offsets))
22547
+ replace(
22548
+ offsets,
22549
+ key,
22550
+ void 0,
22551
+ `${path}/labels/offsets`,
22552
+ "Manual legacy-label offset cannot be represented by endpoint labels."
22553
+ );
22554
+ delete labels.offsets;
22555
+ }
22556
+ if (Array.isArray(current.layer))
22557
+ current.layer.forEach((child, index) => {
22558
+ const childNode = node(child);
22559
+ if (childNode)
22560
+ visit(childNode, `${path}/layer/${index}`, {
22561
+ data,
22562
+ theme: current.theme ?? inherited.theme,
22563
+ responsive: current.responsive ?? inherited.responsive,
22564
+ chromeLayout: current.chromeLayout ?? inherited.chromeLayout,
22565
+ encoding: effectiveEncoding,
22566
+ endpointLabels: current.endpointLabels ?? endpoint,
22567
+ legend: current.legend ?? inherited.legend,
22568
+ labels: current.labels ?? inherited.labels
22569
+ });
22570
+ });
22571
+ }
22572
+ const root = node(spec);
22573
+ if (root) visit(root, "");
22574
+ return { spec, repairs };
22575
+ }
21332
22576
  export {
21333
22577
  ARROWHEAD_LENGTH,
21334
22578
  AUTO_CANVAS_THRESHOLD,
@@ -21340,6 +22584,7 @@ export {
21340
22584
  MAX_3D_NODES,
21341
22585
  UPDATE_DEFAULTS,
21342
22586
  applySpecPatch,
22587
+ automaticControlPoint,
21343
22588
  buildArcPath,
21344
22589
  buildAreaPath,
21345
22590
  buildEdgeTooltip,
@@ -21357,20 +22602,28 @@ export {
21357
22602
  compileTable,
21358
22603
  compileTileMap2 as compileTileMap,
21359
22604
  computeArrowheadPoints,
22605
+ createAnnotationEditContext,
21360
22606
  dedupeKeys,
21361
22607
  diagnoseChart,
21362
22608
  evaluatePredicate,
21363
22609
  facetMinHeight,
21364
22610
  getChartRenderer,
21365
22611
  heuristicMeasure,
22612
+ interpolateAnnotationGeometry,
22613
+ inverseLabelPlacement,
21366
22614
  isConditionalValueDef,
21367
22615
  legendGap,
21368
22616
  linkedEntityKeys,
22617
+ migrateSpecV8,
21369
22618
  normalizeSpec,
21370
22619
  observationKey,
21371
22620
  parseAnnotationSpans,
22621
+ previewAnnotationMove,
22622
+ previewConnectorBend,
22623
+ previewConnectorOffset,
21372
22624
  recommendCompositions,
21373
22625
  reconcileReaderSelection,
22626
+ refreshConnector,
21374
22627
  registerChartRenderer,
21375
22628
  resolveAnimation,
21376
22629
  resolveConditionalValue,