@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 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
- function createMeasureText() {
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 { width: text.length * fontSize * 0.6, height: fontSize * 1.2 };
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 Inter, sans-serif`;
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
- const measureText = createMeasureText();
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
- const bgX = isMultiLine ? annotation.label.x - maxLineWidth / 2 - pad : annotation.label.x - pad;
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: annotation.label.y - fontSize + (lineHeight - fontSize) / 2 - pad,
5225
- width: maxLineWidth + pad * 2,
5226
- height: totalHeight + pad * 2,
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
- if (orientation === "x") {
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 xAxisExtent = computeXAxisExtent(layout);
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 xAxisExtent = computeXAxisExtent(layout);
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
- if (isHorizontal && i > 0) {
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 (isHorizontal) {
5863
- const labelWidth = estimateTextWidth4(
5864
- entry.label,
5865
- legend.labelStyle.fontSize,
5866
- legend.labelStyle.fontWeight
5867
- );
5868
- const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
5869
- offsetX += entryWidth;
5870
- } else {
5871
- offsetY += legend.swatchSize + legend.entryGap;
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
- // WebKit/iOS Safari getBBox() bug: text with dominant-baseline:hanging
6283
- // reports bounding boxes extending above y=0. The SVG spec default
6284
- // overflow is "hidden", which clips this phantom extent. Setting
6285
- // overflow:visible prevents the clipping. Chart marks are already
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
- const measureText = createMeasureText();
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
- const measureText = createMeasureText();
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, { x: chrome.title.x, y: chrome.title.y });
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, { x: chrome.subtitle.x, y: chrome.subtitle.y });
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, { x: chrome.source.x, y: bottomOffset + chrome.source.y });
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, { x: chrome.byline.x, y: bottomOffset + chrome.byline.y });
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, { x: chrome.footer.x, y: bottomOffset + chrome.footer.y });
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
- const measureText = createMeasureText();
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();