@opendata-ai/openchart-vanilla 8.0.0-rc.13 → 8.0.0-rc.15

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.d.ts CHANGED
@@ -219,6 +219,10 @@ interface GraphInstance {
219
219
  getHighlight(): string[] | null;
220
220
  /** Headless snapshot of the legend (node categories + edge categories). */
221
221
  getLegend(): GraphLegendData;
222
+ /** Set the active category filter declaratively (replaces legend toggle state). */
223
+ setActiveCategories(values: string[]): void;
224
+ /** Current active category values (empty = all active, no filter). */
225
+ getActiveCategories(): string[];
222
226
  resize(): void;
223
227
  destroy(): void;
224
228
  }
package/dist/index.js CHANGED
@@ -570,6 +570,7 @@ var ZoomTransform = class _ZoomTransform {
570
570
  if (nodes.length === 0) {
571
571
  return { transform: _ZoomTransform.identity(), contentHeight: canvasH };
572
572
  }
573
+ const insetTop = Math.min(Math.max(0, opts?.insetTop ?? 0), canvasH * 0.4);
573
574
  let minX = Infinity;
574
575
  let minY = Infinity;
575
576
  let maxX = -Infinity;
@@ -585,8 +586,12 @@ var ZoomTransform = class _ZoomTransform {
585
586
  let graphH = maxY - minY;
586
587
  if (graphW === 0 && graphH === 0) {
587
588
  return {
588
- transform: new _ZoomTransform(canvasW / 2 - minX, canvasH / 2 - minY, 1),
589
- contentHeight: padding * 2
589
+ transform: new _ZoomTransform(
590
+ canvasW / 2 - minX,
591
+ insetTop + (canvasH - insetTop) / 2 - minY,
592
+ 1
593
+ ),
594
+ contentHeight: padding * 2 + insetTop
590
595
  };
591
596
  }
592
597
  if (opts?.spread !== false && nodes.length > 50) {
@@ -601,13 +606,13 @@ var ZoomTransform = class _ZoomTransform {
601
606
  maxY = cy2 + graphH / 2;
602
607
  }
603
608
  const availW = canvasW - padding * 2;
604
- const availH = canvasH - padding * 2;
609
+ const availH = canvasH - insetTop - padding * 2;
605
610
  const k = Math.min(1, availW / graphW, availH / graphH);
606
611
  const cx = (minX + maxX) / 2;
607
612
  const cy = (minY + maxY) / 2;
608
613
  const tx = canvasW / 2 - cx * k;
609
- const ty = canvasH / 2 - cy * k;
610
- const contentHeight = graphH * k + padding * 2;
614
+ const ty = insetTop + (canvasH - insetTop) / 2 - cy * k;
615
+ const contentHeight = graphH * k + padding * 2 + insetTop;
611
616
  return {
612
617
  transform: new _ZoomTransform(tx, ty, k),
613
618
  contentHeight
@@ -727,6 +732,62 @@ function nodeEnterProgress(globalT, index2, total, buckets = 8) {
727
732
  return Math.round(clamped * b) / b;
728
733
  }
729
734
  var ENTRANCE_STAGGER_MAX_NODES = 3e3;
735
+ var ENTRANCE_DRIFT_PX = 16;
736
+ function centroid(nodes) {
737
+ let cx = 0;
738
+ let cy = 0;
739
+ for (const n of nodes) {
740
+ cx += n.x;
741
+ cy += n.y;
742
+ }
743
+ cx /= nodes.length;
744
+ cy /= nodes.length;
745
+ return { cx, cy };
746
+ }
747
+ function entranceOrder(nodes) {
748
+ const rank = /* @__PURE__ */ new Map();
749
+ if (nodes.length === 0) return rank;
750
+ const { cx, cy } = centroid(nodes);
751
+ const sorted = [...nodes].sort((a2, b) => {
752
+ const da = (a2.x - cx) * (a2.x - cx) + (a2.y - cy) * (a2.y - cy);
753
+ const db = (b.x - cx) * (b.x - cx) + (b.y - cy) * (b.y - cy);
754
+ return da - db;
755
+ });
756
+ for (let i = 0; i < sorted.length; i++) rank.set(sorted[i].id, i);
757
+ return rank;
758
+ }
759
+ function entranceOffsets(nodes, dist = ENTRANCE_DRIFT_PX) {
760
+ const offsets = /* @__PURE__ */ new Map();
761
+ if (nodes.length === 0) return offsets;
762
+ const { cx, cy } = centroid(nodes);
763
+ for (const n of nodes) {
764
+ const dx = n.x - cx;
765
+ const dy = n.y - cy;
766
+ const len = Math.sqrt(dx * dx + dy * dy);
767
+ if (len < 1e-6) {
768
+ offsets.set(n.id, { x: 0, y: -dist });
769
+ } else {
770
+ offsets.set(n.id, { x: dx / len * dist, y: dy / len * dist });
771
+ }
772
+ }
773
+ return offsets;
774
+ }
775
+ function popScale(t) {
776
+ if (t <= 0) return 0;
777
+ if (t >= 1) return 1;
778
+ const c1 = 1.70158;
779
+ const c3 = c1 + 1;
780
+ const u = t - 1;
781
+ return 1 + c3 * u * u * u + c1 * u * u;
782
+ }
783
+ function popAlpha(t) {
784
+ const a2 = t / 0.6;
785
+ return a2 >= 1 ? 1 : a2 <= 0 ? 0 : a2;
786
+ }
787
+ function driftFactor(t) {
788
+ const u = 1 - (t <= 0 ? 0 : t >= 1 ? 1 : t);
789
+ return u * u;
790
+ }
730
791
 
731
792
  // src/graph/canvas-renderer.ts
732
793
  var LABEL_FONT_MIN = 8;
@@ -765,12 +826,24 @@ function nodeTierAlpha(tier, dimOpacity) {
765
826
  function lerp(a2, b, t) {
766
827
  return a2 + (b - a2) * t;
767
828
  }
829
+ var ZERO_SHIFT = { x: 0, y: 0 };
768
830
  function makeEntranceReveal(entrance, total) {
769
831
  const g = entrance.t;
770
- const ramp = (nodeT) => 0.6 + 0.4 * nodeT;
832
+ const { stagger, order, offsets } = entrance;
833
+ const rankOf = (node) => order?.get(node.id) ?? node.index;
834
+ const nodeT = (node) => stagger ? nodeEnterProgress(g, rankOf(node), total) : g;
771
835
  const edgeAlpha = Math.max(0, (g - 0.3) / 0.7);
836
+ const globalRamp = 0.6 + 0.4 * g;
772
837
  return {
773
- nodeRamp: (node) => ramp(entrance.stagger ? nodeEnterProgress(g, node.index, total) : g),
838
+ nodeAlpha: (node) => stagger ? popAlpha(nodeT(node)) : globalRamp,
839
+ nodeScale: (node) => stagger ? popScale(nodeT(node)) : globalRamp,
840
+ shift: (id) => {
841
+ if (!stagger || !offsets || !order) return ZERO_SHIFT;
842
+ const off = offsets.get(id);
843
+ if (!off) return ZERO_SHIFT;
844
+ const f = driftFactor(nodeEnterProgress(g, order.get(id) ?? 0, total));
845
+ return f > 0 ? { x: off.x * f, y: off.y * f } : ZERO_SHIFT;
846
+ },
774
847
  edgeAlpha,
775
848
  labelAlpha: g
776
849
  };
@@ -1131,8 +1204,10 @@ var GraphCanvasRenderer = class {
1131
1204
  return nodeTierAlpha(nodeTier(node, nextFocus), dimOpacity);
1132
1205
  };
1133
1206
  const searchAlpha = (node) => searchMatches !== null && !searchMatches.has(node.id) ? SEARCH_NON_MATCH_ALPHA : 1;
1134
- const entranceRamp = (node) => entrance ? entrance.nodeRamp(node) : 1;
1135
- const effectiveAlpha = (node) => focusAlpha(node) * searchAlpha(node) * entranceRamp(node) * enterAlphaFor(node.id);
1207
+ const entranceAlpha = (node) => entrance ? entrance.nodeAlpha(node) : 1;
1208
+ const entranceScale = (node) => entrance ? entrance.nodeScale(node) : 1;
1209
+ const entranceShift = (node) => entrance ? entrance.shift(node.id) : ZERO_SHIFT;
1210
+ const effectiveAlpha = (node) => focusAlpha(node) * searchAlpha(node) * entranceAlpha(node) * enterAlphaFor(node.id);
1136
1211
  const bulkNodes = [];
1137
1212
  const specialNodes = [];
1138
1213
  for (const node of nodes) {
@@ -1142,8 +1217,8 @@ var GraphCanvasRenderer = class {
1142
1217
  bulkNodes.push(node);
1143
1218
  }
1144
1219
  }
1145
- const r = (node) => Math.max(node.radius, minRadius) * entranceRamp(node);
1146
- if (showGlow) {
1220
+ const r = (node) => Math.max(node.radius, minRadius) * entranceScale(node);
1221
+ if (showGlow && !entrance) {
1147
1222
  this.drawGlowBatched(ctx, bulkNodes, searchMatches, minRadius);
1148
1223
  }
1149
1224
  const fillGroups = /* @__PURE__ */ new Map();
@@ -1163,8 +1238,10 @@ var GraphCanvasRenderer = class {
1163
1238
  ctx.beginPath();
1164
1239
  for (const node of group) {
1165
1240
  const nr = r(node);
1166
- ctx.moveTo(node.x + nr, node.y);
1167
- ctx.arc(node.x, node.y, nr, 0, TWO_PI);
1241
+ if (nr <= 0) continue;
1242
+ const s = entranceShift(node);
1243
+ ctx.moveTo(node.x + s.x + nr, node.y + s.y);
1244
+ ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
1168
1245
  }
1169
1246
  ctx.fill();
1170
1247
  }
@@ -1186,8 +1263,10 @@ var GraphCanvasRenderer = class {
1186
1263
  ctx.beginPath();
1187
1264
  for (const node of group) {
1188
1265
  const nr = r(node);
1189
- ctx.moveTo(node.x + nr, node.y);
1190
- ctx.arc(node.x, node.y, nr, 0, TWO_PI);
1266
+ if (nr <= 0) continue;
1267
+ const s = entranceShift(node);
1268
+ ctx.moveTo(node.x + s.x + nr, node.y + s.y);
1269
+ ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
1191
1270
  }
1192
1271
  ctx.stroke();
1193
1272
  }
@@ -1199,17 +1278,20 @@ var GraphCanvasRenderer = class {
1199
1278
  const hoverScale = hoverRadiusScale?.get(node.id) ?? (isHovered ? 1.15 : 1);
1200
1279
  const radius = baseRadius * hoverScale;
1201
1280
  const brightened = isHovered && hoverScale >= 1.075;
1281
+ const s = entranceShift(node);
1282
+ const nx = node.x + s.x;
1283
+ const ny = node.y + s.y;
1202
1284
  ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
1203
1285
  if (showGlow && !dimmed) {
1204
1286
  ctx.beginPath();
1205
- ctx.arc(node.x, node.y, radius * GLOW_RADIUS_MULTIPLIER, 0, TWO_PI);
1287
+ ctx.arc(nx, ny, radius * GLOW_RADIUS_MULTIPLIER, 0, TWO_PI);
1206
1288
  ctx.fillStyle = node.fill;
1207
1289
  ctx.globalAlpha = GLOW_ALPHA;
1208
1290
  ctx.fill();
1209
1291
  ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
1210
1292
  }
1211
1293
  ctx.beginPath();
1212
- ctx.arc(node.x, node.y, radius, 0, TWO_PI);
1294
+ ctx.arc(nx, ny, radius, 0, TWO_PI);
1213
1295
  ctx.fillStyle = brightened ? brighten(node.fill) : node.fill;
1214
1296
  ctx.fill();
1215
1297
  ctx.strokeStyle = node.stroke;
@@ -1217,7 +1299,7 @@ var GraphCanvasRenderer = class {
1217
1299
  ctx.stroke();
1218
1300
  if (isSelected) {
1219
1301
  ctx.beginPath();
1220
- ctx.arc(node.x, node.y, radius + 3, 0, TWO_PI);
1302
+ ctx.arc(nx, ny, radius + 3, 0, TWO_PI);
1221
1303
  ctx.strokeStyle = theme.colors.categorical[0] ?? "#3b82f6";
1222
1304
  ctx.lineWidth = 2;
1223
1305
  ctx.stroke();
@@ -3531,10 +3613,21 @@ function diffGraphUpdate(prevNodes, prevEdges, next, prevConfig, seed) {
3531
3613
  const exitingNodes = prevNodes.filter((n) => !nextIds.has(n.id));
3532
3614
  const nextEdgeKeys = new Set(next.edges.map((e) => `${e.source} ${e.target}`));
3533
3615
  const exitingEdges = prevEdges.filter((e) => !nextEdgeKeys.has(`${e.source} ${e.target}`));
3534
- const prevEdgeKeys = new Set(prevEdges.map((e) => `${e.source} ${e.target}`));
3616
+ const prevEdgeCounts = /* @__PURE__ */ new Map();
3617
+ for (const e of prevEdges) {
3618
+ const k = `${e.source} ${e.target}`;
3619
+ prevEdgeCounts.set(k, (prevEdgeCounts.get(k) ?? 0) + 1);
3620
+ }
3535
3621
  let enteringEdgeCount = 0;
3622
+ const remainingPrev = new Map(prevEdgeCounts);
3536
3623
  for (const e of next.edges) {
3537
- if (!prevEdgeKeys.has(`${e.source} ${e.target}`)) enteringEdgeCount++;
3624
+ const k = `${e.source} ${e.target}`;
3625
+ const c2 = remainingPrev.get(k);
3626
+ if (c2 && c2 > 0) {
3627
+ remainingPrev.set(k, c2 - 1);
3628
+ } else {
3629
+ enteringEdgeCount++;
3630
+ }
3538
3631
  }
3539
3632
  const sameNodes = prevIds.size === nextIds.size && enteringIds.length === 0;
3540
3633
  const sameEdges = edgeSetsEqual(prevEdges, next.edges);
@@ -3668,6 +3761,8 @@ function createGraph(container, spec, options) {
3668
3761
  let entranceProgress = 1;
3669
3762
  let entranceActive = false;
3670
3763
  let entranceStagger = false;
3764
+ let entranceOrderMap = null;
3765
+ let entranceOffsetMap = null;
3671
3766
  let entranceFitInFlight = false;
3672
3767
  let entranceReveal = null;
3673
3768
  let suppressEntranceOnce = options?.suppressEntrance ?? false;
@@ -3807,6 +3902,7 @@ function createGraph(container, spec, options) {
3807
3902
  wrapper.appendChild(legendEl);
3808
3903
  }
3809
3904
  container.appendChild(wrapper);
3905
+ syncChromeInset();
3810
3906
  const canvasHeight = Math.max(height, 200);
3811
3907
  renderer = new GraphCanvasRenderer(canvas);
3812
3908
  renderer.resize(width, canvasHeight);
@@ -3856,6 +3952,16 @@ function createGraph(container, spec, options) {
3856
3952
  }
3857
3953
  function syncLegendActiveState() {
3858
3954
  if (legendController) legendController.update(legendViewData());
3955
+ syncChromeInset();
3956
+ }
3957
+ function syncChromeInset() {
3958
+ if (!chromeEl) return;
3959
+ const legendW = legendEl?.offsetWidth ?? 0;
3960
+ chromeEl.style.right = legendW > 0 ? `${legendW + 24}px` : "";
3961
+ }
3962
+ function chromeInsetTop() {
3963
+ if (!chromeEl || chromeEl.style.display === "none") return 0;
3964
+ return chromeEl.offsetHeight;
3859
3965
  }
3860
3966
  function springyDragEnabled() {
3861
3967
  return compilation.interaction.springyDrag && compilation.nodes.length <= SPRINGY_DRAG_MAX_NODES;
@@ -3943,7 +4049,8 @@ function createGraph(container, spec, options) {
3943
4049
  const { width: cw, height: ch } = getCanvasDimensions();
3944
4050
  const warmed = (compilation.simulationConfig.warmupTicks ?? 0) > 0;
3945
4051
  const { transform } = ZoomTransform.fitBounds(positionedNodes, cw, ch, void 0, {
3946
- spread: !warmed
4052
+ spread: !warmed,
4053
+ insetTop: chromeInsetTop()
3947
4054
  });
3948
4055
  return transform;
3949
4056
  }
@@ -3961,12 +4068,19 @@ function createGraph(container, spec, options) {
3961
4068
  return;
3962
4069
  }
3963
4070
  const { width: cw, height: ch } = getCanvasDimensions();
3964
- const pulledBack = fit.zoomAt(fit.k * 0.92, cw / 2, ch / 2);
4071
+ const pulledBack = fit.zoomAt(fit.k * 0.85, cw / 2, ch / 2);
3965
4072
  interactionManager.setTransform(pulledBack);
3966
4073
  cameraChangePending = true;
3967
4074
  entranceActive = true;
3968
4075
  entranceProgress = 0;
3969
4076
  entranceStagger = enter.stagger && positionedNodes.length <= ENTRANCE_STAGGER_MAX_NODES;
4077
+ if (entranceStagger) {
4078
+ entranceOrderMap = entranceOrder(positionedNodes);
4079
+ entranceOffsetMap = entranceOffsets(positionedNodes);
4080
+ } else {
4081
+ entranceOrderMap = null;
4082
+ entranceOffsetMap = null;
4083
+ }
3970
4084
  if (enter.cameraFit) {
3971
4085
  entranceFitInFlight = true;
3972
4086
  flyCamera(fit, { duration: enter.duration + 100 }, () => {
@@ -4182,7 +4296,12 @@ function createGraph(container, spec, options) {
4182
4296
  focus: focus ?? { t: 1, prev: settledNext, next: settledNext },
4183
4297
  hoverRadiusScale,
4184
4298
  dimOpacity: highlightDimOpacity ?? compilation.interaction.dimOpacity,
4185
- entrance: entranceActive && entranceProgress < 1 ? { t: entranceProgress, stagger: entranceStagger } : void 0,
4299
+ entrance: entranceActive && entranceProgress < 1 ? {
4300
+ t: entranceProgress,
4301
+ stagger: entranceStagger,
4302
+ order: entranceOrderMap ?? void 0,
4303
+ offsets: entranceOffsetMap ?? void 0
4304
+ } : void 0,
4186
4305
  enterAlpha: enterAlphaMap ?? void 0,
4187
4306
  exiting: exitingGhosts ?? void 0
4188
4307
  };
@@ -4421,7 +4540,10 @@ function createGraph(container, spec, options) {
4421
4540
  positionedNodes,
4422
4541
  cw,
4423
4542
  ch,
4424
- opts?.padding
4543
+ opts?.padding,
4544
+ {
4545
+ insetTop: chromeInsetTop()
4546
+ }
4425
4547
  );
4426
4548
  flyCamera(fitTransform, opts);
4427
4549
  }
@@ -4501,6 +4623,18 @@ function createGraph(container, spec, options) {
4501
4623
  }))
4502
4624
  };
4503
4625
  }
4626
+ function setActiveCategories(values) {
4627
+ if (destroyed) return;
4628
+ activeCategories = new Set(values);
4629
+ highlightSet = categoryHighlightSet();
4630
+ highlightDimOpacity = null;
4631
+ syncLegendActiveState();
4632
+ refreshFocus();
4633
+ emitHighlightChange();
4634
+ }
4635
+ function getActiveCategories() {
4636
+ return [...activeCategories];
4637
+ }
4504
4638
  function toggleLegendCategory(value) {
4505
4639
  if (activeCategories.has(value)) activeCategories.delete(value);
4506
4640
  else activeCategories.add(value);
@@ -4516,6 +4650,7 @@ function createGraph(container, spec, options) {
4516
4650
  const { width, height } = getContainerDimensions();
4517
4651
  const canvasHeight = Math.max(height, 200);
4518
4652
  renderer.resize(width, canvasHeight);
4653
+ syncChromeInset();
4519
4654
  if (entranceFitInFlight && interactionManager) {
4520
4655
  cancelFlight();
4521
4656
  entranceFitInFlight = false;
@@ -4787,6 +4922,9 @@ function createGraph(container, spec, options) {
4787
4922
  },
4788
4923
  getHighlight: () => null,
4789
4924
  getLegend: () => ({ field: null, nodes: [], edges: [] }),
4925
+ setActiveCategories() {
4926
+ },
4927
+ getActiveCategories: () => [],
4790
4928
  resize() {
4791
4929
  },
4792
4930
  destroy() {
@@ -4815,6 +4953,8 @@ function createGraph(container, spec, options) {
4815
4953
  clearHighlight,
4816
4954
  getHighlight,
4817
4955
  getLegend,
4956
+ setActiveCategories,
4957
+ getActiveCategories,
4818
4958
  resize: doResize,
4819
4959
  destroy
4820
4960
  };