@opendata-ai/openchart-vanilla 6.25.4 → 6.26.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
@@ -2805,7 +2805,7 @@ function createGraph(container, spec, options) {
2805
2805
  }
2806
2806
  chromeEl = document.createElement("div");
2807
2807
  chromeEl.className = "oc-graph-chrome";
2808
- renderChrome3();
2808
+ renderChrome4();
2809
2809
  wrapper.appendChild(chromeEl);
2810
2810
  canvas = document.createElement("canvas");
2811
2811
  canvas.className = "oc-graph-canvas";
@@ -2825,7 +2825,7 @@ function createGraph(container, spec, options) {
2825
2825
  renderer = new GraphCanvasRenderer(canvas);
2826
2826
  renderer.resize(width, canvasHeight);
2827
2827
  }
2828
- function renderChrome3() {
2828
+ function renderChrome4() {
2829
2829
  if (!chromeEl) return;
2830
2830
  let html = "";
2831
2831
  if (compilation.chrome.title) {
@@ -2843,7 +2843,7 @@ function createGraph(container, spec, options) {
2843
2843
  }
2844
2844
  function renderLegend3() {
2845
2845
  if (!legendEl) return;
2846
- const entries = compilation.legend.entries;
2846
+ const entries = "entries" in compilation.legend ? compilation.legend.entries : [];
2847
2847
  if (entries.length === 0) {
2848
2848
  legendEl.style.display = "none";
2849
2849
  return;
@@ -3139,7 +3139,7 @@ function createGraph(container, spec, options) {
3139
3139
  compilation = compile();
3140
3140
  adjacencyMap = buildAdjacencyMap(compilation.edges);
3141
3141
  buildDataMaps();
3142
- renderChrome3();
3142
+ renderChrome4();
3143
3143
  renderLegend3();
3144
3144
  initSimulation();
3145
3145
  initInteraction();
@@ -3174,7 +3174,7 @@ function createGraph(container, spec, options) {
3174
3174
  };
3175
3175
  });
3176
3176
  spatialIndex.rebuild(positionedNodes);
3177
- renderChrome3();
3177
+ renderChrome4();
3178
3178
  renderLegend3();
3179
3179
  needsRender = true;
3180
3180
  scheduleRender();
@@ -3637,6 +3637,18 @@ import {
3637
3637
  getAxisTitleOffset,
3638
3638
  TICK_LABEL_OFFSET
3639
3639
  } from "@opendata-ai/openchart-core";
3640
+ function appendCompoundLabel(parent, primaryText, subtitle, fontWeight) {
3641
+ const primarySpan = createSVGElement("tspan");
3642
+ primarySpan.setAttribute("font-weight", String(fontWeight));
3643
+ primarySpan.textContent = primaryText;
3644
+ parent.appendChild(primarySpan);
3645
+ const subtitleSpan = createSVGElement("tspan");
3646
+ subtitleSpan.setAttribute("dx", "0.5em");
3647
+ subtitleSpan.textContent = subtitle;
3648
+ subtitleSpan.setAttribute("font-weight", "400");
3649
+ subtitleSpan.setAttribute("fill-opacity", "0.6");
3650
+ parent.appendChild(subtitleSpan);
3651
+ }
3640
3652
  function renderAxis(parent, axis, orientation, layout) {
3641
3653
  const g = createSVGElement("g");
3642
3654
  const isRight = orientation === "y" && axis.orient === "right";
@@ -3693,27 +3705,62 @@ function renderAxis(parent, axis, orientation, layout) {
3693
3705
  const availableWidth = area.x - TICK_LABEL_OFFSET;
3694
3706
  const fontSize = axis.tickLabelStyle.fontSize;
3695
3707
  const fontWeight = axis.tickLabelStyle.fontWeight;
3696
- const fullWidth = estimateTextWidth2(tick.label, fontSize, fontWeight);
3697
- if (fullWidth > availableWidth && availableWidth > 20) {
3698
- const ellipsis = "\u2026";
3699
- const ellipsisWidth = estimateTextWidth2(ellipsis, fontSize, fontWeight);
3700
- let lo = 0;
3701
- let hi = tick.label.length;
3702
- while (lo < hi) {
3703
- const mid = lo + hi + 1 >>> 1;
3704
- const candidate = tick.label.slice(0, mid);
3705
- if (estimateTextWidth2(candidate, fontSize, fontWeight) + ellipsisWidth <= availableWidth) {
3706
- lo = mid;
3708
+ if (tick.subtitle) {
3709
+ const gapWidth = fontSize * 0.5;
3710
+ const subtitleWidth = estimateTextWidth2(tick.subtitle, fontSize, fontWeight);
3711
+ const primaryWidth = estimateTextWidth2(tick.label, fontSize, fontWeight);
3712
+ const totalWidth = primaryWidth + gapWidth + subtitleWidth;
3713
+ if (totalWidth > availableWidth && availableWidth > 20) {
3714
+ const ellipsis = "\u2026";
3715
+ const ellipsisWidth = estimateTextWidth2(ellipsis, fontSize, fontWeight);
3716
+ const budgetForPrimary = availableWidth - gapWidth - subtitleWidth - ellipsisWidth;
3717
+ let primaryText = tick.label;
3718
+ if (budgetForPrimary > 0) {
3719
+ let lo = 0;
3720
+ let hi = tick.label.length;
3721
+ while (lo < hi) {
3722
+ const mid = lo + hi + 1 >>> 1;
3723
+ const candidate = tick.label.slice(0, mid);
3724
+ if (estimateTextWidth2(candidate, fontSize, fontWeight) <= budgetForPrimary) {
3725
+ lo = mid;
3726
+ } else {
3727
+ hi = mid - 1;
3728
+ }
3729
+ }
3730
+ primaryText = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
3707
3731
  } else {
3708
- hi = mid - 1;
3732
+ primaryText = ellipsis;
3709
3733
  }
3734
+ appendCompoundLabel(label, primaryText, tick.subtitle, fontWeight);
3735
+ const titleEl = createSVGElement("title");
3736
+ titleEl.textContent = `${tick.label} ${tick.subtitle}`;
3737
+ label.appendChild(titleEl);
3738
+ } else {
3739
+ appendCompoundLabel(label, tick.label, tick.subtitle, fontWeight);
3710
3740
  }
3711
- label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
3712
- const titleEl = createSVGElement("title");
3713
- titleEl.textContent = tick.label;
3714
- label.appendChild(titleEl);
3715
3741
  } else {
3716
- label.textContent = tick.label;
3742
+ const fullWidth = estimateTextWidth2(tick.label, fontSize, fontWeight);
3743
+ if (fullWidth > availableWidth && availableWidth > 20) {
3744
+ const ellipsis = "\u2026";
3745
+ const ellipsisWidth = estimateTextWidth2(ellipsis, fontSize, fontWeight);
3746
+ let lo = 0;
3747
+ let hi = tick.label.length;
3748
+ while (lo < hi) {
3749
+ const mid = lo + hi + 1 >>> 1;
3750
+ const candidate = tick.label.slice(0, mid);
3751
+ if (estimateTextWidth2(candidate, fontSize, fontWeight) + ellipsisWidth <= availableWidth) {
3752
+ lo = mid;
3753
+ } else {
3754
+ hi = mid - 1;
3755
+ }
3756
+ }
3757
+ label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
3758
+ const titleEl = createSVGElement("title");
3759
+ titleEl.textContent = tick.label;
3760
+ label.appendChild(titleEl);
3761
+ } else {
3762
+ label.textContent = tick.label;
3763
+ }
3717
3764
  }
3718
3765
  } else {
3719
3766
  label.textContent = tick.label;
@@ -3930,8 +3977,11 @@ function renderChrome(parent, layout) {
3930
3977
 
3931
3978
  // src/renderers/legend.ts
3932
3979
  import { estimateTextWidth as estimateTextWidth3 } from "@opendata-ai/openchart-core";
3980
+ function isCategorical(legend) {
3981
+ return !legend.type || legend.type === "categorical";
3982
+ }
3933
3983
  function renderLegend(parent, legend) {
3934
- if (legend.entries.length === 0) return;
3984
+ if (!isCategorical(legend) || legend.entries.length === 0) return;
3935
3985
  const g = createSVGElement("g");
3936
3986
  g.setAttribute("class", "oc-legend");
3937
3987
  g.setAttribute("role", "list");
@@ -4434,6 +4484,24 @@ function renderChartSVG(layout, container, opts) {
4434
4484
  overlay.setAttribute("class", "oc-voronoi-overlay");
4435
4485
  overlay.setAttribute("data-voronoi-overlay", "true");
4436
4486
  clippedGroup.appendChild(overlay);
4487
+ if (opts?.crosshair) {
4488
+ const crosshairLine = createSVGElement("line");
4489
+ crosshairLine.setAttribute("data-crosshair", "true");
4490
+ crosshairLine.setAttribute("class", "oc-crosshair");
4491
+ setAttrs(crosshairLine, {
4492
+ x1: 0,
4493
+ y1: layout.area.y,
4494
+ x2: 0,
4495
+ y2: layout.area.y + layout.area.height,
4496
+ stroke: layout.theme.colors.gridline,
4497
+ "stroke-opacity": "0.5",
4498
+ "stroke-dasharray": "4,3",
4499
+ "stroke-width": "1",
4500
+ "pointer-events": "none"
4501
+ });
4502
+ crosshairLine.style.display = "none";
4503
+ clippedGroup.appendChild(crosshairLine);
4504
+ }
4437
4505
  }
4438
4506
  svg.appendChild(clippedGroup);
4439
4507
  renderAnnotations(svg, layout);
@@ -4695,6 +4763,7 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4695
4763
  const voronoiPoints = collectVoronoiPoints(layout);
4696
4764
  if (voronoiPoints.length === 0) return () => {
4697
4765
  };
4766
+ const crosshair = svg.querySelector("[data-crosshair]");
4698
4767
  const cleanups = [];
4699
4768
  const handleMouseMove = (e) => {
4700
4769
  const mouseEvent = e;
@@ -4707,11 +4776,17 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4707
4776
  const svgY = (mouseEvent.clientY - svgRect.top) * scaleY;
4708
4777
  const nearest = findNearestPoint(voronoiPoints, svgX, svgY);
4709
4778
  if (!nearest?.tooltip) return;
4779
+ if (crosshair) {
4780
+ crosshair.setAttribute("x1", String(nearest.x));
4781
+ crosshair.setAttribute("x2", String(nearest.x));
4782
+ crosshair.style.display = "";
4783
+ }
4710
4784
  const containerX = mouseEvent.clientX - svgRect.left;
4711
4785
  const containerY = mouseEvent.clientY - svgRect.top;
4712
4786
  tooltipManager.show(nearest.tooltip, containerX, containerY);
4713
4787
  };
4714
4788
  const handleMouseLeave = () => {
4789
+ if (crosshair) crosshair.style.display = "none";
4715
4790
  tooltipManager.hide();
4716
4791
  };
4717
4792
  const handleTouchStart = (e) => {
@@ -4727,6 +4802,11 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4727
4802
  const svgY = (touch.clientY - svgRect.top) * scaleY;
4728
4803
  const nearest = findNearestPoint(voronoiPoints, svgX, svgY);
4729
4804
  if (!nearest?.tooltip) return;
4805
+ if (crosshair) {
4806
+ crosshair.setAttribute("x1", String(nearest.x));
4807
+ crosshair.setAttribute("x2", String(nearest.x));
4808
+ crosshair.style.display = "";
4809
+ }
4730
4810
  const containerX = touch.clientX - svgRect.left;
4731
4811
  const containerY = touch.clientY - svgRect.top;
4732
4812
  tooltipManager.show(nearest.tooltip, containerX, containerY);
@@ -5058,7 +5138,7 @@ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setD
5058
5138
  };
5059
5139
  }
5060
5140
  function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
5061
- const SVG_NS4 = "http://www.w3.org/2000/svg";
5141
+ const SVG_NS5 = "http://www.w3.org/2000/svg";
5062
5142
  const cleanups = [];
5063
5143
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
5064
5144
  for (const el of annotationGroups) {
@@ -5097,7 +5177,7 @@ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
5097
5177
  const createdHandles = [];
5098
5178
  for (const ep of endpoints) {
5099
5179
  if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
5100
- const handleEl = document.createElementNS(SVG_NS4, "circle");
5180
+ const handleEl = document.createElementNS(SVG_NS5, "circle");
5101
5181
  handleEl.setAttribute("class", "oc-connector-handle");
5102
5182
  handleEl.setAttribute("data-endpoint", ep.name);
5103
5183
  handleEl.setAttribute("cx", String(ep.cx));
@@ -5612,7 +5692,7 @@ function getEditableElements(spec, layout) {
5612
5692
  for (const series of seriesLabels) {
5613
5693
  refs.push(elementRef.seriesLabel(series));
5614
5694
  }
5615
- if (layout.legend.entries.length > 0) {
5695
+ if ("entries" in layout.legend && layout.legend.entries.length > 0) {
5616
5696
  refs.push(elementRef.legend());
5617
5697
  }
5618
5698
  return refs;
@@ -5994,7 +6074,11 @@ function createChart(container, spec, options) {
5994
6074
  }
5995
6075
  currentLayout = compile();
5996
6076
  const shouldAnimate = isFirstRender && !!currentLayout.animation?.enabled;
5997
- svgElement = renderChartSVG(currentLayout, container, { animate: shouldAnimate });
6077
+ const crosshair = "crosshair" in currentSpec && !!currentSpec.crosshair;
6078
+ svgElement = renderChartSVG(currentLayout, container, {
6079
+ animate: shouldAnimate,
6080
+ crosshair
6081
+ });
5998
6082
  tooltipManager = createTooltipManager(container);
5999
6083
  cleanupTooltipEvents = wireTooltipEvents(
6000
6084
  svgElement,
@@ -6706,8 +6790,9 @@ function renderBrand2(parent, layout) {
6706
6790
  a2.appendChild(text);
6707
6791
  parent.appendChild(a2);
6708
6792
  }
6709
- function renderLegend2(parent, legend) {
6710
- if (legend.entries.length === 0) return;
6793
+ function renderLegend2(parent, legendLayout) {
6794
+ if (!("entries" in legendLayout) || legendLayout.entries.length === 0) return;
6795
+ const legend = legendLayout;
6711
6796
  const g = createSVGElement2("g");
6712
6797
  g.setAttribute("class", "oc-legend");
6713
6798
  g.setAttribute("role", "list");
@@ -8125,6 +8210,535 @@ function createTable(container, spec, options) {
8125
8210
  destroy
8126
8211
  };
8127
8212
  }
8213
+
8214
+ // src/tilemap-mount.ts
8215
+ import { compileTileMap } from "@opendata-ai/openchart-engine";
8216
+
8217
+ // src/tilemap-renderer.ts
8218
+ var SVG_NS4 = "http://www.w3.org/2000/svg";
8219
+ var XLINK_NS3 = "http://www.w3.org/1999/xlink";
8220
+ var BRAND_URL4 = "https://tryopendata.ai";
8221
+ var EASE_VAR_MAP4 = {
8222
+ smooth: "var(--oc-ease-smooth)",
8223
+ snappy: "var(--oc-ease-snappy)"
8224
+ };
8225
+ var gradientIdCounter = 0;
8226
+ function createSVGElement3(tag) {
8227
+ return document.createElementNS(SVG_NS4, tag);
8228
+ }
8229
+ function setAttrs3(el, attrs) {
8230
+ for (const [key, value] of Object.entries(attrs)) {
8231
+ el.setAttribute(key, String(value));
8232
+ }
8233
+ }
8234
+ function renderChrome3(parent, layout) {
8235
+ const g = createSVGElement3("g");
8236
+ g.setAttribute("class", "oc-chrome");
8237
+ const { chrome } = layout;
8238
+ const bottomOffset = layout.area.y + layout.area.height;
8239
+ if (chrome.title) {
8240
+ const text = createSVGElement3("text");
8241
+ setAttrs3(text, { x: chrome.title.x, y: chrome.title.y });
8242
+ text.setAttribute("class", "oc-title");
8243
+ text.setAttribute("font-family", chrome.title.style.fontFamily);
8244
+ text.setAttribute("font-size", String(chrome.title.style.fontSize));
8245
+ text.setAttribute("font-weight", String(chrome.title.style.fontWeight));
8246
+ text.style.setProperty("fill", chrome.title.style.fill);
8247
+ text.textContent = chrome.title.text;
8248
+ g.appendChild(text);
8249
+ }
8250
+ if (chrome.subtitle) {
8251
+ const text = createSVGElement3("text");
8252
+ setAttrs3(text, { x: chrome.subtitle.x, y: chrome.subtitle.y });
8253
+ text.setAttribute("class", "oc-subtitle");
8254
+ text.setAttribute("font-family", chrome.subtitle.style.fontFamily);
8255
+ text.setAttribute("font-size", String(chrome.subtitle.style.fontSize));
8256
+ text.setAttribute("font-weight", String(chrome.subtitle.style.fontWeight));
8257
+ text.style.setProperty(
8258
+ "fill",
8259
+ chrome.subtitle.style.fill
8260
+ );
8261
+ text.textContent = chrome.subtitle.text;
8262
+ g.appendChild(text);
8263
+ }
8264
+ if (chrome.source) {
8265
+ const text = createSVGElement3("text");
8266
+ setAttrs3(text, { x: chrome.source.x, y: bottomOffset + chrome.source.y });
8267
+ text.setAttribute("class", "oc-source");
8268
+ text.setAttribute("font-family", chrome.source.style.fontFamily);
8269
+ text.setAttribute("font-size", String(chrome.source.style.fontSize));
8270
+ text.setAttribute("font-weight", String(chrome.source.style.fontWeight));
8271
+ text.style.setProperty(
8272
+ "fill",
8273
+ chrome.source.style.fill
8274
+ );
8275
+ text.textContent = chrome.source.text;
8276
+ g.appendChild(text);
8277
+ }
8278
+ if (chrome.byline) {
8279
+ const text = createSVGElement3("text");
8280
+ setAttrs3(text, { x: chrome.byline.x, y: bottomOffset + chrome.byline.y });
8281
+ text.setAttribute("class", "oc-byline");
8282
+ text.setAttribute("font-family", chrome.byline.style.fontFamily);
8283
+ text.setAttribute("font-size", String(chrome.byline.style.fontSize));
8284
+ text.setAttribute("font-weight", String(chrome.byline.style.fontWeight));
8285
+ text.style.setProperty(
8286
+ "fill",
8287
+ chrome.byline.style.fill
8288
+ );
8289
+ text.textContent = chrome.byline.text;
8290
+ g.appendChild(text);
8291
+ }
8292
+ if (chrome.footer) {
8293
+ const text = createSVGElement3("text");
8294
+ setAttrs3(text, { x: chrome.footer.x, y: bottomOffset + chrome.footer.y });
8295
+ text.setAttribute("class", "oc-footer");
8296
+ text.setAttribute("font-family", chrome.footer.style.fontFamily);
8297
+ text.setAttribute("font-size", String(chrome.footer.style.fontSize));
8298
+ text.setAttribute("font-weight", String(chrome.footer.style.fontWeight));
8299
+ text.style.setProperty(
8300
+ "fill",
8301
+ chrome.footer.style.fill
8302
+ );
8303
+ text.textContent = chrome.footer.text;
8304
+ g.appendChild(text);
8305
+ }
8306
+ parent.appendChild(g);
8307
+ }
8308
+ function renderWatermark(parent, layout) {
8309
+ if (layout.width < 480) return;
8310
+ const { width, height } = layout;
8311
+ const { theme } = layout;
8312
+ const padding = theme.spacing.padding;
8313
+ const rightEdge = width - padding;
8314
+ const bottomEdge = height - padding;
8315
+ const fill = theme.colors.axis;
8316
+ const a2 = createSVGElement3("a");
8317
+ a2.setAttribute("href", BRAND_URL4);
8318
+ a2.setAttributeNS(XLINK_NS3, "xlink:href", BRAND_URL4);
8319
+ a2.setAttribute("target", "_blank");
8320
+ a2.setAttribute("rel", "noopener");
8321
+ a2.setAttribute("class", "oc-chrome-ref");
8322
+ const text = createSVGElement3("text");
8323
+ setAttrs3(text, {
8324
+ x: rightEdge,
8325
+ y: bottomEdge,
8326
+ "dominant-baseline": "alphabetic",
8327
+ "text-anchor": "end",
8328
+ "font-family": theme.fonts.family,
8329
+ "font-size": 12,
8330
+ "fill-opacity": 0.55
8331
+ });
8332
+ text.style.setProperty("fill", fill);
8333
+ const trySpan = createSVGElement3("tspan");
8334
+ setAttrs3(trySpan, { "font-weight": 500 });
8335
+ trySpan.textContent = "try";
8336
+ const openDataSpan = createSVGElement3("tspan");
8337
+ setAttrs3(openDataSpan, { "font-weight": 600, "font-size": 16 });
8338
+ openDataSpan.textContent = "OpenData";
8339
+ const aiSpan = createSVGElement3("tspan");
8340
+ setAttrs3(aiSpan, { "font-weight": 500 });
8341
+ aiSpan.textContent = ".ai";
8342
+ text.appendChild(trySpan);
8343
+ text.appendChild(openDataSpan);
8344
+ text.appendChild(aiSpan);
8345
+ a2.appendChild(text);
8346
+ parent.appendChild(a2);
8347
+ }
8348
+ function renderTiles(parent, tiles, animation) {
8349
+ const g = createSVGElement3("g");
8350
+ g.setAttribute("class", "oc-tilemap-tiles");
8351
+ g.setAttribute("role", "list");
8352
+ const tileDelays = [];
8353
+ if (animation?.enabled) {
8354
+ const baseStagger = 800 / Math.max(tiles.length, 1);
8355
+ let seed = 17;
8356
+ for (let i = 0; i < tiles.length; i++) {
8357
+ const idx = tiles[i].animationIndex ?? i;
8358
+ seed = seed * 1103515245 + 12345 & 2147483647;
8359
+ const jitter = (seed % 1e3 / 1e3 - 0.5) * 0.8;
8360
+ tileDelays.push(Math.max(0, Math.round(idx * baseStagger * (1 + jitter))));
8361
+ }
8362
+ }
8363
+ for (let i = 0; i < tiles.length; i++) {
8364
+ const tile = tiles[i];
8365
+ const tileGroup = createSVGElement3("g");
8366
+ tileGroup.setAttribute("class", "oc-tilemap-tile");
8367
+ tileGroup.setAttribute("data-state", tile.stateCode);
8368
+ tileGroup.setAttribute("role", "listitem");
8369
+ if (tile.aria?.label) {
8370
+ tileGroup.setAttribute("aria-label", tile.aria.label);
8371
+ }
8372
+ if (animation?.enabled) {
8373
+ const idx = tile.animationIndex ?? i;
8374
+ tileGroup.setAttribute("data-animation-index", String(idx));
8375
+ tileGroup.style.setProperty(
8376
+ "--oc-mark-index",
8377
+ String(idx)
8378
+ );
8379
+ tileGroup.style.setProperty(
8380
+ "--oc-tile-delay",
8381
+ `${tileDelays[i]}ms`
8382
+ );
8383
+ }
8384
+ const rect = createSVGElement3("rect");
8385
+ setAttrs3(rect, {
8386
+ x: tile.x,
8387
+ y: tile.y,
8388
+ width: tile.size,
8389
+ height: tile.size,
8390
+ rx: tile.cornerRadius,
8391
+ fill: tile.fill,
8392
+ stroke: tile.stroke,
8393
+ "stroke-width": tile.strokeWidth
8394
+ });
8395
+ tileGroup.appendChild(rect);
8396
+ const codeLabel = createSVGElement3("text");
8397
+ setAttrs3(codeLabel, {
8398
+ x: tile.label.x,
8399
+ y: tile.label.y,
8400
+ "text-anchor": "middle",
8401
+ "dominant-baseline": "central",
8402
+ "font-family": tile.label.style.fontFamily,
8403
+ "font-size": tile.label.style.fontSize,
8404
+ "font-weight": tile.label.style.fontWeight
8405
+ });
8406
+ codeLabel.style.setProperty(
8407
+ "fill",
8408
+ tile.label.style.fill
8409
+ );
8410
+ codeLabel.textContent = tile.label.text;
8411
+ tileGroup.appendChild(codeLabel);
8412
+ if (tile.valueLabel.visible && tile.valueLabel.text) {
8413
+ const valueLabel = createSVGElement3("text");
8414
+ setAttrs3(valueLabel, {
8415
+ x: tile.valueLabel.x,
8416
+ y: tile.valueLabel.y,
8417
+ "text-anchor": "middle",
8418
+ "dominant-baseline": "central",
8419
+ "font-family": tile.valueLabel.style.fontFamily,
8420
+ "font-size": tile.valueLabel.style.fontSize,
8421
+ "font-weight": tile.valueLabel.style.fontWeight
8422
+ });
8423
+ valueLabel.style.setProperty(
8424
+ "fill",
8425
+ tile.valueLabel.style.fill
8426
+ );
8427
+ valueLabel.textContent = tile.valueLabel.text;
8428
+ tileGroup.appendChild(valueLabel);
8429
+ }
8430
+ g.appendChild(tileGroup);
8431
+ }
8432
+ parent.appendChild(g);
8433
+ }
8434
+ function renderGradientLegend(parent, layout) {
8435
+ if (!layout.gradientLegend) return;
8436
+ const { gradientLegend } = layout;
8437
+ const g = createSVGElement3("g");
8438
+ g.setAttribute("class", "oc-tilemap-legend");
8439
+ const defs = parent.querySelector("defs") || createSVGElement3("defs");
8440
+ const exists = parent.querySelector("defs");
8441
+ if (!exists) {
8442
+ parent.insertBefore(defs, parent.firstChild);
8443
+ }
8444
+ const gradientId = `oc-tilemap-legend-gradient-${gradientIdCounter++}`;
8445
+ const grad = createSVGElement3("linearGradient");
8446
+ grad.id = gradientId;
8447
+ grad.setAttribute("x1", "0%");
8448
+ grad.setAttribute("y1", "0%");
8449
+ grad.setAttribute("x2", "100%");
8450
+ grad.setAttribute("y2", "0%");
8451
+ for (const stop of gradientLegend.colorStops) {
8452
+ const s = createSVGElement3("stop");
8453
+ setAttrs3(s, { offset: `${stop.offset * 100}%`, "stop-color": stop.color });
8454
+ grad.appendChild(s);
8455
+ }
8456
+ defs.appendChild(grad);
8457
+ const bar = createSVGElement3("rect");
8458
+ setAttrs3(bar, {
8459
+ x: gradientLegend.bounds.x,
8460
+ y: gradientLegend.bounds.y,
8461
+ width: gradientLegend.bounds.width,
8462
+ height: gradientLegend.bounds.height,
8463
+ rx: 3,
8464
+ fill: `url(#${gradientId})`
8465
+ });
8466
+ g.appendChild(bar);
8467
+ const minText = createSVGElement3("text");
8468
+ setAttrs3(minText, {
8469
+ x: gradientLegend.bounds.x,
8470
+ y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
8471
+ "text-anchor": "start",
8472
+ "font-family": gradientLegend.labelStyle.fontFamily,
8473
+ "font-size": gradientLegend.labelStyle.fontSize,
8474
+ "font-weight": gradientLegend.labelStyle.fontWeight
8475
+ });
8476
+ minText.style.setProperty(
8477
+ "fill",
8478
+ gradientLegend.labelStyle.fill
8479
+ );
8480
+ minText.textContent = gradientLegend.minLabel;
8481
+ g.appendChild(minText);
8482
+ const maxText = createSVGElement3("text");
8483
+ setAttrs3(maxText, {
8484
+ x: gradientLegend.bounds.x + gradientLegend.bounds.width,
8485
+ y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
8486
+ "text-anchor": "end",
8487
+ "font-family": gradientLegend.labelStyle.fontFamily,
8488
+ "font-size": gradientLegend.labelStyle.fontSize,
8489
+ "font-weight": gradientLegend.labelStyle.fontWeight
8490
+ });
8491
+ maxText.style.setProperty(
8492
+ "fill",
8493
+ gradientLegend.labelStyle.fill
8494
+ );
8495
+ maxText.textContent = gradientLegend.maxLabel;
8496
+ g.appendChild(maxText);
8497
+ parent.appendChild(g);
8498
+ }
8499
+ function renderTileMapSVG(layout, opts) {
8500
+ const { width, height, tiles, a11y, watermark, animation } = layout;
8501
+ const animate = opts?.animate && animation?.enabled;
8502
+ const svg = createSVGElement3("svg");
8503
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
8504
+ svg.setAttribute("width", String(width));
8505
+ svg.setAttribute("height", String(height));
8506
+ svg.setAttribute("role", "img");
8507
+ if (a11y.altText) {
8508
+ svg.setAttribute("aria-label", a11y.altText);
8509
+ }
8510
+ const classes = animate ? "oc-tilemap oc-animate" : "oc-tilemap";
8511
+ svg.setAttribute("class", classes);
8512
+ if (animate && animation) {
8513
+ const stagger = Math.max(5, Math.round(800 / Math.max(tiles.length, 1)));
8514
+ svg.style.setProperty("--oc-animation-duration", `${animation.duration}ms`);
8515
+ svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
8516
+ svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
8517
+ const easeVar = EASE_VAR_MAP4[animation.ease] || EASE_VAR_MAP4.smooth;
8518
+ svg.style.setProperty("--oc-animation-ease", easeVar);
8519
+ }
8520
+ const defs = createSVGElement3("defs");
8521
+ svg.appendChild(defs);
8522
+ renderChrome3(svg, layout);
8523
+ renderTiles(svg, tiles, animate ? animation : void 0);
8524
+ renderGradientLegend(svg, layout);
8525
+ if (watermark) {
8526
+ renderWatermark(svg, layout);
8527
+ }
8528
+ return svg;
8529
+ }
8530
+
8531
+ // src/tilemap-mount.ts
8532
+ function resolveDarkMode5(mode) {
8533
+ if (mode === "force") return true;
8534
+ if (mode === "off" || mode === void 0) return false;
8535
+ if (typeof window !== "undefined" && window.matchMedia) {
8536
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
8537
+ }
8538
+ return false;
8539
+ }
8540
+ function createTileMap(container, spec, options) {
8541
+ let currentSpec = spec;
8542
+ let currentLayout;
8543
+ let destroyed = false;
8544
+ let svgElement = null;
8545
+ let tooltipManager = null;
8546
+ let cleanupTooltipEvents = null;
8547
+ let disconnectResize = null;
8548
+ let animationCleanup = null;
8549
+ let pendingResize = false;
8550
+ const measureText = createMeasureText();
8551
+ function getContainerDimensions() {
8552
+ const rect = container.getBoundingClientRect();
8553
+ return {
8554
+ width: Math.max(rect.width || 600, 100),
8555
+ height: Math.max(rect.height || 400, 100)
8556
+ };
8557
+ }
8558
+ function compile() {
8559
+ const { width, height } = getContainerDimensions();
8560
+ const darkMode = resolveDarkMode5(options?.darkMode);
8561
+ const compileOpts = {
8562
+ width,
8563
+ height,
8564
+ theme: options?.theme,
8565
+ darkMode,
8566
+ watermark: options?.watermark,
8567
+ measureText
8568
+ };
8569
+ return compileTileMap(currentSpec, compileOpts);
8570
+ }
8571
+ function wireTooltipAndInteraction(svg, layout) {
8572
+ const cleanups = [];
8573
+ const tileElements = svg.querySelectorAll(".oc-tilemap-tile");
8574
+ for (const el of tileElements) {
8575
+ const stateCode = el.getAttribute("data-state");
8576
+ if (!stateCode) continue;
8577
+ const content = layout.tooltipDescriptors.get(stateCode);
8578
+ const tile = layout.tiles.find((t) => t.stateCode === stateCode);
8579
+ const handleMouseEnter = (e) => {
8580
+ const mouseEvent = e;
8581
+ if (content && tooltipManager && options?.tooltip !== false) {
8582
+ const svgRect = svg.getBoundingClientRect();
8583
+ const x3 = mouseEvent.clientX - svgRect.left;
8584
+ const y3 = mouseEvent.clientY - svgRect.top;
8585
+ tooltipManager.show(content, x3, y3);
8586
+ }
8587
+ if (tile) {
8588
+ options?.onTileHover?.({
8589
+ stateCode: tile.stateCode,
8590
+ stateName: tile.data.stateName,
8591
+ value: tile.value,
8592
+ data: tile.data
8593
+ });
8594
+ }
8595
+ };
8596
+ const handleMouseMove = (e) => {
8597
+ if (content && tooltipManager && options?.tooltip !== false) {
8598
+ const mouseEvent = e;
8599
+ const svgRect = svg.getBoundingClientRect();
8600
+ const x3 = mouseEvent.clientX - svgRect.left;
8601
+ const y3 = mouseEvent.clientY - svgRect.top;
8602
+ tooltipManager.show(content, x3, y3);
8603
+ }
8604
+ };
8605
+ const handleMouseLeave = () => {
8606
+ tooltipManager?.hide();
8607
+ options?.onTileHover?.(null);
8608
+ };
8609
+ const handleClick = () => {
8610
+ if (tile) {
8611
+ options?.onTileClick?.({
8612
+ stateCode: tile.stateCode,
8613
+ stateName: tile.data.stateName,
8614
+ value: tile.value,
8615
+ data: tile.data
8616
+ });
8617
+ }
8618
+ };
8619
+ el.addEventListener("mouseenter", handleMouseEnter);
8620
+ el.addEventListener("mousemove", handleMouseMove);
8621
+ el.addEventListener("mouseleave", handleMouseLeave);
8622
+ el.addEventListener("click", handleClick);
8623
+ cleanups.push(() => {
8624
+ el.removeEventListener("mouseenter", handleMouseEnter);
8625
+ el.removeEventListener("mousemove", handleMouseMove);
8626
+ el.removeEventListener("mouseleave", handleMouseLeave);
8627
+ el.removeEventListener("click", handleClick);
8628
+ });
8629
+ }
8630
+ return () => {
8631
+ for (const cleanup of cleanups) {
8632
+ cleanup();
8633
+ }
8634
+ };
8635
+ }
8636
+ function render(animate = false) {
8637
+ if (animationCleanup) {
8638
+ animationCleanup();
8639
+ animationCleanup = null;
8640
+ }
8641
+ if (svgElement) {
8642
+ if (cleanupTooltipEvents) {
8643
+ cleanupTooltipEvents();
8644
+ cleanupTooltipEvents = null;
8645
+ }
8646
+ svgElement.remove();
8647
+ }
8648
+ const newSvg = renderTileMapSVG(currentLayout, { animate });
8649
+ container.appendChild(newSvg);
8650
+ svgElement = newSvg;
8651
+ cleanupTooltipEvents = wireTooltipAndInteraction(newSvg, currentLayout);
8652
+ if (options?.tooltip !== false) {
8653
+ if (!tooltipManager) {
8654
+ tooltipManager = createTooltipManager(container);
8655
+ }
8656
+ }
8657
+ if (currentLayout.animation?.enabled) {
8658
+ animationCleanup = setupAnimationCleanup(newSvg, () => {
8659
+ if (pendingResize && !destroyed) {
8660
+ pendingResize = false;
8661
+ resize();
8662
+ }
8663
+ });
8664
+ }
8665
+ }
8666
+ function update(newSpec) {
8667
+ currentSpec = newSpec;
8668
+ currentLayout = compile();
8669
+ render();
8670
+ }
8671
+ function resize() {
8672
+ if (destroyed) return;
8673
+ if (animationCleanup) {
8674
+ pendingResize = true;
8675
+ return;
8676
+ }
8677
+ currentLayout = compile();
8678
+ render();
8679
+ }
8680
+ function exportChart(format, options_) {
8681
+ if (!svgElement) return "";
8682
+ switch (format) {
8683
+ case "svg":
8684
+ return exportSVG(svgElement);
8685
+ case "svg-with-fonts":
8686
+ return exportSVGWithFonts(svgElement);
8687
+ case "png":
8688
+ return exportPNG(svgElement, options_);
8689
+ case "jpg":
8690
+ return exportJPG(svgElement, options_);
8691
+ default:
8692
+ return "";
8693
+ }
8694
+ }
8695
+ function destroy() {
8696
+ if (destroyed) return;
8697
+ destroyed = true;
8698
+ if (animationCleanup) {
8699
+ cancelAnimations(svgElement);
8700
+ animationCleanup();
8701
+ animationCleanup = null;
8702
+ }
8703
+ if (cleanupTooltipEvents) {
8704
+ cleanupTooltipEvents();
8705
+ cleanupTooltipEvents = null;
8706
+ }
8707
+ if (svgElement) {
8708
+ svgElement.remove();
8709
+ svgElement = null;
8710
+ }
8711
+ if (tooltipManager) {
8712
+ tooltipManager.destroy();
8713
+ tooltipManager = null;
8714
+ }
8715
+ if (disconnectResize) {
8716
+ disconnectResize();
8717
+ disconnectResize = null;
8718
+ }
8719
+ container.classList.remove("oc-tilemap-root", "oc-dark");
8720
+ }
8721
+ container.classList.add("oc-tilemap-root");
8722
+ if (resolveDarkMode5(options?.darkMode)) {
8723
+ container.classList.add("oc-dark");
8724
+ }
8725
+ currentLayout = compile();
8726
+ render(true);
8727
+ if (options?.responsive !== false) {
8728
+ disconnectResize = observeResize(container, () => {
8729
+ resize();
8730
+ });
8731
+ }
8732
+ return {
8733
+ update,
8734
+ resize,
8735
+ export: exportChart,
8736
+ destroy,
8737
+ get layout() {
8738
+ return currentLayout;
8739
+ }
8740
+ };
8741
+ }
8128
8742
  export {
8129
8743
  attachKeyboardNav,
8130
8744
  createChart,
@@ -8133,6 +8747,7 @@ export {
8133
8747
  createSimulationWorker,
8134
8748
  createTable,
8135
8749
  createTextEditOverlay,
8750
+ createTileMap,
8136
8751
  createTooltipManager,
8137
8752
  exportCSV,
8138
8753
  exportJPG,