@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.d.ts +88 -33
- package/dist/index.js +1819 -566
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
225
|
+
computeNoteBandHeight as computeNoteBandHeight2,
|
|
209
226
|
generateAltText,
|
|
210
227
|
generateDataTable,
|
|
211
|
-
getBreakpoint as
|
|
212
|
-
getHeightClass as
|
|
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
|
|
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
|
|
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: {
|
|
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 =
|
|
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))
|
|
465
|
-
|
|
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 {
|
|
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
|
|
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)
|
|
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
|
|
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 = {
|
|
791
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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" &&
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
7952
|
+
const channelKey = type === "map" ? "color" : "value";
|
|
7953
|
+
const valueCh = encoding?.[channelKey];
|
|
7306
7954
|
if (!valueCh || typeof valueCh !== "object") {
|
|
7307
|
-
|
|
7308
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
7891
|
-
const density = normalizeTableDensity(spec
|
|
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
|
|
8995
|
-
if (!
|
|
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
|
|
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(
|
|
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
|
|
9100
|
-
if (
|
|
9101
|
-
for (const key of Object.keys(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
11820
|
+
const lines = wrapText3(text, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
|
|
11071
11821
|
for (const line4 of lines) {
|
|
11072
|
-
const w =
|
|
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 =
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
11979
|
-
const limit = Math.max(1,
|
|
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 =
|
|
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
|
|
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) =>
|
|
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 ?
|
|
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
|
|
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
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
14328
|
+
let layout0 = compileChart2(primary0, optionsWithReserve);
|
|
13567
14329
|
const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
|
|
13568
14330
|
primary1.annotations = [];
|
|
13569
|
-
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
15628
|
-
degrees.set(
|
|
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 = (
|
|
15691
|
-
const val = Number(
|
|
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 = (
|
|
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((
|
|
16715
|
+
return nodes.map((node2) => {
|
|
15730
16716
|
let radius = DEFAULT_NODE_RADIUS;
|
|
15731
16717
|
if (sizeScale && encoding.nodeSize?.field) {
|
|
15732
|
-
const val = Number(
|
|
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(
|
|
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(
|
|
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 =
|
|
16734
|
+
const labelVal = node2[encoding.nodeLabel.field];
|
|
15749
16735
|
label = labelVal != null ? String(labelVal) : void 0;
|
|
15750
16736
|
} else {
|
|
15751
|
-
label =
|
|
16737
|
+
label = node2.id;
|
|
15752
16738
|
}
|
|
15753
16739
|
let labelPriority;
|
|
15754
16740
|
if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
|
|
15755
|
-
const val = Number(
|
|
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(
|
|
16744
|
+
const degree = degrees.get(node2.id) ?? 0;
|
|
15759
16745
|
labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
15760
16746
|
}
|
|
15761
|
-
const { id: _id, ...rest } =
|
|
15762
|
-
const data = { id:
|
|
15763
|
-
const override = nodeOverrides?.[
|
|
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:
|
|
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
|
|
15875
|
-
const value =
|
|
15876
|
-
|
|
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
|
|
15884
|
-
if (
|
|
15885
|
-
colorMap.set(
|
|
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
|
|
15893
|
-
if (
|
|
15894
|
-
const communityColor = colorMap.get(
|
|
16878
|
+
for (const node2 of nodes) {
|
|
16879
|
+
if (node2.community != null) {
|
|
16880
|
+
const communityColor = colorMap.get(node2.community);
|
|
15895
16881
|
if (communityColor) {
|
|
15896
|
-
|
|
15897
|
-
|
|
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
|
|
15970
|
-
if (
|
|
15971
|
-
counts.set(
|
|
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
|
|
15985
|
-
const category = String(
|
|
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,
|
|
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
|
|
17024
|
+
for (const node2 of nodes) {
|
|
16039
17025
|
const fields = [];
|
|
16040
|
-
if (
|
|
17026
|
+
if (node2.community != null) {
|
|
16041
17027
|
fields.push({
|
|
16042
17028
|
label: "Community",
|
|
16043
|
-
value:
|
|
16044
|
-
color:
|
|
17029
|
+
value: node2.community,
|
|
17030
|
+
color: node2.fill
|
|
16045
17031
|
});
|
|
16046
17032
|
}
|
|
16047
|
-
for (const [key, value] of Object.entries(
|
|
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(
|
|
16056
|
-
title:
|
|
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
|
-
|
|
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
|
-
|
|
16270
|
-
|
|
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
|
|
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 :
|
|
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
|
|
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 =
|
|
16385
|
-
const isCrampedHeight =
|
|
16386
|
-
const chrome = plan
|
|
16387
|
-
|
|
16388
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
16647
|
-
const legendPos = plan
|
|
16648
|
-
const legendHeight = plan
|
|
16649
|
-
const legendBoundsWidth = plan
|
|
16650
|
-
const sizeLegend = plan
|
|
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
|
-
|
|
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 {
|
|
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
|
|
16886
|
-
estimateTextWidth as
|
|
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
|
|
16981
|
-
const category = nodeCategoryMap.get(
|
|
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(
|
|
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
|
|
17016
|
-
const depth =
|
|
17858
|
+
for (const node2 of nodes) {
|
|
17859
|
+
const depth = node2.depth ?? 0;
|
|
17017
17860
|
const group = byDepth.get(depth);
|
|
17018
|
-
if (group) group.push(
|
|
17019
|
-
else byDepth.set(depth, [
|
|
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
|
|
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(
|
|
17061
|
-
const depth =
|
|
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 =
|
|
17071
|
-
const x1 =
|
|
17072
|
-
const y0 =
|
|
17073
|
-
const y1 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
17264
|
-
const label = otherLabels.get(
|
|
17265
|
-
if (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 = (
|
|
17271
|
-
const name =
|
|
17272
|
-
const nameWidth =
|
|
17273
|
-
const valueWidth =
|
|
17274
|
-
formatFlowValue(
|
|
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
|
|
17286
|
-
const depth =
|
|
17287
|
-
const width = labelBlockWidth(
|
|
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 - ((
|
|
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 - (
|
|
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
|
|
17336
|
-
const d =
|
|
17337
|
-
const x0 =
|
|
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 = (
|
|
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 - ((
|
|
17347
|
-
const needed =
|
|
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((
|
|
17351
|
-
const fill = nodeColorMap.get(
|
|
17352
|
-
const depth =
|
|
17353
|
-
const merged = otherMembers.get(
|
|
17354
|
-
const valueText = formatFlowValue(
|
|
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:
|
|
17358
|
-
y:
|
|
17359
|
-
width: (
|
|
17360
|
-
height: (
|
|
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
|
-
|
|
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(
|
|
17376
|
-
nodeId:
|
|
17377
|
-
value:
|
|
18219
|
+
valueLabel: valueLabelFor(node2, depth, valueText),
|
|
18220
|
+
nodeId: node2.id,
|
|
18221
|
+
value: node2.value ?? 0,
|
|
17378
18222
|
depth,
|
|
17379
|
-
data: merged ? { id:
|
|
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: `${
|
|
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 =
|
|
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
|
|
18383
|
+
for (const node2 of nodes) {
|
|
17540
18384
|
const fields = [
|
|
17541
18385
|
{
|
|
17542
18386
|
label: "Total flow",
|
|
17543
|
-
value: formatFlowValue(
|
|
18387
|
+
value: formatFlowValue(node2.value, nodeFormatter ?? formatter)
|
|
17544
18388
|
}
|
|
17545
18389
|
];
|
|
17546
|
-
descriptors.set(`node-${
|
|
17547
|
-
title:
|
|
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
|
|
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
|
|
18573
|
+
defaultNumberFormatter as defaultNumberFormatter10,
|
|
17730
18574
|
formatDate as formatDate3,
|
|
17731
|
-
resolveNumberFormatter as
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
18453
|
-
getBreakpoint as
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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) :
|
|
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 =
|
|
20290
|
-
const heightClass =
|
|
20291
|
-
let strategy =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
20689
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
20733
|
-
(footnotes
|
|
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
|
-
|
|
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
|
|
20876
|
-
|
|
20877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|