@opendata-ai/openchart-vanilla 6.25.4 → 6.27.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");
@@ -4364,6 +4414,9 @@ function renderChartSVG(layout, container, opts) {
4364
4414
  svg.style.height = `${height}px`;
4365
4415
  svg.setAttribute("role", layout.a11y.role);
4366
4416
  svg.setAttribute("aria-label", layout.a11y.altText);
4417
+ if (layout.display === "sparkline") {
4418
+ svg.setAttribute("data-display", "sparkline");
4419
+ }
4367
4420
  const classes = opts?.animate ? "oc-chart oc-animate" : "oc-chart";
4368
4421
  svg.setAttribute("class", classes);
4369
4422
  if (animation?.enabled) {
@@ -4434,6 +4487,24 @@ function renderChartSVG(layout, container, opts) {
4434
4487
  overlay.setAttribute("class", "oc-voronoi-overlay");
4435
4488
  overlay.setAttribute("data-voronoi-overlay", "true");
4436
4489
  clippedGroup.appendChild(overlay);
4490
+ if (opts?.crosshair) {
4491
+ const crosshairLine = createSVGElement("line");
4492
+ crosshairLine.setAttribute("data-crosshair", "true");
4493
+ crosshairLine.setAttribute("class", "oc-crosshair");
4494
+ setAttrs(crosshairLine, {
4495
+ x1: 0,
4496
+ y1: layout.area.y,
4497
+ x2: 0,
4498
+ y2: layout.area.y + layout.area.height,
4499
+ stroke: layout.theme.colors.gridline,
4500
+ "stroke-opacity": "0.5",
4501
+ "stroke-dasharray": "4,3",
4502
+ "stroke-width": "1",
4503
+ "pointer-events": "none"
4504
+ });
4505
+ crosshairLine.style.display = "none";
4506
+ clippedGroup.appendChild(crosshairLine);
4507
+ }
4437
4508
  }
4438
4509
  svg.appendChild(clippedGroup);
4439
4510
  renderAnnotations(svg, layout);
@@ -4695,6 +4766,7 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4695
4766
  const voronoiPoints = collectVoronoiPoints(layout);
4696
4767
  if (voronoiPoints.length === 0) return () => {
4697
4768
  };
4769
+ const crosshair = svg.querySelector("[data-crosshair]");
4698
4770
  const cleanups = [];
4699
4771
  const handleMouseMove = (e) => {
4700
4772
  const mouseEvent = e;
@@ -4707,11 +4779,17 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4707
4779
  const svgY = (mouseEvent.clientY - svgRect.top) * scaleY;
4708
4780
  const nearest = findNearestPoint(voronoiPoints, svgX, svgY);
4709
4781
  if (!nearest?.tooltip) return;
4782
+ if (crosshair) {
4783
+ crosshair.setAttribute("x1", String(nearest.x));
4784
+ crosshair.setAttribute("x2", String(nearest.x));
4785
+ crosshair.style.display = "";
4786
+ }
4710
4787
  const containerX = mouseEvent.clientX - svgRect.left;
4711
4788
  const containerY = mouseEvent.clientY - svgRect.top;
4712
4789
  tooltipManager.show(nearest.tooltip, containerX, containerY);
4713
4790
  };
4714
4791
  const handleMouseLeave = () => {
4792
+ if (crosshair) crosshair.style.display = "none";
4715
4793
  tooltipManager.hide();
4716
4794
  };
4717
4795
  const handleTouchStart = (e) => {
@@ -4727,6 +4805,11 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
4727
4805
  const svgY = (touch.clientY - svgRect.top) * scaleY;
4728
4806
  const nearest = findNearestPoint(voronoiPoints, svgX, svgY);
4729
4807
  if (!nearest?.tooltip) return;
4808
+ if (crosshair) {
4809
+ crosshair.setAttribute("x1", String(nearest.x));
4810
+ crosshair.setAttribute("x2", String(nearest.x));
4811
+ crosshair.style.display = "";
4812
+ }
4730
4813
  const containerX = touch.clientX - svgRect.left;
4731
4814
  const containerY = touch.clientY - svgRect.top;
4732
4815
  tooltipManager.show(nearest.tooltip, containerX, containerY);
@@ -5058,7 +5141,7 @@ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setD
5058
5141
  };
5059
5142
  }
5060
5143
  function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
5061
- const SVG_NS4 = "http://www.w3.org/2000/svg";
5144
+ const SVG_NS5 = "http://www.w3.org/2000/svg";
5062
5145
  const cleanups = [];
5063
5146
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
5064
5147
  for (const el of annotationGroups) {
@@ -5097,7 +5180,7 @@ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
5097
5180
  const createdHandles = [];
5098
5181
  for (const ep of endpoints) {
5099
5182
  if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
5100
- const handleEl = document.createElementNS(SVG_NS4, "circle");
5183
+ const handleEl = document.createElementNS(SVG_NS5, "circle");
5101
5184
  handleEl.setAttribute("class", "oc-connector-handle");
5102
5185
  handleEl.setAttribute("data-endpoint", ep.name);
5103
5186
  handleEl.setAttribute("cx", String(ep.cx));
@@ -5612,7 +5695,7 @@ function getEditableElements(spec, layout) {
5612
5695
  for (const series of seriesLabels) {
5613
5696
  refs.push(elementRef.seriesLabel(series));
5614
5697
  }
5615
- if (layout.legend.entries.length > 0) {
5698
+ if ("entries" in layout.legend && layout.legend.entries.length > 0) {
5616
5699
  refs.push(elementRef.legend());
5617
5700
  }
5618
5701
  return refs;
@@ -5733,6 +5816,20 @@ function createChart(container, spec, options) {
5733
5816
  }
5734
5817
  function getContainerDimensions() {
5735
5818
  const rect = container.getBoundingClientRect();
5819
+ const isSparkline = "display" in currentSpec && currentSpec.display === "sparkline";
5820
+ if (isSparkline) {
5821
+ let width = rect.width;
5822
+ let height = rect.height;
5823
+ if (!height && container.parentElement) {
5824
+ const parentRect = container.parentElement.getBoundingClientRect();
5825
+ height = parentRect.height;
5826
+ if (!width) width = parentRect.width;
5827
+ }
5828
+ return {
5829
+ width: Math.max(width || 200, 30),
5830
+ height: Math.max(height || 40, 20)
5831
+ };
5832
+ }
5736
5833
  return {
5737
5834
  width: Math.max(rect.width || 600, 100),
5738
5835
  height: Math.max(rect.height || 400, 100)
@@ -5994,7 +6091,11 @@ function createChart(container, spec, options) {
5994
6091
  }
5995
6092
  currentLayout = compile();
5996
6093
  const shouldAnimate = isFirstRender && !!currentLayout.animation?.enabled;
5997
- svgElement = renderChartSVG(currentLayout, container, { animate: shouldAnimate });
6094
+ const crosshair = !!currentLayout.crosshair;
6095
+ svgElement = renderChartSVG(currentLayout, container, {
6096
+ animate: shouldAnimate,
6097
+ crosshair
6098
+ });
5998
6099
  tooltipManager = createTooltipManager(container);
5999
6100
  cleanupTooltipEvents = wireTooltipEvents(
6000
6101
  svgElement,
@@ -6706,8 +6807,9 @@ function renderBrand2(parent, layout) {
6706
6807
  a2.appendChild(text);
6707
6808
  parent.appendChild(a2);
6708
6809
  }
6709
- function renderLegend2(parent, legend) {
6710
- if (legend.entries.length === 0) return;
6810
+ function renderLegend2(parent, legendLayout) {
6811
+ if (!("entries" in legendLayout) || legendLayout.entries.length === 0) return;
6812
+ const legend = legendLayout;
6711
6813
  const g = createSVGElement2("g");
6712
6814
  g.setAttribute("class", "oc-legend");
6713
6815
  g.setAttribute("role", "list");
@@ -8125,6 +8227,535 @@ function createTable(container, spec, options) {
8125
8227
  destroy
8126
8228
  };
8127
8229
  }
8230
+
8231
+ // src/tilemap-mount.ts
8232
+ import { compileTileMap } from "@opendata-ai/openchart-engine";
8233
+
8234
+ // src/tilemap-renderer.ts
8235
+ var SVG_NS4 = "http://www.w3.org/2000/svg";
8236
+ var XLINK_NS3 = "http://www.w3.org/1999/xlink";
8237
+ var BRAND_URL4 = "https://tryopendata.ai";
8238
+ var EASE_VAR_MAP4 = {
8239
+ smooth: "var(--oc-ease-smooth)",
8240
+ snappy: "var(--oc-ease-snappy)"
8241
+ };
8242
+ var gradientIdCounter = 0;
8243
+ function createSVGElement3(tag) {
8244
+ return document.createElementNS(SVG_NS4, tag);
8245
+ }
8246
+ function setAttrs3(el, attrs) {
8247
+ for (const [key, value] of Object.entries(attrs)) {
8248
+ el.setAttribute(key, String(value));
8249
+ }
8250
+ }
8251
+ function renderChrome3(parent, layout) {
8252
+ const g = createSVGElement3("g");
8253
+ g.setAttribute("class", "oc-chrome");
8254
+ const { chrome } = layout;
8255
+ const bottomOffset = layout.area.y + layout.area.height;
8256
+ if (chrome.title) {
8257
+ const text = createSVGElement3("text");
8258
+ setAttrs3(text, { x: chrome.title.x, y: chrome.title.y });
8259
+ text.setAttribute("class", "oc-title");
8260
+ text.setAttribute("font-family", chrome.title.style.fontFamily);
8261
+ text.setAttribute("font-size", String(chrome.title.style.fontSize));
8262
+ text.setAttribute("font-weight", String(chrome.title.style.fontWeight));
8263
+ text.style.setProperty("fill", chrome.title.style.fill);
8264
+ text.textContent = chrome.title.text;
8265
+ g.appendChild(text);
8266
+ }
8267
+ if (chrome.subtitle) {
8268
+ const text = createSVGElement3("text");
8269
+ setAttrs3(text, { x: chrome.subtitle.x, y: chrome.subtitle.y });
8270
+ text.setAttribute("class", "oc-subtitle");
8271
+ text.setAttribute("font-family", chrome.subtitle.style.fontFamily);
8272
+ text.setAttribute("font-size", String(chrome.subtitle.style.fontSize));
8273
+ text.setAttribute("font-weight", String(chrome.subtitle.style.fontWeight));
8274
+ text.style.setProperty(
8275
+ "fill",
8276
+ chrome.subtitle.style.fill
8277
+ );
8278
+ text.textContent = chrome.subtitle.text;
8279
+ g.appendChild(text);
8280
+ }
8281
+ if (chrome.source) {
8282
+ const text = createSVGElement3("text");
8283
+ setAttrs3(text, { x: chrome.source.x, y: bottomOffset + chrome.source.y });
8284
+ text.setAttribute("class", "oc-source");
8285
+ text.setAttribute("font-family", chrome.source.style.fontFamily);
8286
+ text.setAttribute("font-size", String(chrome.source.style.fontSize));
8287
+ text.setAttribute("font-weight", String(chrome.source.style.fontWeight));
8288
+ text.style.setProperty(
8289
+ "fill",
8290
+ chrome.source.style.fill
8291
+ );
8292
+ text.textContent = chrome.source.text;
8293
+ g.appendChild(text);
8294
+ }
8295
+ if (chrome.byline) {
8296
+ const text = createSVGElement3("text");
8297
+ setAttrs3(text, { x: chrome.byline.x, y: bottomOffset + chrome.byline.y });
8298
+ text.setAttribute("class", "oc-byline");
8299
+ text.setAttribute("font-family", chrome.byline.style.fontFamily);
8300
+ text.setAttribute("font-size", String(chrome.byline.style.fontSize));
8301
+ text.setAttribute("font-weight", String(chrome.byline.style.fontWeight));
8302
+ text.style.setProperty(
8303
+ "fill",
8304
+ chrome.byline.style.fill
8305
+ );
8306
+ text.textContent = chrome.byline.text;
8307
+ g.appendChild(text);
8308
+ }
8309
+ if (chrome.footer) {
8310
+ const text = createSVGElement3("text");
8311
+ setAttrs3(text, { x: chrome.footer.x, y: bottomOffset + chrome.footer.y });
8312
+ text.setAttribute("class", "oc-footer");
8313
+ text.setAttribute("font-family", chrome.footer.style.fontFamily);
8314
+ text.setAttribute("font-size", String(chrome.footer.style.fontSize));
8315
+ text.setAttribute("font-weight", String(chrome.footer.style.fontWeight));
8316
+ text.style.setProperty(
8317
+ "fill",
8318
+ chrome.footer.style.fill
8319
+ );
8320
+ text.textContent = chrome.footer.text;
8321
+ g.appendChild(text);
8322
+ }
8323
+ parent.appendChild(g);
8324
+ }
8325
+ function renderWatermark(parent, layout) {
8326
+ if (layout.width < 480) return;
8327
+ const { width, height } = layout;
8328
+ const { theme } = layout;
8329
+ const padding = theme.spacing.padding;
8330
+ const rightEdge = width - padding;
8331
+ const bottomEdge = height - padding;
8332
+ const fill = theme.colors.axis;
8333
+ const a2 = createSVGElement3("a");
8334
+ a2.setAttribute("href", BRAND_URL4);
8335
+ a2.setAttributeNS(XLINK_NS3, "xlink:href", BRAND_URL4);
8336
+ a2.setAttribute("target", "_blank");
8337
+ a2.setAttribute("rel", "noopener");
8338
+ a2.setAttribute("class", "oc-chrome-ref");
8339
+ const text = createSVGElement3("text");
8340
+ setAttrs3(text, {
8341
+ x: rightEdge,
8342
+ y: bottomEdge,
8343
+ "dominant-baseline": "alphabetic",
8344
+ "text-anchor": "end",
8345
+ "font-family": theme.fonts.family,
8346
+ "font-size": 12,
8347
+ "fill-opacity": 0.55
8348
+ });
8349
+ text.style.setProperty("fill", fill);
8350
+ const trySpan = createSVGElement3("tspan");
8351
+ setAttrs3(trySpan, { "font-weight": 500 });
8352
+ trySpan.textContent = "try";
8353
+ const openDataSpan = createSVGElement3("tspan");
8354
+ setAttrs3(openDataSpan, { "font-weight": 600, "font-size": 16 });
8355
+ openDataSpan.textContent = "OpenData";
8356
+ const aiSpan = createSVGElement3("tspan");
8357
+ setAttrs3(aiSpan, { "font-weight": 500 });
8358
+ aiSpan.textContent = ".ai";
8359
+ text.appendChild(trySpan);
8360
+ text.appendChild(openDataSpan);
8361
+ text.appendChild(aiSpan);
8362
+ a2.appendChild(text);
8363
+ parent.appendChild(a2);
8364
+ }
8365
+ function renderTiles(parent, tiles, animation) {
8366
+ const g = createSVGElement3("g");
8367
+ g.setAttribute("class", "oc-tilemap-tiles");
8368
+ g.setAttribute("role", "list");
8369
+ const tileDelays = [];
8370
+ if (animation?.enabled) {
8371
+ const baseStagger = 800 / Math.max(tiles.length, 1);
8372
+ let seed = 17;
8373
+ for (let i = 0; i < tiles.length; i++) {
8374
+ const idx = tiles[i].animationIndex ?? i;
8375
+ seed = seed * 1103515245 + 12345 & 2147483647;
8376
+ const jitter = (seed % 1e3 / 1e3 - 0.5) * 0.8;
8377
+ tileDelays.push(Math.max(0, Math.round(idx * baseStagger * (1 + jitter))));
8378
+ }
8379
+ }
8380
+ for (let i = 0; i < tiles.length; i++) {
8381
+ const tile = tiles[i];
8382
+ const tileGroup = createSVGElement3("g");
8383
+ tileGroup.setAttribute("class", "oc-tilemap-tile");
8384
+ tileGroup.setAttribute("data-state", tile.stateCode);
8385
+ tileGroup.setAttribute("role", "listitem");
8386
+ if (tile.aria?.label) {
8387
+ tileGroup.setAttribute("aria-label", tile.aria.label);
8388
+ }
8389
+ if (animation?.enabled) {
8390
+ const idx = tile.animationIndex ?? i;
8391
+ tileGroup.setAttribute("data-animation-index", String(idx));
8392
+ tileGroup.style.setProperty(
8393
+ "--oc-mark-index",
8394
+ String(idx)
8395
+ );
8396
+ tileGroup.style.setProperty(
8397
+ "--oc-tile-delay",
8398
+ `${tileDelays[i]}ms`
8399
+ );
8400
+ }
8401
+ const rect = createSVGElement3("rect");
8402
+ setAttrs3(rect, {
8403
+ x: tile.x,
8404
+ y: tile.y,
8405
+ width: tile.size,
8406
+ height: tile.size,
8407
+ rx: tile.cornerRadius,
8408
+ fill: tile.fill,
8409
+ stroke: tile.stroke,
8410
+ "stroke-width": tile.strokeWidth
8411
+ });
8412
+ tileGroup.appendChild(rect);
8413
+ const codeLabel = createSVGElement3("text");
8414
+ setAttrs3(codeLabel, {
8415
+ x: tile.label.x,
8416
+ y: tile.label.y,
8417
+ "text-anchor": "middle",
8418
+ "dominant-baseline": "central",
8419
+ "font-family": tile.label.style.fontFamily,
8420
+ "font-size": tile.label.style.fontSize,
8421
+ "font-weight": tile.label.style.fontWeight
8422
+ });
8423
+ codeLabel.style.setProperty(
8424
+ "fill",
8425
+ tile.label.style.fill
8426
+ );
8427
+ codeLabel.textContent = tile.label.text;
8428
+ tileGroup.appendChild(codeLabel);
8429
+ if (tile.valueLabel.visible && tile.valueLabel.text) {
8430
+ const valueLabel = createSVGElement3("text");
8431
+ setAttrs3(valueLabel, {
8432
+ x: tile.valueLabel.x,
8433
+ y: tile.valueLabel.y,
8434
+ "text-anchor": "middle",
8435
+ "dominant-baseline": "central",
8436
+ "font-family": tile.valueLabel.style.fontFamily,
8437
+ "font-size": tile.valueLabel.style.fontSize,
8438
+ "font-weight": tile.valueLabel.style.fontWeight
8439
+ });
8440
+ valueLabel.style.setProperty(
8441
+ "fill",
8442
+ tile.valueLabel.style.fill
8443
+ );
8444
+ valueLabel.textContent = tile.valueLabel.text;
8445
+ tileGroup.appendChild(valueLabel);
8446
+ }
8447
+ g.appendChild(tileGroup);
8448
+ }
8449
+ parent.appendChild(g);
8450
+ }
8451
+ function renderGradientLegend(parent, layout) {
8452
+ if (!layout.gradientLegend) return;
8453
+ const { gradientLegend } = layout;
8454
+ const g = createSVGElement3("g");
8455
+ g.setAttribute("class", "oc-tilemap-legend");
8456
+ const defs = parent.querySelector("defs") || createSVGElement3("defs");
8457
+ const exists = parent.querySelector("defs");
8458
+ if (!exists) {
8459
+ parent.insertBefore(defs, parent.firstChild);
8460
+ }
8461
+ const gradientId = `oc-tilemap-legend-gradient-${gradientIdCounter++}`;
8462
+ const grad = createSVGElement3("linearGradient");
8463
+ grad.id = gradientId;
8464
+ grad.setAttribute("x1", "0%");
8465
+ grad.setAttribute("y1", "0%");
8466
+ grad.setAttribute("x2", "100%");
8467
+ grad.setAttribute("y2", "0%");
8468
+ for (const stop of gradientLegend.colorStops) {
8469
+ const s = createSVGElement3("stop");
8470
+ setAttrs3(s, { offset: `${stop.offset * 100}%`, "stop-color": stop.color });
8471
+ grad.appendChild(s);
8472
+ }
8473
+ defs.appendChild(grad);
8474
+ const bar = createSVGElement3("rect");
8475
+ setAttrs3(bar, {
8476
+ x: gradientLegend.bounds.x,
8477
+ y: gradientLegend.bounds.y,
8478
+ width: gradientLegend.bounds.width,
8479
+ height: gradientLegend.bounds.height,
8480
+ rx: 3,
8481
+ fill: `url(#${gradientId})`
8482
+ });
8483
+ g.appendChild(bar);
8484
+ const minText = createSVGElement3("text");
8485
+ setAttrs3(minText, {
8486
+ x: gradientLegend.bounds.x,
8487
+ y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
8488
+ "text-anchor": "start",
8489
+ "font-family": gradientLegend.labelStyle.fontFamily,
8490
+ "font-size": gradientLegend.labelStyle.fontSize,
8491
+ "font-weight": gradientLegend.labelStyle.fontWeight
8492
+ });
8493
+ minText.style.setProperty(
8494
+ "fill",
8495
+ gradientLegend.labelStyle.fill
8496
+ );
8497
+ minText.textContent = gradientLegend.minLabel;
8498
+ g.appendChild(minText);
8499
+ const maxText = createSVGElement3("text");
8500
+ setAttrs3(maxText, {
8501
+ x: gradientLegend.bounds.x + gradientLegend.bounds.width,
8502
+ y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
8503
+ "text-anchor": "end",
8504
+ "font-family": gradientLegend.labelStyle.fontFamily,
8505
+ "font-size": gradientLegend.labelStyle.fontSize,
8506
+ "font-weight": gradientLegend.labelStyle.fontWeight
8507
+ });
8508
+ maxText.style.setProperty(
8509
+ "fill",
8510
+ gradientLegend.labelStyle.fill
8511
+ );
8512
+ maxText.textContent = gradientLegend.maxLabel;
8513
+ g.appendChild(maxText);
8514
+ parent.appendChild(g);
8515
+ }
8516
+ function renderTileMapSVG(layout, opts) {
8517
+ const { width, height, tiles, a11y, watermark, animation } = layout;
8518
+ const animate = opts?.animate && animation?.enabled;
8519
+ const svg = createSVGElement3("svg");
8520
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
8521
+ svg.setAttribute("width", String(width));
8522
+ svg.setAttribute("height", String(height));
8523
+ svg.setAttribute("role", "img");
8524
+ if (a11y.altText) {
8525
+ svg.setAttribute("aria-label", a11y.altText);
8526
+ }
8527
+ const classes = animate ? "oc-tilemap oc-animate" : "oc-tilemap";
8528
+ svg.setAttribute("class", classes);
8529
+ if (animate && animation) {
8530
+ const stagger = Math.max(5, Math.round(800 / Math.max(tiles.length, 1)));
8531
+ svg.style.setProperty("--oc-animation-duration", `${animation.duration}ms`);
8532
+ svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
8533
+ svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
8534
+ const easeVar = EASE_VAR_MAP4[animation.ease] || EASE_VAR_MAP4.smooth;
8535
+ svg.style.setProperty("--oc-animation-ease", easeVar);
8536
+ }
8537
+ const defs = createSVGElement3("defs");
8538
+ svg.appendChild(defs);
8539
+ renderChrome3(svg, layout);
8540
+ renderTiles(svg, tiles, animate ? animation : void 0);
8541
+ renderGradientLegend(svg, layout);
8542
+ if (watermark) {
8543
+ renderWatermark(svg, layout);
8544
+ }
8545
+ return svg;
8546
+ }
8547
+
8548
+ // src/tilemap-mount.ts
8549
+ function resolveDarkMode5(mode) {
8550
+ if (mode === "force") return true;
8551
+ if (mode === "off" || mode === void 0) return false;
8552
+ if (typeof window !== "undefined" && window.matchMedia) {
8553
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
8554
+ }
8555
+ return false;
8556
+ }
8557
+ function createTileMap(container, spec, options) {
8558
+ let currentSpec = spec;
8559
+ let currentLayout;
8560
+ let destroyed = false;
8561
+ let svgElement = null;
8562
+ let tooltipManager = null;
8563
+ let cleanupTooltipEvents = null;
8564
+ let disconnectResize = null;
8565
+ let animationCleanup = null;
8566
+ let pendingResize = false;
8567
+ const measureText = createMeasureText();
8568
+ function getContainerDimensions() {
8569
+ const rect = container.getBoundingClientRect();
8570
+ return {
8571
+ width: Math.max(rect.width || 600, 100),
8572
+ height: Math.max(rect.height || 400, 100)
8573
+ };
8574
+ }
8575
+ function compile() {
8576
+ const { width, height } = getContainerDimensions();
8577
+ const darkMode = resolveDarkMode5(options?.darkMode);
8578
+ const compileOpts = {
8579
+ width,
8580
+ height,
8581
+ theme: options?.theme,
8582
+ darkMode,
8583
+ watermark: options?.watermark,
8584
+ measureText
8585
+ };
8586
+ return compileTileMap(currentSpec, compileOpts);
8587
+ }
8588
+ function wireTooltipAndInteraction(svg, layout) {
8589
+ const cleanups = [];
8590
+ const tileElements = svg.querySelectorAll(".oc-tilemap-tile");
8591
+ for (const el of tileElements) {
8592
+ const stateCode = el.getAttribute("data-state");
8593
+ if (!stateCode) continue;
8594
+ const content = layout.tooltipDescriptors.get(stateCode);
8595
+ const tile = layout.tiles.find((t) => t.stateCode === stateCode);
8596
+ const handleMouseEnter = (e) => {
8597
+ const mouseEvent = e;
8598
+ if (content && tooltipManager && options?.tooltip !== false) {
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
+ if (tile) {
8605
+ options?.onTileHover?.({
8606
+ stateCode: tile.stateCode,
8607
+ stateName: tile.data.stateName,
8608
+ value: tile.value,
8609
+ data: tile.data
8610
+ });
8611
+ }
8612
+ };
8613
+ const handleMouseMove = (e) => {
8614
+ if (content && tooltipManager && options?.tooltip !== false) {
8615
+ const mouseEvent = e;
8616
+ const svgRect = svg.getBoundingClientRect();
8617
+ const x3 = mouseEvent.clientX - svgRect.left;
8618
+ const y3 = mouseEvent.clientY - svgRect.top;
8619
+ tooltipManager.show(content, x3, y3);
8620
+ }
8621
+ };
8622
+ const handleMouseLeave = () => {
8623
+ tooltipManager?.hide();
8624
+ options?.onTileHover?.(null);
8625
+ };
8626
+ const handleClick = () => {
8627
+ if (tile) {
8628
+ options?.onTileClick?.({
8629
+ stateCode: tile.stateCode,
8630
+ stateName: tile.data.stateName,
8631
+ value: tile.value,
8632
+ data: tile.data
8633
+ });
8634
+ }
8635
+ };
8636
+ el.addEventListener("mouseenter", handleMouseEnter);
8637
+ el.addEventListener("mousemove", handleMouseMove);
8638
+ el.addEventListener("mouseleave", handleMouseLeave);
8639
+ el.addEventListener("click", handleClick);
8640
+ cleanups.push(() => {
8641
+ el.removeEventListener("mouseenter", handleMouseEnter);
8642
+ el.removeEventListener("mousemove", handleMouseMove);
8643
+ el.removeEventListener("mouseleave", handleMouseLeave);
8644
+ el.removeEventListener("click", handleClick);
8645
+ });
8646
+ }
8647
+ return () => {
8648
+ for (const cleanup of cleanups) {
8649
+ cleanup();
8650
+ }
8651
+ };
8652
+ }
8653
+ function render(animate = false) {
8654
+ if (animationCleanup) {
8655
+ animationCleanup();
8656
+ animationCleanup = null;
8657
+ }
8658
+ if (svgElement) {
8659
+ if (cleanupTooltipEvents) {
8660
+ cleanupTooltipEvents();
8661
+ cleanupTooltipEvents = null;
8662
+ }
8663
+ svgElement.remove();
8664
+ }
8665
+ const newSvg = renderTileMapSVG(currentLayout, { animate });
8666
+ container.appendChild(newSvg);
8667
+ svgElement = newSvg;
8668
+ cleanupTooltipEvents = wireTooltipAndInteraction(newSvg, currentLayout);
8669
+ if (options?.tooltip !== false) {
8670
+ if (!tooltipManager) {
8671
+ tooltipManager = createTooltipManager(container);
8672
+ }
8673
+ }
8674
+ if (currentLayout.animation?.enabled) {
8675
+ animationCleanup = setupAnimationCleanup(newSvg, () => {
8676
+ if (pendingResize && !destroyed) {
8677
+ pendingResize = false;
8678
+ resize();
8679
+ }
8680
+ });
8681
+ }
8682
+ }
8683
+ function update(newSpec) {
8684
+ currentSpec = newSpec;
8685
+ currentLayout = compile();
8686
+ render();
8687
+ }
8688
+ function resize() {
8689
+ if (destroyed) return;
8690
+ if (animationCleanup) {
8691
+ pendingResize = true;
8692
+ return;
8693
+ }
8694
+ currentLayout = compile();
8695
+ render();
8696
+ }
8697
+ function exportChart(format, options_) {
8698
+ if (!svgElement) return "";
8699
+ switch (format) {
8700
+ case "svg":
8701
+ return exportSVG(svgElement);
8702
+ case "svg-with-fonts":
8703
+ return exportSVGWithFonts(svgElement);
8704
+ case "png":
8705
+ return exportPNG(svgElement, options_);
8706
+ case "jpg":
8707
+ return exportJPG(svgElement, options_);
8708
+ default:
8709
+ return "";
8710
+ }
8711
+ }
8712
+ function destroy() {
8713
+ if (destroyed) return;
8714
+ destroyed = true;
8715
+ if (animationCleanup) {
8716
+ cancelAnimations(svgElement);
8717
+ animationCleanup();
8718
+ animationCleanup = null;
8719
+ }
8720
+ if (cleanupTooltipEvents) {
8721
+ cleanupTooltipEvents();
8722
+ cleanupTooltipEvents = null;
8723
+ }
8724
+ if (svgElement) {
8725
+ svgElement.remove();
8726
+ svgElement = null;
8727
+ }
8728
+ if (tooltipManager) {
8729
+ tooltipManager.destroy();
8730
+ tooltipManager = null;
8731
+ }
8732
+ if (disconnectResize) {
8733
+ disconnectResize();
8734
+ disconnectResize = null;
8735
+ }
8736
+ container.classList.remove("oc-tilemap-root", "oc-dark");
8737
+ }
8738
+ container.classList.add("oc-tilemap-root");
8739
+ if (resolveDarkMode5(options?.darkMode)) {
8740
+ container.classList.add("oc-dark");
8741
+ }
8742
+ currentLayout = compile();
8743
+ render(true);
8744
+ if (options?.responsive !== false) {
8745
+ disconnectResize = observeResize(container, () => {
8746
+ resize();
8747
+ });
8748
+ }
8749
+ return {
8750
+ update,
8751
+ resize,
8752
+ export: exportChart,
8753
+ destroy,
8754
+ get layout() {
8755
+ return currentLayout;
8756
+ }
8757
+ };
8758
+ }
8128
8759
  export {
8129
8760
  attachKeyboardNav,
8130
8761
  createChart,
@@ -8133,6 +8764,7 @@ export {
8133
8764
  createSimulationWorker,
8134
8765
  createTable,
8135
8766
  createTextEditOverlay,
8767
+ createTileMap,
8136
8768
  createTooltipManager,
8137
8769
  exportCSV,
8138
8770
  exportJPG,