force-graph 1.49.5 → 1.50.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.
@@ -418,6 +418,11 @@ var CanvasForceGraph = Kapsule({
418
418
  triggerUpdate: false
419
419
  },
420
420
  // in link length ratio per frame
421
+ linkDirectionalParticleOffset: {
422
+ "default": 0,
423
+ triggerUpdate: false
424
+ },
425
+ // starting position offset along the link's length, like a pre-delay. Values between [0, 1]
421
426
  linkDirectionalParticleWidth: {
422
427
  "default": 4,
423
428
  triggerUpdate: false
@@ -425,6 +430,9 @@ var CanvasForceGraph = Kapsule({
425
430
  linkDirectionalParticleColor: {
426
431
  triggerUpdate: false
427
432
  },
433
+ linkDirectionalParticleCanvasObject: {
434
+ triggerUpdate: false
435
+ },
428
436
  globalScale: {
429
437
  "default": 1,
430
438
  triggerUpdate: false
@@ -746,6 +754,7 @@ var CanvasForceGraph = Kapsule({
746
754
  function paintPhotons() {
747
755
  var getNumPhotons = accessorFn(state.linkDirectionalParticles);
748
756
  var getSpeed = accessorFn(state.linkDirectionalParticleSpeed);
757
+ var getOffset = accessorFn(state.linkDirectionalParticleOffset);
749
758
  var getDiameter = accessorFn(state.linkDirectionalParticleWidth);
750
759
  var getVisibility = accessorFn(state.linkVisibility);
751
760
  var getColor = accessorFn(state.linkDirectionalParticleColor || state.linkColor);
@@ -759,6 +768,7 @@ var CanvasForceGraph = Kapsule({
759
768
  if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
760
769
 
761
770
  var particleSpeed = getSpeed(link);
771
+ var particleOffset = Math.abs(getOffset(link));
762
772
  var photons = link.__photons || [];
763
773
  var photonR = Math.max(0, getDiameter(link) / 2) / Math.sqrt(state.globalScale);
764
774
  var photonColor = getColor(link) || 'rgba(0,0,0,0.28)';
@@ -771,7 +781,7 @@ var CanvasForceGraph = Kapsule({
771
781
  photons.forEach(function (photon) {
772
782
  var singleHop = !!photon.__singleHop;
773
783
  if (!photon.hasOwnProperty('__progressRatio')) {
774
- photon.__progressRatio = singleHop ? 0 : cyclePhotonIdx / numCyclePhotons;
784
+ photon.__progressRatio = singleHop ? 0 : (cyclePhotonIdx + particleOffset) / numCyclePhotons;
775
785
  }
776
786
  !singleHop && cyclePhotonIdx++; // increase regular photon index
777
787
 
@@ -791,9 +801,13 @@ var CanvasForceGraph = Kapsule({
791
801
  x: start.x + (end.x - start.x) * photonPosRatio || 0,
792
802
  y: start.y + (end.y - start.y) * photonPosRatio || 0
793
803
  };
794
- ctx.beginPath();
795
- ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
796
- ctx.fill();
804
+ if (state.linkDirectionalParticleCanvasObject) {
805
+ state.linkDirectionalParticleCanvasObject(coords.x, coords.y, link, ctx, state.globalScale);
806
+ } else {
807
+ ctx.beginPath();
808
+ ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
809
+ ctx.fill();
810
+ }
797
811
  });
798
812
  if (needsCleanup) {
799
813
  // remove expired single hop photons
@@ -945,7 +959,7 @@ var DRAG_CLICK_TOLERANCE_PX = 5; // How many px can a node be accidentally dragg
945
959
  // Expose config from forceGraph
946
960
  var bindFG = linkKapsule('forceGraph', CanvasForceGraph);
947
961
  var bindBoth = linkKapsule(['forceGraph', 'shadowGraph'], CanvasForceGraph);
948
- var linkedProps = Object.assign.apply(Object, _toConsumableArray(['nodeColor', 'nodeAutoColorBy', 'nodeCanvasObject', 'nodeCanvasObjectMode', 'linkColor', 'linkAutoColorBy', 'linkLineDash', 'linkWidth', 'linkCanvasObject', 'linkCanvasObjectMode', 'linkDirectionalArrowLength', 'linkDirectionalArrowColor', 'linkDirectionalArrowRelPos', 'linkDirectionalParticles', 'linkDirectionalParticleSpeed', 'linkDirectionalParticleWidth', 'linkDirectionalParticleColor', 'dagMode', 'dagLevelDistance', 'dagNodeFilter', 'onDagError', 'd3AlphaMin', 'd3AlphaDecay', 'd3VelocityDecay', 'warmupTicks', 'cooldownTicks', 'cooldownTime', 'onEngineTick', 'onEngineStop'].map(function (p) {
962
+ var linkedProps = Object.assign.apply(Object, _toConsumableArray(['nodeColor', 'nodeAutoColorBy', 'nodeCanvasObject', 'nodeCanvasObjectMode', 'linkColor', 'linkAutoColorBy', 'linkLineDash', 'linkWidth', 'linkCanvasObject', 'linkCanvasObjectMode', 'linkDirectionalArrowLength', 'linkDirectionalArrowColor', 'linkDirectionalArrowRelPos', 'linkDirectionalParticles', 'linkDirectionalParticleSpeed', 'linkDirectionalParticleOffset', 'linkDirectionalParticleWidth', 'linkDirectionalParticleColor', 'linkDirectionalParticleCanvasObject', 'dagMode', 'dagLevelDistance', 'dagNodeFilter', 'onDagError', 'd3AlphaMin', 'd3AlphaDecay', 'd3VelocityDecay', 'warmupTicks', 'cooldownTicks', 'cooldownTime', 'onEngineTick', 'onEngineStop'].map(function (p) {
949
963
  return _defineProperty({}, p, bindFG.linkProp(p));
950
964
  })).concat(_toConsumableArray(['nodeRelSize', 'nodeId', 'nodeVal', 'nodeVisibility', 'linkSource', 'linkTarget', 'linkVisibility', 'linkCurvature'].map(function (p) {
951
965
  return _defineProperty({}, p, bindBoth.linkProp(p));
@@ -1539,6 +1553,9 @@ var forceGraph = Kapsule({
1539
1553
 
1540
1554
  // Handle click/touch events on nodes/links
1541
1555
  container.addEventListener('pointerup', function (ev) {
1556
+ if (!state.isPointerPressed) {
1557
+ return; // don't trigger click events if pointer is not pressed on the canvas
1558
+ }
1542
1559
  state.isPointerPressed = false;
1543
1560
  if (state.isPointerDragging) {
1544
1561
  state.isPointerDragging = false;
@@ -65,7 +65,7 @@
65
65
  .d3AlphaDecay(0.02)
66
66
  .d3VelocityDecay(0.3);
67
67
 
68
- fetch('//unpkg.com/d3@5.9.7/yarn.lock')
68
+ fetch('//cdn.jsdelivr.net/npm/d3@5.9.7/yarn.lock')
69
69
  .then(r => r.text())
70
70
  .then(text => {
71
71
  const yarnlock = _yarnpkg_lockfile.parse(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "force-graph",
3
- "version": "1.49.5",
3
+ "version": "1.50.0",
4
4
  "description": "2D force-directed graph rendered on HTML5 canvas",
5
5
  "type": "module",
6
6
  "unpkg": "dist/force-graph.min.js",
@@ -59,24 +59,24 @@
59
59
  "d3-scale-chromatic": "1 - 3",
60
60
  "d3-selection": "2 - 3",
61
61
  "d3-zoom": "2 - 3",
62
- "float-tooltip": "^1.6",
62
+ "float-tooltip": "^1.7",
63
63
  "index-array-by": "1",
64
64
  "kapsule": "^1.16",
65
65
  "lodash-es": "4"
66
66
  },
67
67
  "devDependencies": {
68
- "@babel/core": "^7.26.9",
69
- "@babel/preset-env": "^7.26.9",
68
+ "@babel/core": "^7.28.0",
69
+ "@babel/preset-env": "^7.28.0",
70
70
  "@rollup/plugin-babel": "^6.0.4",
71
- "@rollup/plugin-commonjs": "^28.0.3",
72
- "@rollup/plugin-node-resolve": "^16.0.0",
71
+ "@rollup/plugin-commonjs": "^28.0.6",
72
+ "@rollup/plugin-node-resolve": "^16.0.1",
73
73
  "@rollup/plugin-terser": "^0.4.4",
74
- "postcss": "^8.5.3",
74
+ "postcss": "^8.5.6",
75
75
  "rimraf": "^6.0.1",
76
- "rollup": "^4.34.9",
77
- "rollup-plugin-dts": "^6.1.1",
76
+ "rollup": "^4.44.1",
77
+ "rollup-plugin-dts": "^6.2.1",
78
78
  "rollup-plugin-postcss": "^4.0.2",
79
- "typescript": "^5.8.2"
79
+ "typescript": "^5.8.3"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=12"
@@ -78,8 +78,10 @@ export default Kapsule({
78
78
  linkDirectionalArrowRelPos: { default: 0.5, triggerUpdate: false, onChange: notifyRedraw }, // value between 0<>1 indicating the relative pos along the (exposed) line
79
79
  linkDirectionalParticles: { default: 0, triggerUpdate: false, onChange: updDataPhotons }, // animate photons travelling in the link direction
80
80
  linkDirectionalParticleSpeed: { default: 0.01, triggerUpdate: false }, // in link length ratio per frame
81
+ linkDirectionalParticleOffset: { default: 0, triggerUpdate: false }, // starting position offset along the link's length, like a pre-delay. Values between [0, 1]
81
82
  linkDirectionalParticleWidth: { default: 4, triggerUpdate: false },
82
83
  linkDirectionalParticleColor: { triggerUpdate: false },
84
+ linkDirectionalParticleCanvasObject: { triggerUpdate: false },
83
85
  globalScale: { default: 1, triggerUpdate: false },
84
86
  d3AlphaMin: { default: 0, triggerUpdate: false},
85
87
  d3AlphaDecay: { default: 0.0228, triggerUpdate: false, onChange(alphaDecay, state) { state.forceLayout.alphaDecay(alphaDecay) }},
@@ -365,6 +367,7 @@ export default Kapsule({
365
367
  function paintPhotons() {
366
368
  const getNumPhotons = accessorFn(state.linkDirectionalParticles);
367
369
  const getSpeed = accessorFn(state.linkDirectionalParticleSpeed);
370
+ const getOffset = accessorFn(state.linkDirectionalParticleOffset);
368
371
  const getDiameter = accessorFn(state.linkDirectionalParticleWidth);
369
372
  const getVisibility = accessorFn(state.linkVisibility);
370
373
  const getColor = accessorFn(state.linkDirectionalParticleColor || state.linkColor);
@@ -382,6 +385,7 @@ export default Kapsule({
382
385
  if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
383
386
 
384
387
  const particleSpeed = getSpeed(link);
388
+ const particleOffset = Math.abs(getOffset(link));
385
389
  const photons = link.__photons || [];
386
390
  const photonR = Math.max(0, getDiameter(link) / 2) / Math.sqrt(state.globalScale);
387
391
  const photonColor = getColor(link) || 'rgba(0,0,0,0.28)';
@@ -399,7 +403,7 @@ export default Kapsule({
399
403
  const singleHop = !!photon.__singleHop;
400
404
 
401
405
  if (!photon.hasOwnProperty('__progressRatio')) {
402
- photon.__progressRatio = singleHop ? 0 : cyclePhotonIdx / numCyclePhotons;
406
+ photon.__progressRatio = singleHop ? 0 : (cyclePhotonIdx + particleOffset) / numCyclePhotons;
403
407
  }
404
408
 
405
409
  !singleHop && cyclePhotonIdx++; // increase regular photon index
@@ -424,9 +428,13 @@ export default Kapsule({
424
428
  y: start.y + (end.y - start.y) * photonPosRatio || 0
425
429
  };
426
430
 
427
- ctx.beginPath();
428
- ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
429
- ctx.fill();
431
+ if(state.linkDirectionalParticleCanvasObject) {
432
+ state.linkDirectionalParticleCanvasObject(coords.x, coords.y, link, ctx, state.globalScale);
433
+ } else {
434
+ ctx.beginPath();
435
+ ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
436
+ ctx.fill();
437
+ }
430
438
  });
431
439
 
432
440
  if (needsCleanup) {
@@ -36,8 +36,10 @@ const linkedProps = Object.assign(
36
36
  'linkDirectionalArrowRelPos',
37
37
  'linkDirectionalParticles',
38
38
  'linkDirectionalParticleSpeed',
39
+ 'linkDirectionalParticleOffset',
39
40
  'linkDirectionalParticleWidth',
40
41
  'linkDirectionalParticleColor',
42
+ 'linkDirectionalParticleCanvasObject',
41
43
  'dagMode',
42
44
  'dagLevelDistance',
43
45
  'dagNodeFilter',
@@ -547,6 +549,10 @@ export default Kapsule({
547
549
 
548
550
  // Handle click/touch events on nodes/links
549
551
  container.addEventListener('pointerup', ev => {
552
+ if (!state.isPointerPressed) {
553
+ return; // don't trigger click events if pointer is not pressed on the canvas
554
+ }
555
+
550
556
  state.isPointerPressed = false;
551
557
  if (state.isPointerDragging) {
552
558
  state.isPointerDragging = false;
package/src/index.d.ts CHANGED
@@ -29,6 +29,7 @@ type CanvasCustomRenderMode = 'replace' | 'before' | 'after';
29
29
  export type CanvasCustomRenderModeFn<T> = (obj: T) => CanvasCustomRenderMode | any;
30
30
  export type CanvasCustomRenderFn<T> = (obj: T, canvasContext: CanvasRenderingContext2D, globalScale: number) => void;
31
31
  export type CanvasPointerAreaPaintFn<T> = (obj: T, paintColor: string, canvasContext: CanvasRenderingContext2D, globalScale: number) => void;
32
+ export type CanvasLinkParticleRenderFn<L> = (x: number, y: number, link: L, canvasContext: CanvasRenderingContext2D, globalScale: number) => void;
32
33
 
33
34
  type DagMode = 'td' | 'bu' | 'lr' | 'rl' | 'radialout' | 'radialin';
34
35
 
@@ -110,10 +111,14 @@ export declare class ForceGraphGeneric<ChainableInstance, N extends NodeObject =
110
111
  linkDirectionalParticles(numParticlesAccessor: LinkAccessor<number, N, L>): ChainableInstance;
111
112
  linkDirectionalParticleSpeed(): LinkAccessor<number, N, L>;
112
113
  linkDirectionalParticleSpeed(relDistancePerFrameAccessor: LinkAccessor<number, N, L>): ChainableInstance;
114
+ linkDirectionalParticleOffset(): LinkAccessor<number, N, L>;
115
+ linkDirectionalParticleOffset(relOffset: LinkAccessor<number, N, L>): ChainableInstance;
113
116
  linkDirectionalParticleWidth(): LinkAccessor<number, N, L>;
114
117
  linkDirectionalParticleWidth(widthAccessor: LinkAccessor<number, N, L>): ChainableInstance;
115
118
  linkDirectionalParticleColor(): LinkAccessor<string, N, L>;
116
119
  linkDirectionalParticleColor(colorAccessor: LinkAccessor<string, N, L>): ChainableInstance;
120
+ linkDirectionalParticleCanvasObject(): CanvasLinkParticleRenderFn<L>;
121
+ linkDirectionalParticleCanvasObject(renderFn: CanvasLinkParticleRenderFn<L>): ChainableInstance;
117
122
  emitParticle(link: L): ChainableInstance;
118
123
  linkPointerAreaPaint(): CanvasPointerAreaPaintFn<L>;
119
124
  linkPointerAreaPaint(renderFn: CanvasPointerAreaPaintFn<L>): ChainableInstance;