@opendata-ai/openchart-vanilla 8.3.0 → 8.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
@@ -3,10 +3,10 @@ import {
3
3
  paletteFromCanvas,
4
4
  readCanvasSRGB,
5
5
  resolveRepeat
6
- } from "./chunk-DCODCLWI.js";
6
+ } from "./chunk-FETYAVTZ.js";
7
7
  import {
8
8
  renderTileMapSVG
9
- } from "./chunk-737VNBU6.js";
9
+ } from "./chunk-LNPRDSK3.js";
10
10
  import {
11
11
  AnimationScheduler,
12
12
  CANVAS_DEFAULT_UPDATE_MAX_MARKS,
@@ -28,10 +28,11 @@ import {
28
28
  resolveDarkMode,
29
29
  resolveEase,
30
30
  resolveFontFamily,
31
+ resolvedSurface,
31
32
  scheduleFontReload,
32
33
  setupAnimationCleanup,
33
34
  setupTableAnimationCleanup
34
- } from "./chunk-PEIX32S3.js";
35
+ } from "./chunk-45XZOWYC.js";
35
36
  import {
36
37
  embedFonts,
37
38
  ensureSVGDimensions,
@@ -43,16 +44,16 @@ import {
43
44
  getSVGBackgroundColor,
44
45
  getSVGDimensions,
45
46
  rasterizeSVGToCanvas
46
- } from "./chunk-T32UHMZA.js";
47
+ } from "./chunk-UOB422LY.js";
47
48
  import {
48
49
  rectPathWithCorners,
49
50
  registerMarkRenderer,
50
51
  renderChartSVG,
51
52
  stampAnimationVars
52
- } from "./chunk-P32LUJFK.js";
53
+ } from "./chunk-YGYMB7KB.js";
53
54
  import {
54
55
  injectThemeStyleBlock
55
- } from "./chunk-RM5XS6NR.js";
56
+ } from "./chunk-HGRNXLDF.js";
56
57
 
57
58
  // src/barlist-mount.ts
58
59
  import { compileBarList } from "@opendata-ai/openchart-engine";
@@ -714,8 +715,8 @@ var ZoomTransform = class _ZoomTransform {
714
715
  // src/graph/camera.ts
715
716
  var K_MIN = 0.05;
716
717
  var K_MAX = 15;
717
- var AUTO_MIN_MS = 300;
718
- var AUTO_MAX_MS = 1200;
718
+ var AUTO_MIN_MS = 400;
719
+ var AUTO_MAX_MS = 600;
719
720
  var AUTO_SCALE = 0.6;
720
721
  function transformToView(t, viewport) {
721
722
  const cx = (viewport.width / 2 - t.x) / t.k;
@@ -884,12 +885,16 @@ function driftFactor(t) {
884
885
  }
885
886
 
886
887
  // src/graph/canvas-renderer.ts
887
- var LABEL_FONT_MIN = 8;
888
+ var LABEL_FONT_MIN = 9;
888
889
  var LABEL_FONT_MAX = 12;
889
- var EDGE_ALPHA_DEFAULT = 0.35;
890
+ var EDGE_ALPHA_DEFAULT_LIGHT = 0.3;
891
+ var EDGE_ALPHA_DEFAULT_DARK = 0.25;
890
892
  var EDGE_ALPHA_CONNECTED = 1;
891
- var SEARCH_NON_MATCH_ALPHA = 0.15;
892
- var DEFAULT_DIM_OPACITY = 0.15;
893
+ var SEARCH_NON_MATCH_ALPHA = 0.25;
894
+ var DEFAULT_DIM_OPACITY = 0.3;
895
+ var LABEL_BUDGET_MIN = 12;
896
+ var LABEL_BUDGET_MAX = 80;
897
+ var LABEL_BUDGET_PER_ZOOM = 30;
893
898
  var CROSSFADE_MAX_EDGES = 2e4;
894
899
  function edgeTier(edge, focus) {
895
900
  if (!focus.hasActive) return "default";
@@ -900,14 +905,14 @@ function nodeTier(node, focus, exemptIds) {
900
905
  if (exemptIds?.has(node.id)) return "connected";
901
906
  return focus.connected.has(node.id) ? "connected" : "dimmed";
902
907
  }
903
- function edgeTierAlpha(tier, dimOpacity) {
908
+ function edgeTierAlpha(tier, dimOpacity, defaultAlpha) {
904
909
  switch (tier) {
905
910
  case "connected":
906
911
  return EDGE_ALPHA_CONNECTED;
907
912
  case "dimmed":
908
913
  return dimOpacity / 3;
909
914
  default:
910
- return EDGE_ALPHA_DEFAULT;
915
+ return defaultAlpha;
911
916
  }
912
917
  }
913
918
  function nodeTierAlpha(tier, dimOpacity) {
@@ -960,13 +965,16 @@ function deriveFocus(hoveredNodeId, selectedNodeIds, adjacencyMap) {
960
965
  }
961
966
  var GLOW_NODE_THRESHOLD = 2e3;
962
967
  var GLOW_RADIUS_MULTIPLIER = 1.3;
963
- var GLOW_ALPHA = 0.15;
968
+ var GLOW_ALPHA = 0.1;
964
969
  var CULL_MARGIN = 50;
965
970
  var TWO_PI = Math.PI * 2;
966
971
  var MIN_SCREEN_RADIUS = 2.5;
967
- function labelThreshold(zoom) {
968
- const t = Math.max(0, Math.min(1, (zoom - 0.2) / 1.8));
969
- return 1 - t;
972
+ function labelBudget(zoom) {
973
+ const raw = Math.round(zoom * LABEL_BUDGET_PER_ZOOM);
974
+ return Math.max(LABEL_BUDGET_MIN, Math.min(LABEL_BUDGET_MAX, raw));
975
+ }
976
+ function boxesOverlap(a, b) {
977
+ return a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
970
978
  }
971
979
  function visibleRect(canvasWidth, canvasHeight, transform, margin = CULL_MARGIN) {
972
980
  const { x, y, k } = transform;
@@ -1034,8 +1042,8 @@ var GraphCanvasRenderer = class {
1034
1042
  const visibleNodes = nodes.filter((n) => nodeInView(n, rect));
1035
1043
  const visibleEdges = edges.filter((e) => edgeInView(e, rect));
1036
1044
  const isDark = theme.isDark;
1045
+ const edgeAlphaDefault = isDark ? EDGE_ALPHA_DEFAULT_DARK : EDGE_ALPHA_DEFAULT_LIGHT;
1037
1046
  const showGlow = isDark && !isGesturing && visibleNodes.length < GLOW_NODE_THRESHOLD;
1038
- const threshold = labelThreshold(transform.k);
1039
1047
  const minRadius = MIN_SCREEN_RADIUS / transform.k;
1040
1048
  ctx.save();
1041
1049
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -1047,7 +1055,7 @@ var GraphCanvasRenderer = class {
1047
1055
  ctx.translate(transform.x, transform.y);
1048
1056
  ctx.scale(transform.k, transform.k);
1049
1057
  if (state.exiting && state.exiting.alpha > 0) {
1050
- this.drawGhosts(ctx, state.exiting, rect);
1058
+ this.drawGhosts(ctx, state.exiting, rect, edgeAlphaDefault);
1051
1059
  }
1052
1060
  if (crossfade && visibleEdges.length <= CROSSFADE_MAX_EDGES) {
1053
1061
  this.drawEdgesCrossfade(
@@ -1057,6 +1065,7 @@ var GraphCanvasRenderer = class {
1057
1065
  crossfade.next,
1058
1066
  crossfade.t,
1059
1067
  dimOpacity,
1068
+ edgeAlphaDefault,
1060
1069
  isGesturing ? null : searchMatches,
1061
1070
  hoveredEdgeId,
1062
1071
  entrance,
@@ -1068,6 +1077,7 @@ var GraphCanvasRenderer = class {
1068
1077
  visibleEdges,
1069
1078
  nextFocus,
1070
1079
  dimOpacity,
1080
+ edgeAlphaDefault,
1071
1081
  isGesturing ? null : searchMatches,
1072
1082
  hoveredEdgeId,
1073
1083
  entrance,
@@ -1095,7 +1105,6 @@ var GraphCanvasRenderer = class {
1095
1105
  this.drawLabels(
1096
1106
  ctx,
1097
1107
  visibleNodes,
1098
- threshold,
1099
1108
  hoveredNodeId,
1100
1109
  selectedNodeIds,
1101
1110
  searchMatches,
@@ -1132,7 +1141,7 @@ var GraphCanvasRenderer = class {
1132
1141
  // -------------------------------------------------------------------------
1133
1142
  // Batched edge drawing
1134
1143
  // -------------------------------------------------------------------------
1135
- drawEdgesBatched(ctx, edges, focus, dimOpacity, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
1144
+ drawEdgesBatched(ctx, edges, focus, dimOpacity, edgeAlphaDefault, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
1136
1145
  const buckets = {
1137
1146
  dimmed: [],
1138
1147
  default: [],
@@ -1151,14 +1160,14 @@ var GraphCanvasRenderer = class {
1151
1160
  this.drawEdgeGroupBatched(
1152
1161
  ctx,
1153
1162
  buckets.dimmed,
1154
- edgeTierAlpha("dimmed", dimOpacity) * ea,
1163
+ edgeTierAlpha("dimmed", dimOpacity, edgeAlphaDefault) * ea,
1155
1164
  searchMatches,
1156
1165
  enterAlphaFor
1157
1166
  );
1158
1167
  this.drawEdgeGroupBatched(
1159
1168
  ctx,
1160
1169
  buckets.default,
1161
- EDGE_ALPHA_DEFAULT * ea,
1170
+ edgeAlphaDefault * ea,
1162
1171
  searchMatches,
1163
1172
  enterAlphaFor
1164
1173
  );
@@ -1177,7 +1186,7 @@ var GraphCanvasRenderer = class {
1177
1186
  * batched at `lerp(edgeTierAlpha[prev], edgeTierAlpha[next], t)`. Preserves the
1178
1187
  * per-group style batching within each bucket.
1179
1188
  */
1180
- drawEdgesCrossfade(ctx, edges, prev, next, t, dimOpacity, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
1189
+ drawEdgesCrossfade(ctx, edges, prev, next, t, dimOpacity, edgeAlphaDefault, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
1181
1190
  const ea = entrance ? entrance.edgeAlpha : 1;
1182
1191
  const buckets = /* @__PURE__ */ new Map();
1183
1192
  let hoveredEdge = null;
@@ -1197,7 +1206,11 @@ var GraphCanvasRenderer = class {
1197
1206
  }
1198
1207
  const ordered = [...buckets.entries()].map(([key, bucket]) => {
1199
1208
  const [prevTier, nextTier] = key.split("|");
1200
- const alpha = lerp(edgeTierAlpha(prevTier, dimOpacity), edgeTierAlpha(nextTier, dimOpacity), t) * ea;
1209
+ const alpha = lerp(
1210
+ edgeTierAlpha(prevTier, dimOpacity, edgeAlphaDefault),
1211
+ edgeTierAlpha(nextTier, dimOpacity, edgeAlphaDefault),
1212
+ t
1213
+ ) * ea;
1201
1214
  return { alpha, bucket };
1202
1215
  }).sort((a, b) => a.alpha - b.alpha);
1203
1216
  for (const { alpha, bucket } of ordered) {
@@ -1431,34 +1444,71 @@ var GraphCanvasRenderer = class {
1431
1444
  // -------------------------------------------------------------------------
1432
1445
  // Labels (drawn individually, skipped during gestures)
1433
1446
  // -------------------------------------------------------------------------
1434
- drawLabels(ctx, nodes, threshold, hoveredNodeId, selectedNodeIds, searchMatches, zoom, theme, entrance, enterAlphaFor) {
1447
+ drawLabels(ctx, nodes, hoveredNodeId, selectedNodeIds, searchMatches, zoom, theme, entrance, enterAlphaFor) {
1435
1448
  const la = entrance ? entrance.labelAlpha : 1;
1436
1449
  const rawSize = 10 / zoom;
1437
1450
  const fontSize = Math.max(LABEL_FONT_MIN, Math.min(LABEL_FONT_MAX, rawSize));
1438
1451
  ctx.font = `${fontSize}px ${theme.fonts.family}`;
1439
1452
  ctx.textAlign = "center";
1440
1453
  ctx.textBaseline = "top";
1454
+ const haloColor = theme.colors.background !== "transparent" ? theme.colors.background : theme.isDark ? "rgba(0, 0, 0, 0.7)" : "rgba(255, 255, 255, 0.85)";
1455
+ const forced = [];
1456
+ const rest = [];
1441
1457
  for (const node of nodes) {
1442
1458
  if (!node.label) continue;
1443
- const isHovered = node.id === hoveredNodeId;
1444
- const isSelected = selectedNodeIds.has(node.id);
1445
- const forced = isHovered || isSelected;
1459
+ const isForced = node.id === hoveredNodeId || selectedNodeIds.has(node.id) || node.labelPriority === Infinity || (searchMatches?.has(node.id) ?? false);
1460
+ if (isForced) forced.push(node);
1461
+ else rest.push(node);
1462
+ }
1463
+ rest.sort((a, b) => b.labelPriority - a.labelPriority);
1464
+ const budget = labelBudget(zoom);
1465
+ const candidates = rest.slice(0, budget * 4);
1466
+ const placed = [];
1467
+ const pad = fontSize * 0.15;
1468
+ const lineHeight = fontSize * 1.2;
1469
+ const textWidth = (text) => ctx.measureText?.(text)?.width ?? text.length * fontSize * 0.55;
1470
+ const boxFor = (node) => {
1471
+ const w = textWidth(node.label);
1472
+ const y0 = node.y + node.radius + 3;
1473
+ return {
1474
+ x0: node.x - w / 2 - pad,
1475
+ x1: node.x + w / 2 + pad,
1476
+ y0: y0 - pad,
1477
+ y1: y0 + lineHeight + pad
1478
+ };
1479
+ };
1480
+ const drawOne = (node, isForced) => {
1446
1481
  const dimmed = searchMatches !== null && !searchMatches.has(node.id);
1447
- if (!forced && node.labelPriority < threshold) continue;
1448
1482
  ctx.globalAlpha = (dimmed ? SEARCH_NON_MATCH_ALPHA : 1) * la * enterAlphaFor(node.id);
1449
1483
  const labelY = node.y + node.radius + 3;
1450
- if (theme.colors.background !== "transparent") {
1451
- ctx.strokeStyle = theme.colors.background;
1452
- } else {
1453
- ctx.strokeStyle = theme.isDark ? "rgba(0, 0, 0, 0.7)" : "rgba(255, 255, 255, 0.85)";
1484
+ if (isForced) {
1485
+ ctx.strokeStyle = haloColor;
1486
+ ctx.lineWidth = 3;
1487
+ ctx.lineJoin = "round";
1488
+ ctx.miterLimit = 2;
1489
+ ctx.strokeText(node.label, node.x, labelY);
1454
1490
  }
1455
- ctx.lineWidth = 3;
1456
- ctx.lineJoin = "round";
1457
- ctx.miterLimit = 2;
1458
- ctx.strokeText(node.label, node.x, labelY);
1459
- ctx.fillStyle = theme.colors.text;
1491
+ ctx.fillStyle = isForced ? theme.colors.text : theme.colors.axis;
1460
1492
  ctx.fillText(node.label, node.x, labelY);
1493
+ };
1494
+ for (const node of forced) placed.push(boxFor(node));
1495
+ let drawn = 0;
1496
+ for (const node of candidates) {
1497
+ if (drawn >= budget) break;
1498
+ const box = boxFor(node);
1499
+ let collides = false;
1500
+ for (const other of placed) {
1501
+ if (boxesOverlap(box, other)) {
1502
+ collides = true;
1503
+ break;
1504
+ }
1505
+ }
1506
+ if (collides) continue;
1507
+ placed.push(box);
1508
+ drawOne(node, false);
1509
+ drawn++;
1461
1510
  }
1511
+ for (const node of forced) drawOne(node, true);
1462
1512
  ctx.globalAlpha = 1;
1463
1513
  }
1464
1514
  // -------------------------------------------------------------------------
@@ -1470,7 +1520,7 @@ var GraphCanvasRenderer = class {
1470
1520
  * they're on their way out. Batched by (stroke/style) for edges and (fill) for
1471
1521
  * nodes, culled to the visible rect.
1472
1522
  */
1473
- drawGhosts(ctx, exiting, rect) {
1523
+ drawGhosts(ctx, exiting, rect, edgeAlphaDefault) {
1474
1524
  const alpha = exiting.alpha;
1475
1525
  const edgeGroups = /* @__PURE__ */ new Map();
1476
1526
  for (const edge of exiting.edges) {
@@ -1486,7 +1536,7 @@ var GraphCanvasRenderer = class {
1486
1536
  ctx.setLineDash(dash);
1487
1537
  ctx.strokeStyle = sample.stroke;
1488
1538
  ctx.lineWidth = sample.strokeWidth;
1489
- ctx.globalAlpha = EDGE_ALPHA_DEFAULT * alpha;
1539
+ ctx.globalAlpha = edgeAlphaDefault * alpha;
1490
1540
  ctx.beginPath();
1491
1541
  for (const edge of group) {
1492
1542
  ctx.moveTo(edge.sourceX, edge.sourceY);
@@ -2929,9 +2979,11 @@ function createGraph(container, spec, options) {
2929
2979
  const resolvedTheme = compilation.theme;
2930
2980
  if (resolvedTheme) {
2931
2981
  const s = wrapper.style;
2932
- s.setProperty("--oc-bg", resolvedTheme.colors.background);
2982
+ s.setProperty("--oc-bg", resolvedSurface(resolvedTheme));
2933
2983
  s.setProperty("--oc-text", resolvedTheme.colors.text);
2934
- s.setProperty("--oc-text-secondary", resolvedTheme.colors.axis ?? resolvedTheme.colors.text);
2984
+ s.setProperty("--oc-text-secondary", resolvedTheme.colors.neutral.secondary);
2985
+ s.setProperty("--oc-text-muted", resolvedTheme.colors.axis);
2986
+ s.setProperty("--oc-border", resolvedTheme.colors.neutral.border);
2935
2987
  s.setProperty("--oc-font-family", resolvedTheme.fonts.family);
2936
2988
  s.fontFamily = resolvedTheme.fonts.family;
2937
2989
  }
@@ -4113,8 +4165,18 @@ function renderHeatmapCell(cell) {
4113
4165
  function renderCategoryCell(cell) {
4114
4166
  const td = document.createElement("td");
4115
4167
  td.className = "oc-table-category";
4116
- td.textContent = cell.formattedValue;
4117
- applyCellStyle(td, cell);
4168
+ const chip = document.createElement("span");
4169
+ chip.className = "oc-table-chip";
4170
+ if (cell.style.backgroundColor) chip.style.background = cell.style.backgroundColor;
4171
+ if (cell.style.color) chip.style.color = cell.style.color;
4172
+ if (cell.style.accent) {
4173
+ const dot = document.createElement("span");
4174
+ dot.className = "oc-table-chip-dot";
4175
+ dot.style.background = cell.style.accent;
4176
+ chip.appendChild(dot);
4177
+ }
4178
+ chip.appendChild(document.createTextNode(cell.formattedValue));
4179
+ td.appendChild(chip);
4118
4180
  return td;
4119
4181
  }
4120
4182
  function renderBarCell(cell) {
@@ -4178,7 +4240,7 @@ function renderSparklineCell(cell) {
4178
4240
  polyline.setAttribute("points", scaledPointsStr);
4179
4241
  polyline.setAttribute("fill", "none");
4180
4242
  polyline.setAttribute("stroke", sparklineData.color);
4181
- polyline.setAttribute("stroke-width", "1.5");
4243
+ polyline.setAttribute("stroke-width", "1.25");
4182
4244
  polyline.setAttribute("stroke-linejoin", "round");
4183
4245
  polyline.setAttribute("vector-effect", "non-scaling-stroke");
4184
4246
  svg.appendChild(polyline);
@@ -4272,6 +4334,27 @@ function renderSparklineCell(cell) {
4272
4334
  td.appendChild(wrapper);
4273
4335
  return td;
4274
4336
  }
4337
+ var DELTA_GLYPH = {
4338
+ up: "\u25B2",
4339
+ down: "\u25BC",
4340
+ flat: "\u2013"
4341
+ };
4342
+ function renderDeltaCell(cell) {
4343
+ const td = document.createElement("td");
4344
+ applyCellStyle(td, cell);
4345
+ const chip = document.createElement("span");
4346
+ chip.className = `oc-table-delta oc-table-delta--${cell.tone}`;
4347
+ const arrow = document.createElement("span");
4348
+ arrow.className = "oc-table-delta-arrow";
4349
+ arrow.setAttribute("aria-hidden", "true");
4350
+ arrow.textContent = DELTA_GLYPH[cell.direction];
4351
+ chip.appendChild(arrow);
4352
+ const value = document.createElement("span");
4353
+ value.textContent = cell.formattedValue;
4354
+ chip.appendChild(value);
4355
+ td.appendChild(chip);
4356
+ return td;
4357
+ }
4275
4358
  function renderImageCell(cell) {
4276
4359
  const td = document.createElement("td");
4277
4360
  applyCellStyle(td, cell);
@@ -4316,6 +4399,8 @@ function renderCell(cell) {
4316
4399
  return renderBarCell(cell);
4317
4400
  case "sparkline":
4318
4401
  return renderSparklineCell(cell);
4402
+ case "delta":
4403
+ return renderDeltaCell(cell);
4319
4404
  case "image":
4320
4405
  return renderImageCell(cell);
4321
4406
  case "flag":
@@ -4662,26 +4747,41 @@ function renderLabels(parent, nodes, measureText) {
4662
4747
  const text = createSVGElement2("text");
4663
4748
  setAttrs2(text, { x: label.x, y: label.y });
4664
4749
  applyTextStyle(text, label.style);
4750
+ text.setAttribute("class", "oc-sankey-label");
4751
+ text.setAttribute("data-node-id", node.nodeId);
4752
+ const fontSize = label.style.fontSize ?? 12;
4753
+ const fontWeight = label.style.fontWeight ?? 400;
4754
+ let lines = [label.text];
4665
4755
  if (label.maxWidth !== void 0 && label.maxWidth > 0) {
4666
- const fontSize = label.style.fontSize ?? 12;
4667
- const fontWeight = label.style.fontWeight ?? 400;
4668
- const lines = wrapText(label.text, fontSize, fontWeight, label.maxWidth, measureText);
4669
- if (lines.length > 1) {
4670
- const lineHeight = fontSize * (label.style.lineHeight ?? 1.3);
4671
- const totalHeight = (lines.length - 1) * lineHeight;
4672
- const startY = label.y - totalHeight / 2;
4673
- for (let i = 0; i < lines.length; i++) {
4674
- const tspan = createSVGElement2("tspan");
4675
- tspan.setAttribute("x", String(label.x));
4676
- tspan.setAttribute("y", String(startY + i * lineHeight));
4677
- tspan.textContent = lines[i];
4678
- text.appendChild(tspan);
4679
- }
4680
- } else {
4681
- text.textContent = label.text;
4756
+ lines = wrapText(label.text, fontSize, fontWeight, label.maxWidth, measureText);
4757
+ }
4758
+ if (lines.length > 1) {
4759
+ const lineHeight = fontSize * (label.style.lineHeight ?? 1.3);
4760
+ const totalHeight = (lines.length - 1) * lineHeight;
4761
+ const startY = label.y - totalHeight / 2;
4762
+ for (let i = 0; i < lines.length; i++) {
4763
+ const tspan = createSVGElement2("tspan");
4764
+ tspan.setAttribute("x", String(label.x));
4765
+ tspan.setAttribute("y", String(startY + i * lineHeight));
4766
+ tspan.textContent = lines[i];
4767
+ text.appendChild(tspan);
4682
4768
  }
4683
4769
  } else {
4684
- text.textContent = label.text;
4770
+ text.textContent = lines[0];
4771
+ }
4772
+ if (node.valueLabel) {
4773
+ if (text.childNodes.length === 0) {
4774
+ const nameSpan = createSVGElement2("tspan");
4775
+ nameSpan.textContent = text.textContent;
4776
+ text.textContent = "";
4777
+ text.appendChild(nameSpan);
4778
+ }
4779
+ const valueSpan = createSVGElement2("tspan");
4780
+ valueSpan.setAttribute("class", "oc-sankey-label-value");
4781
+ valueSpan.setAttribute("dx", "5");
4782
+ applyTextStyle(valueSpan, node.valueLabel.style);
4783
+ valueSpan.textContent = node.valueLabel.text;
4784
+ text.appendChild(valueSpan);
4685
4785
  }
4686
4786
  g.appendChild(text);
4687
4787
  }
@@ -4739,8 +4839,8 @@ function renderSankeySVG(layout, animation) {
4739
4839
 
4740
4840
  // src/sankey-mount.ts
4741
4841
  var HIGHLIGHT_OPACITY = 0.7;
4742
- var DIM_OPACITY = 0.15;
4743
- var NODE_DIM_OPACITY = 0.2;
4842
+ var DIM_OPACITY = 0.12;
4843
+ var NODE_DIM_OPACITY = 0.3;
4744
4844
  function createSankey(container, spec, options) {
4745
4845
  let currentSpec = spec;
4746
4846
  let currentLayout;
@@ -4802,7 +4902,7 @@ function createSankey(container, spec, options) {
4802
4902
  tooltipManager.show(content, x, y);
4803
4903
  }
4804
4904
  options?.onNodeHover?.(nodeData);
4805
- if (nodeId) highlightConnectedLinks(svg, nodeId, layout);
4905
+ if (nodeId) highlightPath(svg, { up: [nodeId], down: [nodeId] }, layout);
4806
4906
  };
4807
4907
  const handleMouseMove = (e) => {
4808
4908
  if (content && tooltipManager) {
@@ -4849,6 +4949,13 @@ function createSankey(container, spec, options) {
4849
4949
  tooltipManager.show(content, x, y);
4850
4950
  }
4851
4951
  options?.onLinkHover?.(linkData);
4952
+ if (sourceId && targetId) {
4953
+ highlightPath(
4954
+ svg,
4955
+ { up: [sourceId], down: [targetId], links: [`${sourceId}->${targetId}`] },
4956
+ layout
4957
+ );
4958
+ }
4852
4959
  };
4853
4960
  const handleMouseMove = (e) => {
4854
4961
  if (content && tooltipManager) {
@@ -4862,6 +4969,7 @@ function createSankey(container, spec, options) {
4862
4969
  const handleMouseLeave = () => {
4863
4970
  tooltipManager?.hide();
4864
4971
  options?.onLinkHover?.(null);
4972
+ resetLinkOpacity(svg, layout);
4865
4973
  };
4866
4974
  const handleClick = () => {
4867
4975
  options?.onLinkClick?.(linkData);
@@ -4888,27 +4996,53 @@ function createSankey(container, spec, options) {
4888
4996
  const link = layout.links.find((l) => l.sourceId === sourceId && l.targetId === targetId);
4889
4997
  return link?.data ?? {};
4890
4998
  }
4891
- function highlightConnectedLinks(svg, nodeId, _layout) {
4892
- const connectedNodeIds = /* @__PURE__ */ new Set([nodeId]);
4893
- const linkElements = svg.querySelectorAll(".oc-sankey-link");
4894
- for (const el of linkElements) {
4895
- const source = el.getAttribute("data-source");
4896
- const target = el.getAttribute("data-target");
4999
+ function tracePath(layout, seeds) {
5000
+ const out = /* @__PURE__ */ new Map();
5001
+ const inn = /* @__PURE__ */ new Map();
5002
+ for (const link of layout.links) {
5003
+ const o = out.get(link.sourceId);
5004
+ if (o) o.push(link);
5005
+ else out.set(link.sourceId, [link]);
5006
+ const i = inn.get(link.targetId);
5007
+ if (i) i.push(link);
5008
+ else inn.set(link.targetId, [link]);
5009
+ }
5010
+ const nodes = /* @__PURE__ */ new Set([...seeds.up, ...seeds.down]);
5011
+ const links = new Set(seeds.links ?? []);
5012
+ const walk = (adjacency, seedIds, nextId) => {
5013
+ const queue = [...seedIds];
5014
+ const seen = new Set(seedIds);
5015
+ while (queue.length > 0) {
5016
+ const id = queue.shift();
5017
+ for (const link of adjacency.get(id) ?? []) {
5018
+ links.add(`${link.sourceId}->${link.targetId}`);
5019
+ const next = nextId(link);
5020
+ nodes.add(next);
5021
+ if (!seen.has(next)) {
5022
+ seen.add(next);
5023
+ queue.push(next);
5024
+ }
5025
+ }
5026
+ }
5027
+ };
5028
+ walk(out, seeds.down, (l) => l.targetId);
5029
+ walk(inn, seeds.up, (l) => l.sourceId);
5030
+ return { nodes, links };
5031
+ }
5032
+ function highlightPath(svg, seeds, layout) {
5033
+ const { nodes: onNodes, links: onLinks } = tracePath(layout, seeds);
5034
+ for (const el of svg.querySelectorAll(".oc-sankey-link")) {
4897
5035
  const path = el.querySelector("path");
4898
5036
  if (!path) continue;
4899
- const isConnected = source === nodeId || target === nodeId;
4900
- path.setAttribute("fill-opacity", String(isConnected ? HIGHLIGHT_OPACITY : DIM_OPACITY));
4901
- if (isConnected) {
4902
- if (source) connectedNodeIds.add(source);
4903
- if (target) connectedNodeIds.add(target);
4904
- }
5037
+ const source = el.getAttribute("data-source");
5038
+ const target = el.getAttribute("data-target");
5039
+ const onPath = onLinks.has(`${source}->${target}`);
5040
+ path.setAttribute("fill-opacity", String(onPath ? HIGHLIGHT_OPACITY : DIM_OPACITY));
4905
5041
  }
4906
- const nodeElements = svg.querySelectorAll(".oc-sankey-node");
4907
- for (const el of nodeElements) {
5042
+ for (const el of svg.querySelectorAll(".oc-sankey-node, .oc-sankey-label")) {
4908
5043
  const nid = el.getAttribute("data-node-id");
4909
5044
  if (!nid) continue;
4910
- const isConnected = connectedNodeIds.has(nid);
4911
- el.style.opacity = isConnected ? "1" : String(NODE_DIM_OPACITY);
5045
+ el.style.opacity = onNodes.has(nid) ? "1" : String(NODE_DIM_OPACITY);
4912
5046
  }
4913
5047
  }
4914
5048
  function resetLinkOpacity(svg, layout) {
@@ -4921,8 +5055,7 @@ function createSankey(container, spec, options) {
4921
5055
  const link = layout.links.find((l) => l.sourceId === source && l.targetId === target);
4922
5056
  path.setAttribute("fill-opacity", String(link?.fillOpacity ?? 0.5));
4923
5057
  }
4924
- const nodeElements = svg.querySelectorAll(".oc-sankey-node");
4925
- for (const el of nodeElements) {
5058
+ for (const el of svg.querySelectorAll(".oc-sankey-node, .oc-sankey-label")) {
4926
5059
  el.style.opacity = "1";
4927
5060
  }
4928
5061
  }
@@ -5287,11 +5420,11 @@ function attachKeyboardNav(options) {
5287
5420
  }
5288
5421
 
5289
5422
  // src/table-mount.ts
5290
- import { getBreakpoint } from "@opendata-ai/openchart-core";
5423
+ import { BREAKPOINT_COMPACT_MAX } from "@opendata-ai/openchart-core";
5291
5424
  import { compileTable } from "@opendata-ai/openchart-engine";
5292
5425
 
5293
5426
  // src/table-renderer.ts
5294
- import { BRAND_FONT_SIZE as BRAND_FONT_SIZE3 } from "@opendata-ai/openchart-core";
5427
+ import { BRAND_FONT_SIZE as BRAND_FONT_SIZE3, cssTokenDefault } from "@opendata-ai/openchart-core";
5295
5428
  import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
5296
5429
  var BRAND_URL3 = "https://tryopendata.ai";
5297
5430
  function renderChromeBlock(layout, position) {
@@ -5333,6 +5466,7 @@ function renderChromeBlock(layout, position) {
5333
5466
  }
5334
5467
  function renderThead(columns, sort) {
5335
5468
  const thead = document.createElement("thead");
5469
+ thead.setAttribute("role", "rowgroup");
5336
5470
  const tr = document.createElement("tr");
5337
5471
  tr.setAttribute("role", "row");
5338
5472
  for (const col of columns) {
@@ -5364,6 +5498,7 @@ function renderThead(columns, sort) {
5364
5498
  }
5365
5499
  function renderTbody(rows, columns) {
5366
5500
  const tbody = document.createElement("tbody");
5501
+ tbody.setAttribute("role", "rowgroup");
5367
5502
  for (let r = 0; r < rows.length; r++) {
5368
5503
  const row = rows[r];
5369
5504
  const tr = document.createElement("tr");
@@ -5376,12 +5511,35 @@ function renderTbody(rows, columns) {
5376
5511
  const td = renderCell(cell);
5377
5512
  td.setAttribute("role", "gridcell");
5378
5513
  td.style.textAlign = columns[c].align;
5514
+ td.setAttribute("data-label", columns[c].label);
5515
+ td.setAttribute("data-priority", String(columns[c].priority));
5379
5516
  tr.appendChild(td);
5380
5517
  }
5381
5518
  tbody.appendChild(tr);
5382
5519
  }
5383
5520
  return tbody;
5384
5521
  }
5522
+ function renderTfoot(totalRow, columns) {
5523
+ const tfoot = document.createElement("tfoot");
5524
+ tfoot.className = "oc-table-total";
5525
+ tfoot.setAttribute("role", "rowgroup");
5526
+ const tr = document.createElement("tr");
5527
+ tr.setAttribute("role", "row");
5528
+ for (let c = 0; c < columns.length; c++) {
5529
+ const cell = totalRow.cells[c];
5530
+ const td = cell ? renderCell(cell) : document.createElement("td");
5531
+ td.setAttribute("role", "gridcell");
5532
+ td.style.textAlign = columns[c].align;
5533
+ td.setAttribute("data-label", columns[c].label);
5534
+ td.setAttribute("data-priority", String(columns[c].priority));
5535
+ if (c === 0 && !td.textContent) {
5536
+ td.textContent = totalRow.label;
5537
+ }
5538
+ tr.appendChild(td);
5539
+ }
5540
+ tfoot.appendChild(tr);
5541
+ return tfoot;
5542
+ }
5385
5543
  function renderSearchBar(layout) {
5386
5544
  if (!layout.search.enabled) return null;
5387
5545
  const div = document.createElement("div");
@@ -5439,12 +5597,23 @@ function renderTable(layout, container, opts) {
5439
5597
  const { theme, chrome } = layout;
5440
5598
  if (theme) {
5441
5599
  const s = wrapper.style;
5442
- s.setProperty("--oc-bg", theme.colors.background);
5600
+ const n = theme.colors.neutral;
5601
+ s.setProperty("--oc-bg", resolvedSurface(theme));
5443
5602
  s.setProperty("--oc-text", theme.colors.text);
5444
- s.setProperty("--oc-text-secondary", theme.colors.axis ?? theme.colors.text);
5445
- s.setProperty("--oc-text-muted", theme.colors.axis ?? theme.colors.text);
5603
+ s.setProperty("--oc-text-secondary", n.secondary);
5604
+ s.setProperty("--oc-text-muted", n[600]);
5605
+ s.setProperty("--oc-text-faint", n.faint);
5606
+ s.setProperty("--oc-gray-100", n[100]);
5607
+ s.setProperty("--oc-gray-200", n[200]);
5608
+ s.setProperty("--oc-gray-300", n[300]);
5609
+ s.setProperty("--oc-gray-400", n[400]);
5446
5610
  s.setProperty("--oc-gridline", theme.colors.gridline);
5447
- s.setProperty("--oc-border", theme.colors.gridline);
5611
+ const stripWs = (c) => c.replace(/\s+/g, "");
5612
+ const defaultGridline = cssTokenDefault("--oc-gridline", theme.isDark ? "dark" : "light");
5613
+ const gridlineOverridden = stripWs(theme.colors.gridline) !== stripWs(defaultGridline);
5614
+ s.setProperty("--oc-border", gridlineOverridden ? theme.colors.gridline : n.border);
5615
+ s.setProperty("--oc-positive", theme.colors.positive);
5616
+ s.setProperty("--oc-negative", theme.colors.negative);
5448
5617
  s.setProperty("--oc-font-family", theme.fonts.family);
5449
5618
  s.fontFamily = theme.fonts.family;
5450
5619
  }
@@ -5469,9 +5638,13 @@ function renderTable(layout, container, opts) {
5469
5638
  s.setProperty("--oc-footer-computed-color", chrome.footer.style.fill);
5470
5639
  }
5471
5640
  }
5472
- if (layout.compact) {
5641
+ wrapper.classList.add(`oc-table--${layout.density}`);
5642
+ if (layout.density === "condensed") {
5473
5643
  wrapper.classList.add("oc-table--compact");
5474
5644
  }
5645
+ if (layout.striped) {
5646
+ wrapper.classList.add("oc-table--striped");
5647
+ }
5475
5648
  const headerChrome = renderChromeBlock(layout, "header");
5476
5649
  if (headerChrome) {
5477
5650
  wrapper.appendChild(headerChrome);
@@ -5499,6 +5672,9 @@ function renderTable(layout, container, opts) {
5499
5672
  table.appendChild(caption);
5500
5673
  table.appendChild(renderThead(layout.columns, layout.sort));
5501
5674
  table.appendChild(renderTbody(layout.rows, layout.columns));
5675
+ if (layout.totalRow) {
5676
+ table.appendChild(renderTfoot(layout.totalRow, layout.columns));
5677
+ }
5502
5678
  scroll.appendChild(table);
5503
5679
  wrapper.appendChild(scroll);
5504
5680
  }
@@ -5551,6 +5727,8 @@ function renderTable(layout, container, opts) {
5551
5727
  }
5552
5728
 
5553
5729
  // src/table-mount.ts
5730
+ var MIN_READABLE_COLUMN_WIDTH = 96;
5731
+ var AUTO_CONDENSE_MAX_WIDTH = 560;
5554
5732
  function csvEscape(value) {
5555
5733
  if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
5556
5734
  return `"${value.replace(/"/g, '""')}"`;
@@ -5575,6 +5753,7 @@ function createTable(container, spec, options) {
5575
5753
  let cleanupAnimations = null;
5576
5754
  let isFirstRender = true;
5577
5755
  let destroyed = false;
5756
+ let sortTouched = false;
5578
5757
  const internalState = {
5579
5758
  sort: null,
5580
5759
  search: "",
@@ -5601,9 +5780,13 @@ function createTable(container, spec, options) {
5601
5780
  search: partial.search !== void 0 ? partial.search : current.search,
5602
5781
  page: partial.page !== void 0 ? partial.page : current.page
5603
5782
  };
5783
+ if (partial.sort !== void 0) sortTouched = true;
5604
5784
  options?.onStateChange?.(next);
5605
5785
  } else {
5606
- if (partial.sort !== void 0) internalState.sort = partial.sort;
5786
+ if (partial.sort !== void 0) {
5787
+ internalState.sort = partial.sort;
5788
+ sortTouched = true;
5789
+ }
5607
5790
  if (partial.search !== void 0) internalState.search = partial.search;
5608
5791
  if (partial.page !== void 0) internalState.page = partial.page;
5609
5792
  options?.onStateChange?.({ ...internalState });
@@ -5619,12 +5802,23 @@ function createTable(container, spec, options) {
5619
5802
  theme: options?.theme,
5620
5803
  darkMode,
5621
5804
  watermark: options?.watermark,
5622
- sort: state.sort ?? void 0,
5805
+ // undefined = "no opinion, apply the spec default"; null = the user
5806
+ // cycled sorting off and expects the original row order back. A
5807
+ // controlled caller's natural initial `sort: null` means "no opinion"
5808
+ // until the user has actually touched sorting.
5809
+ sort: resolveEffectiveSort(state),
5623
5810
  search: state.search || void 0,
5624
5811
  page: state.page
5625
5812
  };
5626
5813
  return compileTable(currentSpec, compileOpts);
5627
5814
  }
5815
+ function sameSort(a, b) {
5816
+ if (a === null || b === null) return a === b;
5817
+ return a.column === b.column && a.direction === b.direction;
5818
+ }
5819
+ function resolveEffectiveSort(state) {
5820
+ return sortTouched ? state.sort : state.sort ?? void 0;
5821
+ }
5628
5822
  function getContainerDimensions() {
5629
5823
  const rect = container.getBoundingClientRect();
5630
5824
  return {
@@ -5642,12 +5836,13 @@ function createTable(container, spec, options) {
5642
5836
  function applyBreakpointClass() {
5643
5837
  if (!wrapperElement) return;
5644
5838
  const { width } = getContainerDimensions();
5645
- const bp = getBreakpoint(width);
5646
- if (bp === "compact" || bp === "medium") {
5647
- wrapperElement.classList.add("oc-table--compact");
5648
- } else if (!currentLayout?.compact) {
5649
- wrapperElement.classList.remove("oc-table--compact");
5650
- }
5839
+ wrapperElement.classList.toggle("oc-table--cards", width < BREAKPOINT_COMPACT_MAX);
5840
+ if (currentSpec.density !== void 0 || currentSpec.compact === true) return;
5841
+ const columns = currentLayout?.columns.length ?? 0;
5842
+ const condensed = columns * MIN_READABLE_COLUMN_WIDTH > width || width < AUTO_CONDENSE_MAX_WIDTH;
5843
+ wrapperElement.classList.toggle("oc-table--condensed", condensed);
5844
+ wrapperElement.classList.toggle("oc-table--compact", condensed);
5845
+ wrapperElement.classList.toggle("oc-table--regular", !condensed);
5651
5846
  }
5652
5847
  function render() {
5653
5848
  if (destroyed) return;
@@ -5835,7 +6030,7 @@ function createTable(container, spec, options) {
5835
6030
  height: 600,
5836
6031
  theme: options?.theme,
5837
6032
  darkMode,
5838
- sort: state.sort ?? void 0,
6033
+ sort: resolveEffectiveSort(state),
5839
6034
  search: state.search || void 0
5840
6035
  // No page/pageSize: get all rows
5841
6036
  });
@@ -5849,6 +6044,9 @@ function createTable(container, spec, options) {
5849
6044
  }
5850
6045
  function setState(partial) {
5851
6046
  if (destroyed) return;
6047
+ if (partial.sort !== void 0 && !sameSort(partial.sort, getState().sort)) {
6048
+ sortTouched = true;
6049
+ }
5852
6050
  if (isControlled && options?.externalState) {
5853
6051
  if (partial.sort !== void 0) options.externalState.sort = partial.sort;
5854
6052
  if (partial.search !== void 0) options.externalState.search = partial.search;
@@ -5913,13 +6111,13 @@ function createTable(container, spec, options) {
5913
6111
  }
5914
6112
 
5915
6113
  // src/tilemap-mount.ts
5916
- import { BREAKPOINT_COMPACT_MAX, BREAKPOINT_MEDIUM_MAX } from "@opendata-ai/openchart-core";
6114
+ import { BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2, BREAKPOINT_MEDIUM_MAX } from "@opendata-ai/openchart-core";
5917
6115
  import { compileTileMap } from "@opendata-ai/openchart-engine";
5918
6116
  var TILEMAP_GRID_ROWS = 8;
5919
6117
  var TILEMAP_TILE_GAP = 5;
5920
6118
  var TILEMAP_CHROME_RESERVE = 180;
5921
6119
  function pickTargetTileSize(width) {
5922
- if (width < BREAKPOINT_COMPACT_MAX) return 34;
6120
+ if (width < BREAKPOINT_COMPACT_MAX2) return 34;
5923
6121
  return 40;
5924
6122
  }
5925
6123
  function createTileMap(container, spec, options) {
@@ -5976,6 +6174,7 @@ function createTileMap(container, spec, options) {
5976
6174
  for (const t of layout.tiles) {
5977
6175
  if (!tilesByStateCode.has(t.stateCode)) tilesByStateCode.set(t.stateCode, t);
5978
6176
  }
6177
+ const tilesGroup = svg.querySelector(".oc-tilemap-tiles");
5979
6178
  const tileElements = svg.querySelectorAll(".oc-tilemap-tile");
5980
6179
  for (const el of tileElements) {
5981
6180
  const stateCode = el.getAttribute("data-state");
@@ -5984,6 +6183,8 @@ function createTileMap(container, spec, options) {
5984
6183
  const tile = tilesByStateCode.get(stateCode);
5985
6184
  const handleMouseEnter = (e) => {
5986
6185
  const mouseEvent = e;
6186
+ tilesGroup?.classList.add("oc-hover-active");
6187
+ el.classList.add("oc-tilemap-tile--hover");
5987
6188
  if (content && tooltipManager && options?.tooltip !== false) {
5988
6189
  const svgRect = svg.getBoundingClientRect();
5989
6190
  const x = mouseEvent.clientX - svgRect.left;
@@ -6009,6 +6210,8 @@ function createTileMap(container, spec, options) {
6009
6210
  }
6010
6211
  };
6011
6212
  const handleMouseLeave = () => {
6213
+ tilesGroup?.classList.remove("oc-hover-active");
6214
+ el.classList.remove("oc-tilemap-tile--hover");
6012
6215
  tooltipManager?.hide();
6013
6216
  options?.onTileHover?.(null);
6014
6217
  };