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

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;
@@ -601,13 +602,13 @@ var ZoomTransform = class _ZoomTransform {
601
602
  maxY = cy2 + graphH / 2;
602
603
  }
603
604
  const availW = canvasW - padding * 2;
604
- const availH = canvasH - padding * 2;
605
+ const availH = canvasH - insetTop - padding * 2;
605
606
  const k = Math.min(1, availW / graphW, availH / graphH);
606
607
  const cx = (minX + maxX) / 2;
607
608
  const cy = (minY + maxY) / 2;
608
609
  const tx = canvasW / 2 - cx * k;
609
- const ty = canvasH / 2 - cy * k;
610
- const contentHeight = graphH * k + padding * 2;
610
+ const ty = insetTop + (canvasH - insetTop) / 2 - cy * k;
611
+ const contentHeight = graphH * k + padding * 2 + insetTop;
611
612
  return {
612
613
  transform: new _ZoomTransform(tx, ty, k),
613
614
  contentHeight
@@ -727,6 +728,65 @@ function nodeEnterProgress(globalT, index2, total, buckets = 8) {
727
728
  return Math.round(clamped * b) / b;
728
729
  }
729
730
  var ENTRANCE_STAGGER_MAX_NODES = 3e3;
731
+ var ENTRANCE_DRIFT_PX = 16;
732
+ function entranceOrder(nodes) {
733
+ const rank = /* @__PURE__ */ new Map();
734
+ if (nodes.length === 0) return rank;
735
+ let cx = 0;
736
+ let cy = 0;
737
+ for (const n of nodes) {
738
+ cx += n.x;
739
+ cy += n.y;
740
+ }
741
+ cx /= nodes.length;
742
+ cy /= nodes.length;
743
+ const sorted = [...nodes].sort((a2, b) => {
744
+ const da = (a2.x - cx) * (a2.x - cx) + (a2.y - cy) * (a2.y - cy);
745
+ const db = (b.x - cx) * (b.x - cx) + (b.y - cy) * (b.y - cy);
746
+ return da - db;
747
+ });
748
+ for (let i = 0; i < sorted.length; i++) rank.set(sorted[i].id, i);
749
+ return rank;
750
+ }
751
+ function entranceOffsets(nodes, dist = ENTRANCE_DRIFT_PX) {
752
+ const offsets = /* @__PURE__ */ new Map();
753
+ if (nodes.length === 0) return offsets;
754
+ let cx = 0;
755
+ let cy = 0;
756
+ for (const n of nodes) {
757
+ cx += n.x;
758
+ cy += n.y;
759
+ }
760
+ cx /= nodes.length;
761
+ cy /= nodes.length;
762
+ for (const n of nodes) {
763
+ const dx = n.x - cx;
764
+ const dy = n.y - cy;
765
+ const len = Math.sqrt(dx * dx + dy * dy);
766
+ if (len < 1e-6) {
767
+ offsets.set(n.id, { x: 0, y: -dist });
768
+ } else {
769
+ offsets.set(n.id, { x: dx / len * dist, y: dy / len * dist });
770
+ }
771
+ }
772
+ return offsets;
773
+ }
774
+ function popScale(t) {
775
+ if (t <= 0) return 0;
776
+ if (t >= 1) return 1;
777
+ const c1 = 1.70158;
778
+ const c3 = c1 + 1;
779
+ const u = t - 1;
780
+ return 1 + c3 * u * u * u + c1 * u * u;
781
+ }
782
+ function popAlpha(t) {
783
+ const a2 = t / 0.6;
784
+ return a2 >= 1 ? 1 : a2 <= 0 ? 0 : a2;
785
+ }
786
+ function driftFactor(t) {
787
+ const u = 1 - (t <= 0 ? 0 : t >= 1 ? 1 : t);
788
+ return u * u;
789
+ }
730
790
 
731
791
  // src/graph/canvas-renderer.ts
732
792
  var LABEL_FONT_MIN = 8;
@@ -765,12 +825,24 @@ function nodeTierAlpha(tier, dimOpacity) {
765
825
  function lerp(a2, b, t) {
766
826
  return a2 + (b - a2) * t;
767
827
  }
828
+ var ZERO_SHIFT = { x: 0, y: 0 };
768
829
  function makeEntranceReveal(entrance, total) {
769
830
  const g = entrance.t;
770
- const ramp = (nodeT) => 0.6 + 0.4 * nodeT;
831
+ const { stagger, order, offsets } = entrance;
832
+ const rankOf = (node) => order?.get(node.id) ?? node.index;
833
+ const nodeT = (node) => stagger ? nodeEnterProgress(g, rankOf(node), total) : g;
771
834
  const edgeAlpha = Math.max(0, (g - 0.3) / 0.7);
835
+ const globalRamp = 0.6 + 0.4 * g;
772
836
  return {
773
- nodeRamp: (node) => ramp(entrance.stagger ? nodeEnterProgress(g, node.index, total) : g),
837
+ nodeAlpha: (node) => stagger ? popAlpha(nodeT(node)) : globalRamp,
838
+ nodeScale: (node) => stagger ? popScale(nodeT(node)) : globalRamp,
839
+ shift: (id) => {
840
+ if (!stagger || !offsets || !order) return ZERO_SHIFT;
841
+ const off = offsets.get(id);
842
+ if (!off) return ZERO_SHIFT;
843
+ const f = driftFactor(nodeEnterProgress(g, order.get(id) ?? 0, total));
844
+ return f > 0 ? { x: off.x * f, y: off.y * f } : ZERO_SHIFT;
845
+ },
774
846
  edgeAlpha,
775
847
  labelAlpha: g
776
848
  };
@@ -1131,8 +1203,10 @@ var GraphCanvasRenderer = class {
1131
1203
  return nodeTierAlpha(nodeTier(node, nextFocus), dimOpacity);
1132
1204
  };
1133
1205
  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);
1206
+ const entranceAlpha = (node) => entrance ? entrance.nodeAlpha(node) : 1;
1207
+ const entranceScale = (node) => entrance ? entrance.nodeScale(node) : 1;
1208
+ const entranceShift = (node) => entrance ? entrance.shift(node.id) : ZERO_SHIFT;
1209
+ const effectiveAlpha = (node) => focusAlpha(node) * searchAlpha(node) * entranceAlpha(node) * enterAlphaFor(node.id);
1136
1210
  const bulkNodes = [];
1137
1211
  const specialNodes = [];
1138
1212
  for (const node of nodes) {
@@ -1142,8 +1216,8 @@ var GraphCanvasRenderer = class {
1142
1216
  bulkNodes.push(node);
1143
1217
  }
1144
1218
  }
1145
- const r = (node) => Math.max(node.radius, minRadius) * entranceRamp(node);
1146
- if (showGlow) {
1219
+ const r = (node) => Math.max(node.radius, minRadius) * entranceScale(node);
1220
+ if (showGlow && !entrance) {
1147
1221
  this.drawGlowBatched(ctx, bulkNodes, searchMatches, minRadius);
1148
1222
  }
1149
1223
  const fillGroups = /* @__PURE__ */ new Map();
@@ -1163,8 +1237,10 @@ var GraphCanvasRenderer = class {
1163
1237
  ctx.beginPath();
1164
1238
  for (const node of group) {
1165
1239
  const nr = r(node);
1166
- ctx.moveTo(node.x + nr, node.y);
1167
- ctx.arc(node.x, node.y, nr, 0, TWO_PI);
1240
+ if (nr <= 0) continue;
1241
+ const s = entranceShift(node);
1242
+ ctx.moveTo(node.x + s.x + nr, node.y + s.y);
1243
+ ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
1168
1244
  }
1169
1245
  ctx.fill();
1170
1246
  }
@@ -1186,8 +1262,10 @@ var GraphCanvasRenderer = class {
1186
1262
  ctx.beginPath();
1187
1263
  for (const node of group) {
1188
1264
  const nr = r(node);
1189
- ctx.moveTo(node.x + nr, node.y);
1190
- ctx.arc(node.x, node.y, nr, 0, TWO_PI);
1265
+ if (nr <= 0) continue;
1266
+ const s = entranceShift(node);
1267
+ ctx.moveTo(node.x + s.x + nr, node.y + s.y);
1268
+ ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
1191
1269
  }
1192
1270
  ctx.stroke();
1193
1271
  }
@@ -3668,6 +3746,8 @@ function createGraph(container, spec, options) {
3668
3746
  let entranceProgress = 1;
3669
3747
  let entranceActive = false;
3670
3748
  let entranceStagger = false;
3749
+ let entranceOrderMap = null;
3750
+ let entranceOffsetMap = null;
3671
3751
  let entranceFitInFlight = false;
3672
3752
  let entranceReveal = null;
3673
3753
  let suppressEntranceOnce = options?.suppressEntrance ?? false;
@@ -3807,6 +3887,7 @@ function createGraph(container, spec, options) {
3807
3887
  wrapper.appendChild(legendEl);
3808
3888
  }
3809
3889
  container.appendChild(wrapper);
3890
+ syncChromeInset();
3810
3891
  const canvasHeight = Math.max(height, 200);
3811
3892
  renderer = new GraphCanvasRenderer(canvas);
3812
3893
  renderer.resize(width, canvasHeight);
@@ -3856,6 +3937,16 @@ function createGraph(container, spec, options) {
3856
3937
  }
3857
3938
  function syncLegendActiveState() {
3858
3939
  if (legendController) legendController.update(legendViewData());
3940
+ syncChromeInset();
3941
+ }
3942
+ function syncChromeInset() {
3943
+ if (!chromeEl) return;
3944
+ const legendW = legendEl?.offsetWidth ?? 0;
3945
+ chromeEl.style.right = legendW > 0 ? `${legendW + 24}px` : "";
3946
+ }
3947
+ function chromeInsetTop() {
3948
+ if (!chromeEl || chromeEl.style.display === "none") return 0;
3949
+ return chromeEl.offsetHeight;
3859
3950
  }
3860
3951
  function springyDragEnabled() {
3861
3952
  return compilation.interaction.springyDrag && compilation.nodes.length <= SPRINGY_DRAG_MAX_NODES;
@@ -3943,7 +4034,8 @@ function createGraph(container, spec, options) {
3943
4034
  const { width: cw, height: ch } = getCanvasDimensions();
3944
4035
  const warmed = (compilation.simulationConfig.warmupTicks ?? 0) > 0;
3945
4036
  const { transform } = ZoomTransform.fitBounds(positionedNodes, cw, ch, void 0, {
3946
- spread: !warmed
4037
+ spread: !warmed,
4038
+ insetTop: chromeInsetTop()
3947
4039
  });
3948
4040
  return transform;
3949
4041
  }
@@ -3961,12 +4053,19 @@ function createGraph(container, spec, options) {
3961
4053
  return;
3962
4054
  }
3963
4055
  const { width: cw, height: ch } = getCanvasDimensions();
3964
- const pulledBack = fit.zoomAt(fit.k * 0.92, cw / 2, ch / 2);
4056
+ const pulledBack = fit.zoomAt(fit.k * 0.85, cw / 2, ch / 2);
3965
4057
  interactionManager.setTransform(pulledBack);
3966
4058
  cameraChangePending = true;
3967
4059
  entranceActive = true;
3968
4060
  entranceProgress = 0;
3969
4061
  entranceStagger = enter.stagger && positionedNodes.length <= ENTRANCE_STAGGER_MAX_NODES;
4062
+ if (entranceStagger) {
4063
+ entranceOrderMap = entranceOrder(positionedNodes);
4064
+ entranceOffsetMap = entranceOffsets(positionedNodes);
4065
+ } else {
4066
+ entranceOrderMap = null;
4067
+ entranceOffsetMap = null;
4068
+ }
3970
4069
  if (enter.cameraFit) {
3971
4070
  entranceFitInFlight = true;
3972
4071
  flyCamera(fit, { duration: enter.duration + 100 }, () => {
@@ -4182,7 +4281,12 @@ function createGraph(container, spec, options) {
4182
4281
  focus: focus ?? { t: 1, prev: settledNext, next: settledNext },
4183
4282
  hoverRadiusScale,
4184
4283
  dimOpacity: highlightDimOpacity ?? compilation.interaction.dimOpacity,
4185
- entrance: entranceActive && entranceProgress < 1 ? { t: entranceProgress, stagger: entranceStagger } : void 0,
4284
+ entrance: entranceActive && entranceProgress < 1 ? {
4285
+ t: entranceProgress,
4286
+ stagger: entranceStagger,
4287
+ order: entranceOrderMap ?? void 0,
4288
+ offsets: entranceOffsetMap ?? void 0
4289
+ } : void 0,
4186
4290
  enterAlpha: enterAlphaMap ?? void 0,
4187
4291
  exiting: exitingGhosts ?? void 0
4188
4292
  };
@@ -4421,7 +4525,10 @@ function createGraph(container, spec, options) {
4421
4525
  positionedNodes,
4422
4526
  cw,
4423
4527
  ch,
4424
- opts?.padding
4528
+ opts?.padding,
4529
+ {
4530
+ insetTop: chromeInsetTop()
4531
+ }
4425
4532
  );
4426
4533
  flyCamera(fitTransform, opts);
4427
4534
  }
@@ -4501,6 +4608,18 @@ function createGraph(container, spec, options) {
4501
4608
  }))
4502
4609
  };
4503
4610
  }
4611
+ function setActiveCategories(values) {
4612
+ if (destroyed) return;
4613
+ activeCategories = new Set(values);
4614
+ highlightSet = categoryHighlightSet();
4615
+ highlightDimOpacity = null;
4616
+ syncLegendActiveState();
4617
+ refreshFocus();
4618
+ emitHighlightChange();
4619
+ }
4620
+ function getActiveCategories() {
4621
+ return [...activeCategories];
4622
+ }
4504
4623
  function toggleLegendCategory(value) {
4505
4624
  if (activeCategories.has(value)) activeCategories.delete(value);
4506
4625
  else activeCategories.add(value);
@@ -4516,6 +4635,7 @@ function createGraph(container, spec, options) {
4516
4635
  const { width, height } = getContainerDimensions();
4517
4636
  const canvasHeight = Math.max(height, 200);
4518
4637
  renderer.resize(width, canvasHeight);
4638
+ syncChromeInset();
4519
4639
  if (entranceFitInFlight && interactionManager) {
4520
4640
  cancelFlight();
4521
4641
  entranceFitInFlight = false;
@@ -4787,6 +4907,9 @@ function createGraph(container, spec, options) {
4787
4907
  },
4788
4908
  getHighlight: () => null,
4789
4909
  getLegend: () => ({ field: null, nodes: [], edges: [] }),
4910
+ setActiveCategories() {
4911
+ },
4912
+ getActiveCategories: () => [],
4790
4913
  resize() {
4791
4914
  },
4792
4915
  destroy() {
@@ -4815,6 +4938,8 @@ function createGraph(container, spec, options) {
4815
4938
  clearHighlight,
4816
4939
  getHighlight,
4817
4940
  getLegend,
4941
+ setActiveCategories,
4942
+ getActiveCategories,
4818
4943
  resize: doResize,
4819
4944
  destroy
4820
4945
  };