@opendata-ai/openchart-vanilla 8.5.1 → 8.6.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.
@@ -200,10 +200,11 @@ declare const LINK_WIDTH_MAX_EDGES = 2000;
200
200
  * setters.
201
201
  *
202
202
  * Differences from 2D that are deliberate and documented in RFC 27: no
203
- * keyboard navigation, no node drag, no cursor repulsion or springy drag, no
204
- * wall-clock warmup budget, and a structural `update()` reheats globally
205
- * (`graphData()` restarts the layout at alpha 1) instead of applying 2D's local
206
- * impulse.
203
+ * keyboard navigation, no node drag, no cursor repulsion or springy drag, and
204
+ * no wall-clock warmup budget. A structural `update()` DOES animate like 2D
205
+ * (enter fade, exit ghosts, churn-scaled local reheat), but it reaches the
206
+ * impulse indirectly: `graphData()` forces alpha to 1 and the library exposes no
207
+ * alpha setter, so `beginUpdateReheat` damps it from outside instead.
207
208
  */
208
209
 
209
210
  /**
@@ -11,9 +11,10 @@ import {
11
11
  popAlpha,
12
12
  popScale,
13
13
  registerGraphRenderer,
14
+ reheatAlpha,
14
15
  resolveHighlightTarget,
15
16
  seedNodePositions
16
- } from "../chunk-WOLX5EZT.js";
17
+ } from "../chunk-273EHRBC.js";
17
18
  import {
18
19
  AnimationScheduler,
19
20
  createTween,
@@ -224,7 +225,7 @@ function forceCluster3D(strength) {
224
225
  };
225
226
  return force;
226
227
  }
227
- function applySimulationConfig(graph, config, nodeCount) {
228
+ function applySimulationConfig(graph, config, nodeCount, opts = {}) {
228
229
  const charge = graph.d3Force("charge");
229
230
  if (charge && typeof charge.strength === "function") {
230
231
  charge.strength(config.chargeStrength);
@@ -247,9 +248,24 @@ function applySimulationConfig(graph, config, nodeCount) {
247
248
  );
248
249
  graph.d3VelocityDecay(config.velocityDecay);
249
250
  graph.d3AlphaDecay(config.alphaDecay);
250
- graph.warmupTicks(budgetedWarmupTicks(config.warmupTicks ?? 0, nodeCount, config.warmupBudgetMs));
251
+ graph.warmupTicks(opts.warmupTicks ?? configuredWarmupTicks(config, nodeCount));
251
252
  graph.cooldownTicks(ticksToSettle(config.alphaDecay));
252
253
  }
254
+ function configuredWarmupTicks(config, nodeCount) {
255
+ return budgetedWarmupTicks(config.warmupTicks ?? 0, nodeCount, config.warmupBudgetMs);
256
+ }
257
+ function beginUpdateReheat(graph, config, initialAlpha) {
258
+ graph.d3Force("center", null);
259
+ const scale = Math.min(1, Math.max(0, initialAlpha));
260
+ graph.d3VelocityDecay(1 - (1 - config.velocityDecay) * scale);
261
+ }
262
+ function endUpdateReheat(graph, config, nodeCount) {
263
+ graph.d3VelocityDecay(config.velocityDecay);
264
+ graph.warmupTicks(configuredWarmupTicks(config, nodeCount));
265
+ if (config.centerForce !== false) {
266
+ graph.d3Force("center", graph.d3Force("center") ?? forceCenter());
267
+ }
268
+ }
253
269
 
254
270
  // src/graph-3d/labels.ts
255
271
  var LABEL_BUDGET_3D = 40;
@@ -389,6 +405,13 @@ function updateLinkPosition(obj, start, end) {
389
405
  if (obj.dashed) obj.object.computeLineDistances();
390
406
  return true;
391
407
  }
408
+ function placeGhostLink(obj, start, end) {
409
+ if (updateLinkPosition(obj, start, end)) return;
410
+ const mesh = obj.object;
411
+ mesh.position.set(start.x, start.y, start.z);
412
+ mesh.lookAt(end.x, end.y, end.z);
413
+ mesh.scale.z = Math.hypot(end.x - start.x, end.y - start.y, end.z - start.z);
414
+ }
392
415
  function disposeLinkObject(obj) {
393
416
  obj.geometry.dispose();
394
417
  obj.material.dispose();
@@ -545,6 +568,8 @@ function createGraph3DRenderer(ctx) {
545
568
  const entranceEdgeAlpha = /* @__PURE__ */ new Map();
546
569
  let entranceCameraPull = 1;
547
570
  let emphasisTween = null;
571
+ let ghosts = [];
572
+ let reheatActive = false;
548
573
  let pumpId = null;
549
574
  const scheduler = new AnimationScheduler(() => pump());
550
575
  function pump() {
@@ -620,11 +645,13 @@ function createGraph3DRenderer(ctx) {
620
645
  const p = positions.get(node.id) ?? [0, 0, 0];
621
646
  return { id: node.id, node, x: p[0], y: p[1], z: p[2] };
622
647
  });
648
+ const keys = edgeKeys(compilation.edges);
623
649
  linkData = compilation.edges.map((edge, edgeIndex) => ({
624
650
  source: edge.source,
625
651
  target: edge.target,
626
652
  edge,
627
- edgeIndex
653
+ edgeIndex,
654
+ key: keys[edgeIndex]
628
655
  }));
629
656
  nodeById = new Map(nodeData.map((d) => [d.id, d]));
630
657
  graph.graphData({ nodes: nodeData, links: linkData });
@@ -647,11 +674,11 @@ function createGraph3DRenderer(ctx) {
647
674
  }
648
675
  const linkThreeObject = (datum) => linkObjectFor(datum).object;
649
676
  function linkObjectFor(datum) {
650
- let obj = linkObjects.get(datum.edgeIndex);
677
+ let obj = linkObjects.get(datum.key);
651
678
  if (!obj) {
652
679
  const resting = edgeBaseAlpha?.get(datum.edgeIndex) ?? 0.3;
653
680
  obj = createLinkObject(datum.edge, useLinkWidth, resting);
654
- linkObjects.set(datum.edgeIndex, obj);
681
+ linkObjects.set(datum.key, obj);
655
682
  const base = displayEdgeAlpha.get(datum.edgeIndex) ?? resting;
656
683
  obj.material.opacity = entranceActive ? base * (entranceEdgeAlpha.get(datum.edgeIndex) ?? 0) : base;
657
684
  }
@@ -700,10 +727,11 @@ function createGraph3DRenderer(ctx) {
700
727
  obj.material.opacity = a;
701
728
  }
702
729
  }
703
- for (const [index, obj] of linkObjects) {
704
- const a = displayEdgeAlpha.get(index);
705
- if (a === void 0) continue;
706
- obj.material.opacity = entranceActive ? a * (entranceEdgeAlpha.get(index) ?? 0) : a;
730
+ for (const datum of linkData) {
731
+ const obj = linkObjects.get(datum.key);
732
+ const a = displayEdgeAlpha.get(datum.edgeIndex);
733
+ if (!obj || a === void 0) continue;
734
+ obj.material.opacity = entranceActive ? a * (entranceEdgeAlpha.get(datum.edgeIndex) ?? 0) : a;
707
735
  }
708
736
  }
709
737
  function armEmphasis() {
@@ -1204,6 +1232,104 @@ function createGraph3DRenderer(ctx) {
1204
1232
  });
1205
1233
  scheduler.add(tween);
1206
1234
  }
1235
+ function ghostParent() {
1236
+ for (const obj of nodeObjects.values()) {
1237
+ if (obj.group.parent) return obj.group.parent;
1238
+ }
1239
+ return graph.scene() ?? null;
1240
+ }
1241
+ function nodeGhost(node, pos, alpha, withLabel) {
1242
+ const obj = createNodeObject(node, labelColor());
1243
+ obj.group.position.set(pos.x, pos.y, pos.z);
1244
+ obj.material.opacity = alpha;
1245
+ const fades = [{ material: obj.material, base: alpha }];
1246
+ if (withLabel) {
1247
+ const sprite = ensureLabelSprite(obj);
1248
+ if (sprite) {
1249
+ sprite.visible = true;
1250
+ fades.push({
1251
+ material: sprite.material,
1252
+ base: 1
1253
+ });
1254
+ }
1255
+ }
1256
+ return { object: obj.group, fades, dispose: () => disposeNodeObject(obj) };
1257
+ }
1258
+ function linkGhost(edge, start, end, alpha, useWidth) {
1259
+ const obj = createLinkObject(edge, useWidth, alpha);
1260
+ placeGhostLink(obj, start, end);
1261
+ return {
1262
+ object: obj.object,
1263
+ fades: [{ material: obj.material, base: alpha }],
1264
+ dispose: () => disposeLinkObject(obj)
1265
+ };
1266
+ }
1267
+ function startExitFade(pending) {
1268
+ if (pending.length === 0) return;
1269
+ const cfg = compilation.animation?.exit ?? null;
1270
+ const parent = ghostParent();
1271
+ if (!cfg || prefersReducedMotion() || !parent) {
1272
+ for (const g of pending) g.dispose();
1273
+ return;
1274
+ }
1275
+ for (const g of pending) {
1276
+ for (const f of g.fades) f.material.transparent = true;
1277
+ parent.add(g.object);
1278
+ }
1279
+ ghosts = ghosts.concat(pending);
1280
+ scheduler.add(
1281
+ createTween({
1282
+ duration: cfg.duration,
1283
+ ease: resolveEase(cfg.ease),
1284
+ apply: (t) => {
1285
+ for (const g of pending) {
1286
+ for (const f of g.fades) f.material.opacity = f.base * (1 - t);
1287
+ }
1288
+ },
1289
+ onDone: () => removeGhosts(pending)
1290
+ })
1291
+ );
1292
+ }
1293
+ function removeGhosts(list) {
1294
+ for (const g of list) {
1295
+ g.object.parent?.remove(g.object);
1296
+ g.dispose();
1297
+ }
1298
+ const gone = new Set(list);
1299
+ ghosts = ghosts.filter((g) => !gone.has(g));
1300
+ }
1301
+ function startEnterFade(enteringIds, enteringEdgeIndices) {
1302
+ const cfg = compilation.animation?.update ?? null;
1303
+ if (enteringIds.length === 0 && enteringEdgeIndices.length === 0) return;
1304
+ if (!cfg || prefersReducedMotion()) return;
1305
+ entranceActive = true;
1306
+ for (const node of compilation.nodes) {
1307
+ entranceNodeAlpha.set(node.id, 1);
1308
+ entranceNodeScale.set(node.id, 1);
1309
+ }
1310
+ for (let i = 0; i < compilation.edges.length; i++) entranceEdgeAlpha.set(i, 1);
1311
+ for (const id of enteringIds) {
1312
+ entranceNodeAlpha.set(id, 0);
1313
+ entranceNodeScale.set(id, ENTRANCE_MIN_SCALE);
1314
+ }
1315
+ for (const index of enteringEdgeIndices) entranceEdgeAlpha.set(index, 0);
1316
+ paint();
1317
+ scheduler.add(
1318
+ createTween({
1319
+ duration: cfg.duration,
1320
+ ease: resolveEase(cfg.ease),
1321
+ apply: (t) => {
1322
+ for (const id of enteringIds) {
1323
+ entranceNodeAlpha.set(id, popAlpha(t));
1324
+ entranceNodeScale.set(id, Math.max(ENTRANCE_MIN_SCALE, popScale(t)));
1325
+ }
1326
+ for (const index of enteringEdgeIndices) entranceEdgeAlpha.set(index, t);
1327
+ paint();
1328
+ },
1329
+ onDone: endEntrance
1330
+ })
1331
+ );
1332
+ }
1207
1333
  function search(query) {
1208
1334
  if (destroyed) return;
1209
1335
  searchManager.search(query, compilation.nodes);
@@ -1307,7 +1433,7 @@ function createGraph3DRenderer(ctx) {
1307
1433
  const edge = compilation.edges[i];
1308
1434
  linkData[i].edge = edge;
1309
1435
  if (shapeClassChanged) continue;
1310
- const obj = linkObjects.get(i);
1436
+ const obj = linkObjects.get(linkData[i].key);
1311
1437
  if (obj) applyLinkVisuals(obj, edge, useLinkWidth);
1312
1438
  }
1313
1439
  if (shapeClassChanged) {
@@ -1320,17 +1446,46 @@ function createGraph3DRenderer(ctx) {
1320
1446
  return;
1321
1447
  }
1322
1448
  const nextNodeIds = new Set(compilation.nodes.map((n) => n.id));
1449
+ const keys = edgeKeys(compilation.edges);
1450
+ const nextKeys = new Set(keys);
1451
+ const prevLinkByKey = new Map(linkData.map((d) => [d.key, d]));
1452
+ const pendingGhosts = [];
1323
1453
  for (const [id, obj] of nodeObjects) {
1324
- if (!nextNodeIds.has(id)) {
1325
- disposeNodeObject(obj);
1326
- nodeObjects.delete(id);
1327
- displayNodeAlpha.delete(id);
1328
- entranceNodeAlpha.delete(id);
1329
- entranceNodeScale.delete(id);
1454
+ if (nextNodeIds.has(id)) continue;
1455
+ const datum = nodeById.get(id);
1456
+ if (datum) {
1457
+ pendingGhosts.push(
1458
+ nodeGhost(
1459
+ datum.node,
1460
+ { x: datum.x ?? 0, y: datum.y ?? 0, z: datum.z ?? 0 },
1461
+ displayNodeAlpha.get(id) ?? datum.node.opacity,
1462
+ Boolean(obj.sprite?.visible)
1463
+ )
1464
+ );
1330
1465
  }
1466
+ disposeNodeObject(obj);
1467
+ nodeObjects.delete(id);
1468
+ displayNodeAlpha.delete(id);
1469
+ entranceNodeAlpha.delete(id);
1470
+ entranceNodeScale.delete(id);
1471
+ }
1472
+ for (const [key, obj] of linkObjects) {
1473
+ if (nextKeys.has(key) && !shapeClassChanged) continue;
1474
+ const datum = prevLinkByKey.get(key);
1475
+ if (datum && !nextKeys.has(key)) {
1476
+ pendingGhosts.push(
1477
+ linkGhost(
1478
+ datum.edge,
1479
+ endpointPos(datum.source),
1480
+ endpointPos(datum.target),
1481
+ displayEdgeAlpha.get(datum.edgeIndex) ?? 0.3,
1482
+ prevUseLinkWidth
1483
+ )
1484
+ );
1485
+ }
1486
+ disposeLinkObject(obj);
1487
+ linkObjects.delete(key);
1331
1488
  }
1332
- for (const obj of linkObjects.values()) disposeLinkObject(obj);
1333
- linkObjects.clear();
1334
1489
  displayEdgeAlpha.clear();
1335
1490
  entranceEdgeAlpha.clear();
1336
1491
  const survivors = new Map(nodeById);
@@ -1351,15 +1506,33 @@ function createGraph3DRenderer(ctx) {
1351
1506
  z: seedZ(node.id, node.radius, node.community)
1352
1507
  };
1353
1508
  });
1354
- linkData = compilation.edges.map((edge, edgeIndex) => ({
1355
- source: edge.source,
1356
- target: edge.target,
1357
- edge,
1358
- edgeIndex
1359
- }));
1509
+ const enteringEdgeIndices = [];
1510
+ linkData = compilation.edges.map((edge, edgeIndex) => {
1511
+ const key = keys[edgeIndex];
1512
+ const existing = shapeClassChanged ? void 0 : prevLinkByKey.get(key);
1513
+ if (!prevLinkByKey.has(key)) enteringEdgeIndices.push(edgeIndex);
1514
+ if (existing) {
1515
+ existing.edge = edge;
1516
+ existing.edgeIndex = edgeIndex;
1517
+ const obj = linkObjects.get(key);
1518
+ if (obj) applyLinkVisuals(obj, edge, useLinkWidth);
1519
+ return existing;
1520
+ }
1521
+ return { source: edge.source, target: edge.target, edge, edgeIndex, key };
1522
+ });
1360
1523
  nodeById = new Map(nodeData.map((d) => [d.id, d]));
1361
- applySimulationConfig(graph, compilation.simulationConfig, nodeData.length);
1524
+ applySimulationConfig(graph, compilation.simulationConfig, nodeData.length, {
1525
+ warmupTicks: 0
1526
+ });
1527
+ beginUpdateReheat(
1528
+ graph,
1529
+ compilation.simulationConfig,
1530
+ reheatAlpha(diff, compilation.edges.length)
1531
+ );
1532
+ reheatActive = true;
1362
1533
  graph.graphData({ nodes: nodeData, links: linkData });
1534
+ startEnterFade(diff.enteringIds, enteringEdgeIndices);
1535
+ startExitFade(pendingGhosts);
1363
1536
  pruneInteractionState();
1364
1537
  armEmphasis();
1365
1538
  }
@@ -1381,6 +1554,7 @@ function createGraph3DRenderer(ctx) {
1381
1554
  if (destroyed) return;
1382
1555
  destroyed = true;
1383
1556
  scheduler.cancelAll();
1557
+ removeGhosts(ghosts);
1384
1558
  if (pumpId !== null) {
1385
1559
  cancelAnimationFrame(pumpId);
1386
1560
  pumpId = null;
@@ -1423,7 +1597,7 @@ function createGraph3DRenderer(ctx) {
1423
1597
  graph.backgroundColor(resolvedSurface(compilation.theme));
1424
1598
  }
1425
1599
  graph.showNavInfo(false).enableNodeDrag(false).nodeId("id").nodeLabel("").linkLabel("").nodeVal((d) => d.node.radius).nodeColor((d) => d.node.fill).nodeThreeObject((d) => nodeObjectFor(d).group).linkColor((d) => d.edge.stroke).linkWidth((d) => useLinkWidth ? d.edge.strokeWidth : 0).linkThreeObject(linkThreeObject).linkPositionUpdate((_obj, coords, d) => {
1426
- const link = linkObjects.get(d.edgeIndex);
1600
+ const link = linkObjects.get(d.key);
1427
1601
  return link ? updateLinkPosition(link, coords.start, coords.end) : false;
1428
1602
  }).linkHoverPrecision(useLinkWidth ? 4 : 8);
1429
1603
  const { width, height } = shell.getSize();
@@ -1484,6 +1658,10 @@ function createGraph3DRenderer(ctx) {
1484
1658
  }
1485
1659
  }).onEngineStop(() => {
1486
1660
  if (destroyed) return;
1661
+ if (reheatActive) {
1662
+ reheatActive = false;
1663
+ endUpdateReheat(graph, compilation.simulationConfig, nodeData.length);
1664
+ }
1487
1665
  if (autoFit && options?.fitOnLoad !== false) {
1488
1666
  autoFit = false;
1489
1667
  fitNow({ duration: 0 });
@@ -1531,6 +1709,15 @@ function createGraph3DRenderer(ctx) {
1531
1709
  destroy
1532
1710
  };
1533
1711
  }
1712
+ function edgeKeys(edges) {
1713
+ const seen = /* @__PURE__ */ new Map();
1714
+ return edges.map((edge) => {
1715
+ const base = `${edge.source}\0${edge.target}`;
1716
+ const n = seen.get(base) ?? 0;
1717
+ seen.set(base, n + 1);
1718
+ return `${base}\0${n}`;
1719
+ });
1720
+ }
1534
1721
  function themeBackground(theme) {
1535
1722
  const colors = theme?.colors;
1536
1723
  if (!colors || Array.isArray(colors)) return void 0;