@opendata-ai/openchart-vanilla 7.2.4 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +267 -74
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/__tests__/font-race.test.ts +199 -0
- package/src/__tests__/known-bugs-render.test.ts +183 -0
- package/src/__tests__/svg-renderer.test.ts +6 -4
- package/src/barlist-mount.ts +58 -3
- package/src/barlist-renderer.ts +4 -1
- package/src/measure-text.ts +100 -3
- package/src/mount.ts +65 -2
- package/src/renderers/annotations.ts +24 -16
- package/src/renderers/axes.ts +19 -16
- package/src/renderers/brand.ts +8 -7
- package/src/renderers/chrome.ts +9 -9
- package/src/renderers/legend.ts +21 -13
- package/src/renderers/svg-dom.ts +1 -27
- package/src/sankey-mount.ts +62 -2
- package/src/sankey-renderer.ts +4 -1
- package/src/svg-renderer.ts +6 -4
- package/src/tilemap-mount.ts +60 -4
- package/src/tilemap-renderer.ts +23 -5
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ function setupTableAnimationCleanup(wrapper) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/barlist-renderer.ts
|
|
44
|
+
import { textAscent } from "@opendata-ai/openchart-core";
|
|
44
45
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
45
46
|
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
46
47
|
var BRAND_URL = "https://tryopendata.ai";
|
|
@@ -64,7 +65,7 @@ function renderChrome(parent, layout) {
|
|
|
64
65
|
const text = createSVGElement("text");
|
|
65
66
|
setAttrs(text, {
|
|
66
67
|
x: el.x,
|
|
67
|
-
y: isBottom ? bottomOffset + el.y : el.y
|
|
68
|
+
y: (isBottom ? bottomOffset + el.y : el.y) + textAscent(el.style.fontSize)
|
|
68
69
|
});
|
|
69
70
|
text.setAttribute("class", `oc-${key}`);
|
|
70
71
|
text.setAttribute("font-family", el.style.fontFamily);
|
|
@@ -467,7 +468,9 @@ function csvEscape(value) {
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
// src/measure-text.ts
|
|
470
|
-
|
|
471
|
+
import { estimateTextWidth } from "@opendata-ai/openchart-core";
|
|
472
|
+
var DEFAULT_FONT_FAMILY = "Inter, sans-serif";
|
|
473
|
+
function createMeasureText(fontFamily = DEFAULT_FONT_FAMILY) {
|
|
471
474
|
let canvas = null;
|
|
472
475
|
let ctx = null;
|
|
473
476
|
return (text, fontSize, fontWeight) => {
|
|
@@ -476,10 +479,13 @@ function createMeasureText() {
|
|
|
476
479
|
ctx = canvas.getContext("2d");
|
|
477
480
|
}
|
|
478
481
|
if (!ctx) {
|
|
479
|
-
return {
|
|
482
|
+
return {
|
|
483
|
+
width: estimateTextWidth(text, fontSize, fontWeight ?? 400),
|
|
484
|
+
height: fontSize * 1.2
|
|
485
|
+
};
|
|
480
486
|
}
|
|
481
487
|
const weight = fontWeight ?? 400;
|
|
482
|
-
ctx.font = `${weight} ${fontSize}px
|
|
488
|
+
ctx.font = `${weight} ${fontSize}px ${fontFamily}`;
|
|
483
489
|
const metrics = ctx.measureText(text);
|
|
484
490
|
return {
|
|
485
491
|
width: metrics.width,
|
|
@@ -487,6 +493,57 @@ function createMeasureText() {
|
|
|
487
493
|
};
|
|
488
494
|
};
|
|
489
495
|
}
|
|
496
|
+
function resolveFontFamily(container) {
|
|
497
|
+
if (typeof window === "undefined" || typeof getComputedStyle !== "function") {
|
|
498
|
+
return DEFAULT_FONT_FAMILY;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const style = getComputedStyle(container);
|
|
502
|
+
const custom = style.getPropertyValue("--oc-font-family").trim();
|
|
503
|
+
if (custom) return custom;
|
|
504
|
+
const computed = style.fontFamily?.trim();
|
|
505
|
+
if (computed) return computed;
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
return DEFAULT_FONT_FAMILY;
|
|
509
|
+
}
|
|
510
|
+
function primaryFontName(fontFamily) {
|
|
511
|
+
const first = fontFamily.split(",")[0]?.trim() ?? "";
|
|
512
|
+
return first.replace(/^["']|["']$/g, "").trim();
|
|
513
|
+
}
|
|
514
|
+
function fontsPending(fontFamily) {
|
|
515
|
+
if (typeof document === "undefined") return false;
|
|
516
|
+
const fonts = document.fonts;
|
|
517
|
+
if (!fonts || typeof fonts.check !== "function") return false;
|
|
518
|
+
const name = primaryFontName(fontFamily);
|
|
519
|
+
if (!name) return false;
|
|
520
|
+
const generics = /* @__PURE__ */ new Set([
|
|
521
|
+
"sans-serif",
|
|
522
|
+
"serif",
|
|
523
|
+
"monospace",
|
|
524
|
+
"system-ui",
|
|
525
|
+
"cursive",
|
|
526
|
+
"fantasy",
|
|
527
|
+
"ui-sans-serif",
|
|
528
|
+
"ui-serif",
|
|
529
|
+
"ui-monospace"
|
|
530
|
+
]);
|
|
531
|
+
if (generics.has(name.toLowerCase())) return false;
|
|
532
|
+
try {
|
|
533
|
+
return !fonts.check(`12px "${name}"`);
|
|
534
|
+
} catch {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function scheduleFontReload(fontFamily, isAlive, onReady) {
|
|
539
|
+
if (!fontsPending(fontFamily)) return false;
|
|
540
|
+
const fonts = document.fonts;
|
|
541
|
+
if (!fonts?.ready) return false;
|
|
542
|
+
fonts.ready.then(() => {
|
|
543
|
+
if (isAlive()) onReady();
|
|
544
|
+
});
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
490
547
|
|
|
491
548
|
// src/resize-observer.ts
|
|
492
549
|
var DEBOUNCE_MS = 16;
|
|
@@ -623,7 +680,14 @@ function createBarList(container, spec, options) {
|
|
|
623
680
|
let disconnectResize = null;
|
|
624
681
|
let animationCleanup = null;
|
|
625
682
|
let pendingResize = false;
|
|
626
|
-
|
|
683
|
+
let fontsReloadPending = false;
|
|
684
|
+
container.classList.add("oc-barlist-root");
|
|
685
|
+
function resolveEffectiveFont() {
|
|
686
|
+
return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
687
|
+
}
|
|
688
|
+
let fontFamily = resolveEffectiveFont();
|
|
689
|
+
let measureText = createMeasureText(fontFamily);
|
|
690
|
+
let renderGen = 0;
|
|
627
691
|
function getContainerDimensions() {
|
|
628
692
|
const rect = container.getBoundingClientRect();
|
|
629
693
|
return {
|
|
@@ -743,9 +807,20 @@ function createBarList(container, spec, options) {
|
|
|
743
807
|
}
|
|
744
808
|
});
|
|
745
809
|
}
|
|
810
|
+
renderGen += 1;
|
|
811
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
812
|
+
if (fontsReloadPending) {
|
|
813
|
+
fontsReloadPending = false;
|
|
814
|
+
container.dataset.ocFontsState = "ready";
|
|
815
|
+
}
|
|
746
816
|
}
|
|
747
817
|
function update(newSpec) {
|
|
748
818
|
currentSpec = newSpec;
|
|
819
|
+
const nextFont = resolveEffectiveFont();
|
|
820
|
+
if (nextFont !== fontFamily) {
|
|
821
|
+
fontFamily = nextFont;
|
|
822
|
+
measureText = createMeasureText(fontFamily);
|
|
823
|
+
}
|
|
749
824
|
currentLayout = compile();
|
|
750
825
|
render();
|
|
751
826
|
}
|
|
@@ -799,12 +874,20 @@ function createBarList(container, spec, options) {
|
|
|
799
874
|
}
|
|
800
875
|
container.classList.remove("oc-barlist-root", "oc-dark");
|
|
801
876
|
}
|
|
802
|
-
container.classList.add("oc-barlist-root");
|
|
803
877
|
if (resolveDarkMode(options?.darkMode)) {
|
|
804
878
|
container.classList.add("oc-dark");
|
|
805
879
|
}
|
|
806
880
|
currentLayout = compile();
|
|
807
881
|
render(true);
|
|
882
|
+
const fontsPending2 = scheduleFontReload(
|
|
883
|
+
fontFamily,
|
|
884
|
+
() => !destroyed,
|
|
885
|
+
() => {
|
|
886
|
+
fontsReloadPending = true;
|
|
887
|
+
resize();
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
container.dataset.ocFontsState = fontsPending2 ? "pending" : "ready";
|
|
808
891
|
if (options?.responsive !== false) {
|
|
809
892
|
disconnectResize = observeResize(container, () => resize());
|
|
810
893
|
}
|
|
@@ -4993,7 +5076,6 @@ function resolveMarkFill(fill, gradientMap) {
|
|
|
4993
5076
|
}
|
|
4994
5077
|
|
|
4995
5078
|
// src/renderers/svg-dom.ts
|
|
4996
|
-
import { estimateTextWidth } from "@opendata-ai/openchart-core";
|
|
4997
5079
|
var SVG_NS3 = "http://www.w3.org/2000/svg";
|
|
4998
5080
|
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
4999
5081
|
function createSVGElement2(tag) {
|
|
@@ -5020,23 +5102,6 @@ function applyTextStyle(el, style) {
|
|
|
5020
5102
|
el.setAttribute("font-variant", style.fontVariant);
|
|
5021
5103
|
}
|
|
5022
5104
|
}
|
|
5023
|
-
function computeXAxisExtent(layout) {
|
|
5024
|
-
const xAxis = layout.axes.x;
|
|
5025
|
-
if (!xAxis) return 0;
|
|
5026
|
-
if (xAxis.tickAngle && Math.abs(xAxis.tickAngle) > 10) {
|
|
5027
|
-
const fontSize = xAxis.tickLabelStyle.fontSize;
|
|
5028
|
-
const fontWeight = xAxis.tickLabelStyle.fontWeight;
|
|
5029
|
-
const angleRad = Math.abs(xAxis.tickAngle) * (Math.PI / 180);
|
|
5030
|
-
let maxLabelWidth = 40;
|
|
5031
|
-
for (const tick of xAxis.ticks) {
|
|
5032
|
-
const w = estimateTextWidth(tick.label, fontSize, fontWeight);
|
|
5033
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
5034
|
-
}
|
|
5035
|
-
const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
|
|
5036
|
-
return xAxis.label ? rotatedHeight + 20 : rotatedHeight;
|
|
5037
|
-
}
|
|
5038
|
-
return xAxis.label ? 48 : 26;
|
|
5039
|
-
}
|
|
5040
5105
|
|
|
5041
5106
|
// src/renderers/annotations.ts
|
|
5042
5107
|
function renderCurvedArrow(parent, from, to, stroke) {
|
|
@@ -5198,10 +5263,6 @@ function renderAnnotation(parent, annotation, index2, bgColor) {
|
|
|
5198
5263
|
const lineHeight = fontSize * (annotation.label.style.lineHeight ?? 1.3);
|
|
5199
5264
|
const isMultiLine = lines.length > 1;
|
|
5200
5265
|
if (isMultiLine) {
|
|
5201
|
-
const isDropLine = annotation.label.connector?.style === "drop-line";
|
|
5202
|
-
if (!isDropLine) {
|
|
5203
|
-
text.setAttribute("text-anchor", "middle");
|
|
5204
|
-
}
|
|
5205
5266
|
for (let i = 0; i < lines.length; i++) {
|
|
5206
5267
|
const tspan = createSVGElement2("tspan");
|
|
5207
5268
|
setAttrs2(tspan, { x: annotation.label.x, dy: i === 0 ? 0 : lineHeight });
|
|
@@ -5212,18 +5273,33 @@ function renderAnnotation(parent, annotation, index2, bgColor) {
|
|
|
5212
5273
|
text.textContent = annotation.label.text;
|
|
5213
5274
|
}
|
|
5214
5275
|
if (annotation.label.background) {
|
|
5215
|
-
const charWidth = fontSize * 0.55;
|
|
5216
|
-
const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
|
|
5217
|
-
const totalHeight = lines.length * lineHeight;
|
|
5218
5276
|
const pad = 3;
|
|
5219
|
-
|
|
5277
|
+
let bgX;
|
|
5278
|
+
let bgY;
|
|
5279
|
+
let bgW;
|
|
5280
|
+
let bgH;
|
|
5281
|
+
if (annotation.label.bounds) {
|
|
5282
|
+
const b = annotation.label.bounds;
|
|
5283
|
+
bgX = b.x - pad;
|
|
5284
|
+
bgY = b.y - pad;
|
|
5285
|
+
bgW = b.width + pad * 2;
|
|
5286
|
+
bgH = b.height + pad * 2;
|
|
5287
|
+
} else {
|
|
5288
|
+
const charWidth = fontSize * 0.55;
|
|
5289
|
+
const maxLineWidth = Math.max(...lines.map((l) => l.length)) * charWidth;
|
|
5290
|
+
const totalHeight = lines.length * lineHeight;
|
|
5291
|
+
bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
|
|
5292
|
+
bgY = annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad;
|
|
5293
|
+
bgW = maxLineWidth + pad * 2;
|
|
5294
|
+
bgH = totalHeight + pad * 2;
|
|
5295
|
+
}
|
|
5220
5296
|
const bgRect = createSVGElement2("rect");
|
|
5221
5297
|
bgRect.setAttribute("class", "oc-annotation-bg");
|
|
5222
5298
|
setAttrs2(bgRect, {
|
|
5223
5299
|
x: bgX,
|
|
5224
|
-
y:
|
|
5225
|
-
width:
|
|
5226
|
-
height:
|
|
5300
|
+
y: bgY,
|
|
5301
|
+
width: bgW,
|
|
5302
|
+
height: bgH,
|
|
5227
5303
|
fill: annotation.label.background,
|
|
5228
5304
|
rx: 2
|
|
5229
5305
|
});
|
|
@@ -5262,7 +5338,8 @@ import {
|
|
|
5262
5338
|
axisTitleOffset,
|
|
5263
5339
|
estimateTextWidth as estimateTextWidth2,
|
|
5264
5340
|
getAxisTitleOffset,
|
|
5265
|
-
TICK_LABEL_OFFSET
|
|
5341
|
+
TICK_LABEL_OFFSET,
|
|
5342
|
+
textAscent as textAscent2
|
|
5266
5343
|
} from "@opendata-ai/openchart-core";
|
|
5267
5344
|
function appendCompoundLabel(parent, primaryText, subtitle, fontWeight) {
|
|
5268
5345
|
const primarySpan = createSVGElement2("tspan");
|
|
@@ -5317,9 +5394,8 @@ function renderAxis(parent, axis, orientation, layout) {
|
|
|
5317
5394
|
const xLabelPad = axis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
|
|
5318
5395
|
setAttrs2(label, {
|
|
5319
5396
|
x: tick.position,
|
|
5320
|
-
y: area.y + area.height + xLabelPad,
|
|
5321
|
-
"text-anchor": "middle"
|
|
5322
|
-
"dominant-baseline": "hanging"
|
|
5397
|
+
y: area.y + area.height + xLabelPad + textAscent2(axis.tickLabelStyle.fontSize),
|
|
5398
|
+
"text-anchor": "middle"
|
|
5323
5399
|
});
|
|
5324
5400
|
}
|
|
5325
5401
|
applyTextStyle(label, axis.tickLabelStyle);
|
|
@@ -5446,7 +5522,18 @@ function renderAxis(parent, axis, orientation, layout) {
|
|
|
5446
5522
|
axisLabel.setAttribute("class", "oc-axis-title");
|
|
5447
5523
|
applyTextStyle(axisLabel, axis.labelStyle);
|
|
5448
5524
|
axisLabel.textContent = axis.label;
|
|
5449
|
-
|
|
5525
|
+
const tp = axis.titlePosition;
|
|
5526
|
+
if (tp) {
|
|
5527
|
+
const attrs = {
|
|
5528
|
+
x: tp.x,
|
|
5529
|
+
y: tp.y,
|
|
5530
|
+
"text-anchor": "middle"
|
|
5531
|
+
};
|
|
5532
|
+
if (tp.angle) {
|
|
5533
|
+
attrs.transform = `rotate(${tp.angle}, ${tp.x}, ${tp.y})`;
|
|
5534
|
+
}
|
|
5535
|
+
setAttrs2(axisLabel, attrs);
|
|
5536
|
+
} else if (orientation === "x") {
|
|
5450
5537
|
let titleY = area.y + area.height + 35;
|
|
5451
5538
|
if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
|
|
5452
5539
|
const angleRad = Math.abs(axis.tickAngle) * (Math.PI / 180);
|
|
@@ -5514,7 +5601,7 @@ function renderAxes(parent, layout) {
|
|
|
5514
5601
|
}
|
|
5515
5602
|
|
|
5516
5603
|
// src/renderers/brand.ts
|
|
5517
|
-
import { BRAND_FONT_SIZE as BRAND_FONT_SIZE2, BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2 } from "@opendata-ai/openchart-core";
|
|
5604
|
+
import { BRAND_FONT_SIZE as BRAND_FONT_SIZE2, BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2, textAscent as textAscent3 } from "@opendata-ai/openchart-core";
|
|
5518
5605
|
var BRAND_URL2 = "https://tryopendata.ai";
|
|
5519
5606
|
function renderBrand(parent, layout) {
|
|
5520
5607
|
if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
|
|
@@ -5523,8 +5610,7 @@ function renderBrand(parent, layout) {
|
|
|
5523
5610
|
const rightEdge = width - padding;
|
|
5524
5611
|
const fill = layout.theme.colors.axis;
|
|
5525
5612
|
const { chrome } = layout;
|
|
5526
|
-
const
|
|
5527
|
-
const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
|
|
5613
|
+
const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
|
|
5528
5614
|
const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
|
|
5529
5615
|
const { legend } = layout;
|
|
5530
5616
|
const bottomLegendOffset = legend.position === "bottom" && legend.bounds.height > 0 ? legend.bounds.height + 8 : 0;
|
|
@@ -5539,8 +5625,7 @@ function renderBrand(parent, layout) {
|
|
|
5539
5625
|
const text = createSVGElement2("text");
|
|
5540
5626
|
setAttrs2(text, {
|
|
5541
5627
|
x: rightEdge,
|
|
5542
|
-
y: chromeY,
|
|
5543
|
-
"dominant-baseline": "hanging",
|
|
5628
|
+
y: chromeY + textAscent3(BRAND_LARGE),
|
|
5544
5629
|
"font-family": layout.theme.fonts.family,
|
|
5545
5630
|
"font-size": BRAND_FONT_SIZE2,
|
|
5546
5631
|
"text-anchor": "end",
|
|
@@ -5565,10 +5650,10 @@ function renderBrand(parent, layout) {
|
|
|
5565
5650
|
}
|
|
5566
5651
|
|
|
5567
5652
|
// src/renderers/chrome.ts
|
|
5568
|
-
import { estimateTextWidth as estimateTextWidth3, wrapText } from "@opendata-ai/openchart-core";
|
|
5653
|
+
import { estimateTextWidth as estimateTextWidth3, textAscent as textAscent4, wrapText } from "@opendata-ai/openchart-core";
|
|
5569
5654
|
function renderChromeElement(parent, element, className, chromeKey, measureText, uppercase = false) {
|
|
5570
5655
|
const text = createSVGElement2("text");
|
|
5571
|
-
setAttrs2(text, { x: element.x, y: element.y });
|
|
5656
|
+
setAttrs2(text, { x: element.x, y: element.y + textAscent4(element.style.fontSize) });
|
|
5572
5657
|
applyTextStyle(text, element.style);
|
|
5573
5658
|
text.setAttribute("class", className);
|
|
5574
5659
|
text.setAttribute("data-chrome-key", chromeKey);
|
|
@@ -5620,8 +5705,7 @@ function renderChrome2(parent, layout) {
|
|
|
5620
5705
|
if (chrome.subtitle) {
|
|
5621
5706
|
renderChromeElement(g, chrome.subtitle, "oc-subtitle", "subtitle", measureText);
|
|
5622
5707
|
}
|
|
5623
|
-
const
|
|
5624
|
-
const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
|
|
5708
|
+
const bottomOffset = layout.chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
|
|
5625
5709
|
if (chrome.source) {
|
|
5626
5710
|
renderChromeElement(
|
|
5627
5711
|
g,
|
|
@@ -5774,11 +5858,16 @@ function renderLegend(parent, legend) {
|
|
|
5774
5858
|
g.setAttribute("role", "list");
|
|
5775
5859
|
g.setAttribute("aria-label", "Chart legend");
|
|
5776
5860
|
const isHorizontal = legend.position === "top" || legend.position === "bottom";
|
|
5861
|
+
const positions = "entryPositions" in legend ? legend.entryPositions : void 0;
|
|
5777
5862
|
let offsetX = legend.bounds.x;
|
|
5778
5863
|
let offsetY = legend.bounds.y;
|
|
5779
5864
|
for (let i = 0; i < legend.entries.length; i++) {
|
|
5780
5865
|
const entry = legend.entries[i];
|
|
5781
|
-
|
|
5866
|
+
const pos = positions?.[i];
|
|
5867
|
+
if (pos) {
|
|
5868
|
+
offsetX = pos.x;
|
|
5869
|
+
offsetY = pos.y;
|
|
5870
|
+
} else if (isHorizontal && i > 0) {
|
|
5782
5871
|
const labelWidth = estimateTextWidth4(
|
|
5783
5872
|
entry.label,
|
|
5784
5873
|
legend.labelStyle.fontSize,
|
|
@@ -5859,16 +5948,18 @@ function renderLegend(parent, legend) {
|
|
|
5859
5948
|
label.textContent = entry.label;
|
|
5860
5949
|
entryG.appendChild(label);
|
|
5861
5950
|
g.appendChild(entryG);
|
|
5862
|
-
if (
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5951
|
+
if (!pos) {
|
|
5952
|
+
if (isHorizontal) {
|
|
5953
|
+
const labelWidth = estimateTextWidth4(
|
|
5954
|
+
entry.label,
|
|
5955
|
+
legend.labelStyle.fontSize,
|
|
5956
|
+
legend.labelStyle.fontWeight
|
|
5957
|
+
);
|
|
5958
|
+
const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
|
|
5959
|
+
offsetX += entryWidth;
|
|
5960
|
+
} else {
|
|
5961
|
+
offsetY += "rowHeight" in legend && legend.rowHeight ? legend.rowHeight : legend.swatchSize + legend.entryGap;
|
|
5962
|
+
}
|
|
5872
5963
|
}
|
|
5873
5964
|
}
|
|
5874
5965
|
parent.appendChild(g);
|
|
@@ -6279,10 +6370,12 @@ function renderChartSVG(layout, container, opts) {
|
|
|
6279
6370
|
setAttrs2(svg, {
|
|
6280
6371
|
viewBox: `0 0 ${width} ${height}`,
|
|
6281
6372
|
xmlns: SVG_NS3,
|
|
6282
|
-
//
|
|
6283
|
-
//
|
|
6284
|
-
//
|
|
6285
|
-
//
|
|
6373
|
+
// The SVG spec default is overflow:"hidden", which clips anything a hair
|
|
6374
|
+
// outside the viewBox. We now position all text on the alphabetic/central
|
|
6375
|
+
// baseline (dominant-baseline:hanging was dropped because WebKit computed
|
|
6376
|
+
// it from different metrics), but WebKit/iOS still reports getBBox extents
|
|
6377
|
+
// with a few pixels of slack around tspans, so text touching an edge can
|
|
6378
|
+
// still get clipped. overflow:visible avoids that. Chart marks are already
|
|
6286
6379
|
// constrained by a clipPath, so nothing bleeds out.
|
|
6287
6380
|
overflow: "visible",
|
|
6288
6381
|
// Hint browsers to enable sub-pixel font hinting and kerning for chart text.
|
|
@@ -6634,13 +6727,20 @@ function createChart(container, spec, options) {
|
|
|
6634
6727
|
let isFirstRender = true;
|
|
6635
6728
|
let cleanupAnimations = null;
|
|
6636
6729
|
let pendingResize = false;
|
|
6730
|
+
let fontsReloadPending = false;
|
|
6637
6731
|
let selectedElement = options?.selectedElement ?? null;
|
|
6638
6732
|
let overlayElement = null;
|
|
6639
6733
|
let isTextEditingActive = false;
|
|
6640
6734
|
const runtimeHiddenSeries = /* @__PURE__ */ new Set();
|
|
6641
6735
|
const runtimeShownSeries = /* @__PURE__ */ new Set();
|
|
6642
6736
|
let textEditCleanup = null;
|
|
6643
|
-
|
|
6737
|
+
container.classList.add("oc-root");
|
|
6738
|
+
function resolveEffectiveFont() {
|
|
6739
|
+
return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
6740
|
+
}
|
|
6741
|
+
let fontFamily = resolveEffectiveFont();
|
|
6742
|
+
let measureText = createMeasureText(fontFamily);
|
|
6743
|
+
let renderGen = 0;
|
|
6644
6744
|
function compile() {
|
|
6645
6745
|
const { width, height } = getContainerDimensions();
|
|
6646
6746
|
const darkMode = resolveDarkMode3(options?.darkMode);
|
|
@@ -7088,6 +7188,12 @@ function createChart(container, spec, options) {
|
|
|
7088
7188
|
}
|
|
7089
7189
|
});
|
|
7090
7190
|
}
|
|
7191
|
+
renderGen += 1;
|
|
7192
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
7193
|
+
if (fontsReloadPending) {
|
|
7194
|
+
fontsReloadPending = false;
|
|
7195
|
+
container.dataset.ocFontsState = "ready";
|
|
7196
|
+
}
|
|
7091
7197
|
if (isFirstRender) {
|
|
7092
7198
|
isFirstRender = false;
|
|
7093
7199
|
}
|
|
@@ -7095,6 +7201,11 @@ function createChart(container, spec, options) {
|
|
|
7095
7201
|
function update(newSpec, updateOpts) {
|
|
7096
7202
|
if (destroyed) return;
|
|
7097
7203
|
currentSpec = newSpec;
|
|
7204
|
+
const nextFont = resolveEffectiveFont();
|
|
7205
|
+
if (nextFont !== fontFamily) {
|
|
7206
|
+
fontFamily = nextFont;
|
|
7207
|
+
measureText = createMeasureText(fontFamily);
|
|
7208
|
+
}
|
|
7098
7209
|
runtimeHiddenSeries.clear();
|
|
7099
7210
|
runtimeShownSeries.clear();
|
|
7100
7211
|
if (updateOpts && "selectedElement" in updateOpts) {
|
|
@@ -7185,6 +7296,15 @@ function createChart(container, spec, options) {
|
|
|
7185
7296
|
container.classList.remove("oc-root");
|
|
7186
7297
|
}
|
|
7187
7298
|
render();
|
|
7299
|
+
const pending = scheduleFontReload(
|
|
7300
|
+
fontFamily,
|
|
7301
|
+
() => !destroyed,
|
|
7302
|
+
() => {
|
|
7303
|
+
fontsReloadPending = true;
|
|
7304
|
+
resize();
|
|
7305
|
+
}
|
|
7306
|
+
);
|
|
7307
|
+
container.dataset.ocFontsState = pending ? "pending" : "ready";
|
|
7188
7308
|
if (options?.responsive !== false) {
|
|
7189
7309
|
disconnectResize = observeResize(container, () => {
|
|
7190
7310
|
resize();
|
|
@@ -7543,6 +7663,7 @@ import {
|
|
|
7543
7663
|
BRAND_FONT_SIZE as BRAND_FONT_SIZE3,
|
|
7544
7664
|
BRAND_MIN_WIDTH as BRAND_MIN_WIDTH3,
|
|
7545
7665
|
estimateTextWidth as estimateTextWidth5,
|
|
7666
|
+
textAscent as textAscent5,
|
|
7546
7667
|
wrapText as wrapText2
|
|
7547
7668
|
} from "@opendata-ai/openchart-core";
|
|
7548
7669
|
import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
|
|
@@ -7586,7 +7707,7 @@ function stampAnimationAttrs2(el, mark, fallbackIndex, animation) {
|
|
|
7586
7707
|
}
|
|
7587
7708
|
function renderChromeElement2(parent, element, className, chromeKey, measureText) {
|
|
7588
7709
|
const text = createSVGElement3("text");
|
|
7589
|
-
setAttrs3(text, { x: element.x, y: element.y });
|
|
7710
|
+
setAttrs3(text, { x: element.x, y: element.y + textAscent5(element.style.fontSize) });
|
|
7590
7711
|
applyTextStyle2(text, element.style);
|
|
7591
7712
|
text.setAttribute("class", className);
|
|
7592
7713
|
text.setAttribute("data-chrome-key", chromeKey);
|
|
@@ -7971,7 +8092,14 @@ function createSankey(container, spec, options) {
|
|
|
7971
8092
|
let isFirstRender = true;
|
|
7972
8093
|
let animationCleanup = null;
|
|
7973
8094
|
let pendingResize = false;
|
|
7974
|
-
|
|
8095
|
+
let fontsReloadPending = false;
|
|
8096
|
+
container.classList.add("oc-sankey-root");
|
|
8097
|
+
function resolveEffectiveFont() {
|
|
8098
|
+
return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
8099
|
+
}
|
|
8100
|
+
let fontFamily = resolveEffectiveFont();
|
|
8101
|
+
let measureText = createMeasureText(fontFamily);
|
|
8102
|
+
let renderGen = 0;
|
|
7975
8103
|
function getContainerDimensions() {
|
|
7976
8104
|
const rect = container.getBoundingClientRect();
|
|
7977
8105
|
return {
|
|
@@ -8177,10 +8305,21 @@ function createSankey(container, spec, options) {
|
|
|
8177
8305
|
}
|
|
8178
8306
|
});
|
|
8179
8307
|
}
|
|
8308
|
+
renderGen += 1;
|
|
8309
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
8310
|
+
if (fontsReloadPending) {
|
|
8311
|
+
fontsReloadPending = false;
|
|
8312
|
+
container.dataset.ocFontsState = "ready";
|
|
8313
|
+
}
|
|
8180
8314
|
}
|
|
8181
8315
|
function update(newSpec) {
|
|
8182
8316
|
if (destroyed) return;
|
|
8183
8317
|
currentSpec = newSpec;
|
|
8318
|
+
const nextFont = resolveEffectiveFont();
|
|
8319
|
+
if (nextFont !== fontFamily) {
|
|
8320
|
+
fontFamily = nextFont;
|
|
8321
|
+
measureText = createMeasureText(fontFamily);
|
|
8322
|
+
}
|
|
8184
8323
|
isFirstRender = true;
|
|
8185
8324
|
render();
|
|
8186
8325
|
}
|
|
@@ -8237,6 +8376,7 @@ function createSankey(container, spec, options) {
|
|
|
8237
8376
|
}
|
|
8238
8377
|
svgElement = null;
|
|
8239
8378
|
container.classList.remove("oc-dark");
|
|
8379
|
+
container.classList.remove("oc-sankey-root");
|
|
8240
8380
|
}
|
|
8241
8381
|
try {
|
|
8242
8382
|
currentLayout = compile();
|
|
@@ -8264,10 +8404,21 @@ function createSankey(container, spec, options) {
|
|
|
8264
8404
|
}
|
|
8265
8405
|
});
|
|
8266
8406
|
}
|
|
8407
|
+
renderGen += 1;
|
|
8408
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
8267
8409
|
} catch (err) {
|
|
8268
8410
|
console.error("[viz] Sankey mount failed:", err);
|
|
8269
8411
|
throw err;
|
|
8270
8412
|
}
|
|
8413
|
+
const fontsPending2 = scheduleFontReload(
|
|
8414
|
+
fontFamily,
|
|
8415
|
+
() => !destroyed,
|
|
8416
|
+
() => {
|
|
8417
|
+
fontsReloadPending = true;
|
|
8418
|
+
resize();
|
|
8419
|
+
}
|
|
8420
|
+
);
|
|
8421
|
+
container.dataset.ocFontsState = fontsPending2 ? "pending" : "ready";
|
|
8271
8422
|
if (options?.responsive !== false) {
|
|
8272
8423
|
disconnectResize = observeResize(container, () => {
|
|
8273
8424
|
resize();
|
|
@@ -9122,6 +9273,7 @@ function createTable(container, spec, options) {
|
|
|
9122
9273
|
import { compileTileMap } from "@opendata-ai/openchart-engine";
|
|
9123
9274
|
|
|
9124
9275
|
// src/tilemap-renderer.ts
|
|
9276
|
+
import { textAscent as textAscent6 } from "@opendata-ai/openchart-core";
|
|
9125
9277
|
var SVG_NS5 = "http://www.w3.org/2000/svg";
|
|
9126
9278
|
var XLINK_NS4 = "http://www.w3.org/1999/xlink";
|
|
9127
9279
|
var BRAND_URL5 = "https://tryopendata.ai";
|
|
@@ -9145,7 +9297,10 @@ function renderChrome4(parent, layout) {
|
|
|
9145
9297
|
const bottomOffset = layout.area.y + layout.area.height;
|
|
9146
9298
|
if (chrome.title) {
|
|
9147
9299
|
const text = createSVGElement4("text");
|
|
9148
|
-
setAttrs4(text, {
|
|
9300
|
+
setAttrs4(text, {
|
|
9301
|
+
x: chrome.title.x,
|
|
9302
|
+
y: chrome.title.y + textAscent6(chrome.title.style.fontSize)
|
|
9303
|
+
});
|
|
9149
9304
|
text.setAttribute("class", "oc-title");
|
|
9150
9305
|
text.setAttribute("font-family", chrome.title.style.fontFamily);
|
|
9151
9306
|
text.setAttribute("font-size", String(chrome.title.style.fontSize));
|
|
@@ -9156,7 +9311,10 @@ function renderChrome4(parent, layout) {
|
|
|
9156
9311
|
}
|
|
9157
9312
|
if (chrome.subtitle) {
|
|
9158
9313
|
const text = createSVGElement4("text");
|
|
9159
|
-
setAttrs4(text, {
|
|
9314
|
+
setAttrs4(text, {
|
|
9315
|
+
x: chrome.subtitle.x,
|
|
9316
|
+
y: chrome.subtitle.y + textAscent6(chrome.subtitle.style.fontSize)
|
|
9317
|
+
});
|
|
9160
9318
|
text.setAttribute("class", "oc-subtitle");
|
|
9161
9319
|
text.setAttribute("font-family", chrome.subtitle.style.fontFamily);
|
|
9162
9320
|
text.setAttribute("font-size", String(chrome.subtitle.style.fontSize));
|
|
@@ -9170,7 +9328,10 @@ function renderChrome4(parent, layout) {
|
|
|
9170
9328
|
}
|
|
9171
9329
|
if (chrome.source) {
|
|
9172
9330
|
const text = createSVGElement4("text");
|
|
9173
|
-
setAttrs4(text, {
|
|
9331
|
+
setAttrs4(text, {
|
|
9332
|
+
x: chrome.source.x,
|
|
9333
|
+
y: bottomOffset + chrome.source.y + textAscent6(chrome.source.style.fontSize)
|
|
9334
|
+
});
|
|
9174
9335
|
text.setAttribute("class", "oc-source");
|
|
9175
9336
|
text.setAttribute("font-family", chrome.source.style.fontFamily);
|
|
9176
9337
|
text.setAttribute("font-size", String(chrome.source.style.fontSize));
|
|
@@ -9184,7 +9345,10 @@ function renderChrome4(parent, layout) {
|
|
|
9184
9345
|
}
|
|
9185
9346
|
if (chrome.byline) {
|
|
9186
9347
|
const text = createSVGElement4("text");
|
|
9187
|
-
setAttrs4(text, {
|
|
9348
|
+
setAttrs4(text, {
|
|
9349
|
+
x: chrome.byline.x,
|
|
9350
|
+
y: bottomOffset + chrome.byline.y + textAscent6(chrome.byline.style.fontSize)
|
|
9351
|
+
});
|
|
9188
9352
|
text.setAttribute("class", "oc-byline");
|
|
9189
9353
|
text.setAttribute("font-family", chrome.byline.style.fontFamily);
|
|
9190
9354
|
text.setAttribute("font-size", String(chrome.byline.style.fontSize));
|
|
@@ -9198,7 +9362,10 @@ function renderChrome4(parent, layout) {
|
|
|
9198
9362
|
}
|
|
9199
9363
|
if (chrome.footer) {
|
|
9200
9364
|
const text = createSVGElement4("text");
|
|
9201
|
-
setAttrs4(text, {
|
|
9365
|
+
setAttrs4(text, {
|
|
9366
|
+
x: chrome.footer.x,
|
|
9367
|
+
y: bottomOffset + chrome.footer.y + textAscent6(chrome.footer.style.fontSize)
|
|
9368
|
+
});
|
|
9202
9369
|
text.setAttribute("class", "oc-footer");
|
|
9203
9370
|
text.setAttribute("font-family", chrome.footer.style.fontFamily);
|
|
9204
9371
|
text.setAttribute("font-size", String(chrome.footer.style.fontSize));
|
|
@@ -9461,7 +9628,14 @@ function createTileMap(container, spec, options) {
|
|
|
9461
9628
|
let disconnectResize = null;
|
|
9462
9629
|
let animationCleanup = null;
|
|
9463
9630
|
let pendingResize = false;
|
|
9464
|
-
|
|
9631
|
+
let fontsReloadPending = false;
|
|
9632
|
+
container.classList.add("oc-tilemap-root");
|
|
9633
|
+
function resolveEffectiveFont() {
|
|
9634
|
+
return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
9635
|
+
}
|
|
9636
|
+
let fontFamily = resolveEffectiveFont();
|
|
9637
|
+
let measureText = createMeasureText(fontFamily);
|
|
9638
|
+
let renderGen = 0;
|
|
9465
9639
|
function getContainerDimensions() {
|
|
9466
9640
|
const rect = container.getBoundingClientRect();
|
|
9467
9641
|
const width = Math.max(rect.width || 600, 100);
|
|
@@ -9574,9 +9748,20 @@ function createTileMap(container, spec, options) {
|
|
|
9574
9748
|
}
|
|
9575
9749
|
});
|
|
9576
9750
|
}
|
|
9751
|
+
renderGen += 1;
|
|
9752
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
9753
|
+
if (fontsReloadPending) {
|
|
9754
|
+
fontsReloadPending = false;
|
|
9755
|
+
container.dataset.ocFontsState = "ready";
|
|
9756
|
+
}
|
|
9577
9757
|
}
|
|
9578
9758
|
function update(newSpec) {
|
|
9579
9759
|
currentSpec = newSpec;
|
|
9760
|
+
const nextFont = resolveEffectiveFont();
|
|
9761
|
+
if (nextFont !== fontFamily) {
|
|
9762
|
+
fontFamily = nextFont;
|
|
9763
|
+
measureText = createMeasureText(fontFamily);
|
|
9764
|
+
}
|
|
9580
9765
|
currentLayout = compile();
|
|
9581
9766
|
render();
|
|
9582
9767
|
}
|
|
@@ -9630,12 +9815,20 @@ function createTileMap(container, spec, options) {
|
|
|
9630
9815
|
}
|
|
9631
9816
|
container.classList.remove("oc-tilemap-root", "oc-dark");
|
|
9632
9817
|
}
|
|
9633
|
-
container.classList.add("oc-tilemap-root");
|
|
9634
9818
|
if (resolveDarkMode6(options?.darkMode)) {
|
|
9635
9819
|
container.classList.add("oc-dark");
|
|
9636
9820
|
}
|
|
9637
9821
|
currentLayout = compile();
|
|
9638
9822
|
render(true);
|
|
9823
|
+
const fontsPending2 = scheduleFontReload(
|
|
9824
|
+
fontFamily,
|
|
9825
|
+
() => !destroyed,
|
|
9826
|
+
() => {
|
|
9827
|
+
fontsReloadPending = true;
|
|
9828
|
+
resize();
|
|
9829
|
+
}
|
|
9830
|
+
);
|
|
9831
|
+
container.dataset.ocFontsState = fontsPending2 ? "pending" : "ready";
|
|
9639
9832
|
if (options?.responsive !== false) {
|
|
9640
9833
|
disconnectResize = observeResize(container, () => {
|
|
9641
9834
|
resize();
|