@krainovsd/graph 0.13.0-beta1 → 0.13.0-beta3

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/lib/cjs/index.cjs CHANGED
@@ -421,50 +421,52 @@ function nodeIterationExtractor(node, i, nodes, state, option, optionConstantGet
421
421
  return customOptions;
422
422
  }
423
423
 
424
- function calculateLinkPositionByNode(link, arrowSize = 0) {
425
- const source = link.source;
426
- const target = link.target;
427
- if (typeof source != "object" || typeof target != "object")
428
- return null;
424
+ function calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, arrowSize = 0) {
429
425
  const sourcePoint = getLinkPoint({
426
+ xStart,
427
+ yStart,
428
+ xEnd,
429
+ yEnd,
430
430
  arrowSize: 0,
431
- node: source,
432
- oppositeNode: target,
433
- });
431
+ node: sourceNode});
434
432
  const targetPoint = getLinkPoint({
435
- arrowSize: arrowSize > 0 ? arrowSize * 0.85 : 0,
436
- node: target,
437
- oppositeNode: source,
438
- });
439
- if (!sourcePoint || !targetPoint)
440
- return null;
433
+ xStart: xEnd,
434
+ yStart: yEnd,
435
+ xEnd: xStart,
436
+ yEnd: yStart,
437
+ arrowSize: arrowSize > 0 ? arrowSize * 0.9 : 0,
438
+ node: targetNode});
439
+ const targetPointArrow = getLinkPoint({
440
+ xStart: xEnd,
441
+ yStart: yEnd,
442
+ xEnd: xStart,
443
+ yEnd: yStart,
444
+ arrowSize: 0,
445
+ node: targetNode});
441
446
  const distance = targetPoint && sourcePoint
442
447
  ? Math.sqrt((targetPoint.x - sourcePoint.x) ** 2 + (targetPoint.y - sourcePoint.y) ** 2)
443
448
  : 0;
444
449
  return {
445
- x1: sourcePoint.x,
446
- y1: sourcePoint.y,
447
- x2: targetPoint.x,
448
- y2: targetPoint.y,
450
+ xStart: sourcePoint.x,
451
+ yStart: sourcePoint.y,
452
+ xEnd: targetPoint.x,
453
+ yEnd: targetPoint.y,
454
+ xEndArrow: targetPointArrow.x,
455
+ yEndArrow: targetPointArrow.y,
449
456
  distance,
450
457
  };
451
458
  }
452
459
  function getLinkPoint(opts) {
453
- if (opts.node.x == undefined ||
454
- opts.node.y == undefined ||
455
- opts.oppositeNode.x == undefined ||
456
- opts.oppositeNode.y == undefined)
457
- return null;
458
460
  let nodePoint;
459
461
  switch (opts.node._shape) {
460
462
  case "circle": {
461
463
  nodePoint = getCircleIntersection({
462
- x: opts.node.x,
463
- y: opts.node.y,
464
+ x: opts.xStart,
465
+ y: opts.yStart,
464
466
  radius: (opts.node._radius ?? COMMON_SETTINGS.nodeRadius) +
465
467
  (opts.node._borderWidth ? opts.node._borderWidth / 2 : 0),
466
- oppositeX: opts.oppositeNode.x,
467
- oppositeY: opts.oppositeNode.y,
468
+ oppositeX: opts.xEnd,
469
+ oppositeY: opts.yEnd,
468
470
  arrowSize: opts.arrowSize,
469
471
  });
470
472
  break;
@@ -473,23 +475,23 @@ function getLinkPoint(opts) {
473
475
  case "text": {
474
476
  nodePoint = getRectangleIntersection({
475
477
  arrowSize: opts.arrowSize,
476
- x: opts.node.x,
477
- y: opts.node.y,
478
+ x: opts.xStart,
479
+ y: opts.yStart,
478
480
  height: (opts.node._height ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
479
481
  width: (opts.node._width ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
480
- oppositeX: opts.oppositeNode.x,
481
- oppositeY: opts.oppositeNode.y,
482
+ oppositeX: opts.xEnd,
483
+ oppositeY: opts.yEnd,
482
484
  borderRadius: opts.node._shape === "text" ? 0 : (opts.node._borderRadius ?? 0),
483
485
  });
484
486
  break;
485
487
  }
486
488
  default: {
487
489
  nodePoint = getCircleIntersection({
488
- x: opts.node.x,
489
- y: opts.node.y,
490
+ x: opts.xStart,
491
+ y: opts.yStart,
490
492
  radius: opts.node._radius ?? COMMON_SETTINGS.nodeRadius,
491
- oppositeX: opts.oppositeNode.x,
492
- oppositeY: opts.oppositeNode.y,
493
+ oppositeX: opts.xEnd,
494
+ oppositeY: opts.yEnd,
493
495
  arrowSize: opts.arrowSize,
494
496
  });
495
497
  }
@@ -770,31 +772,34 @@ function getParticlePosition(opts) {
770
772
  }
771
773
  }
772
774
 
773
- function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, }) {
775
+ function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, curve, }) {
774
776
  if (!areaRect)
775
777
  return undefined;
776
778
  const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
777
779
  return d3Array.greatest(links, (link) => {
778
- if (isNearLink(pointerX, pointerY, link, linkHoverExtraZone))
779
- return link.index;
780
+ if (!jsHelpers.isObject(link.source) ||
781
+ !jsHelpers.isObject(link.target) ||
782
+ link.source.visible === false ||
783
+ link.target.visible === false)
784
+ return;
785
+ const x1 = (link._x1 ?? link.source.x);
786
+ const y1 = (link._y1 ?? link.source.y);
787
+ const x2 = (link._x2 ?? link.target.x);
788
+ const y2 = (link._y2 ?? link.target.y);
789
+ const cx = link._cx ?? 0;
790
+ const cy = link._cy ?? 0;
791
+ return curve
792
+ ? hitTestQuadratic(pointerX, pointerY, x1, y1, cx, cy, x2, y2, linkHoverExtraZone)
793
+ ? link.index
794
+ : undefined
795
+ : isNearLink(pointerX, pointerY, x1, y1, x2, y2, linkHoverExtraZone)
796
+ ? link.index
797
+ : undefined;
780
798
  });
781
799
  }
782
- function isNearLink(mouseX, mouseY, link, threshold = 2) {
783
- if (!jsHelpers.isObject(link.source) ||
784
- !jsHelpers.isObject(link.target) ||
785
- link.source.visible === false ||
786
- link.target.visible === false)
787
- return false;
788
- const x1 = link.source.x;
789
- const y1 = link.source.y;
790
- const x2 = link.target.x;
791
- const y2 = link.target.y;
792
- const distance = distanceToLine(mouseX, mouseY, x1, y1, x2, y2);
793
- return distance <= threshold;
794
- }
795
- function distanceToLine(x, y, x1, y1, x2, y2) {
796
- const A = x - x1;
797
- const B = y - y1;
800
+ function isNearLink(px, py, x1, y1, x2, y2, threshold = 2) {
801
+ const A = px - x1;
802
+ const B = py - y1;
798
803
  const C = x2 - x1;
799
804
  const D = y2 - y1;
800
805
  const dot = A * C + B * D;
@@ -816,9 +821,56 @@ function distanceToLine(x, y, x1, y1, x2, y2) {
816
821
  xx = x1 + param * C;
817
822
  yy = y1 + param * D;
818
823
  }
819
- const dx = x - xx;
820
- const dy = y - yy;
821
- return Math.sqrt(dx * dx + dy * dy);
824
+ const dx = px - xx;
825
+ const dy = py - yy;
826
+ return Math.sqrt(dx * dx + dy * dy) <= threshold;
827
+ }
828
+ function hitTestQuadratic(px, py, x0, y0, xc, yc, x2, y2, tol) {
829
+ const bb = bezierBBox(x0, y0, xc, yc, x2, y2);
830
+ if (px < bb.minX - tol || px > bb.maxX + tol || py < bb.minY - tol || py > bb.maxY + tol) {
831
+ return false;
832
+ }
833
+ const chord = Math.hypot(x2 - x0, y2 - y0);
834
+ const contLen = Math.hypot(xc - x0, yc - y0) + Math.hypot(x2 - xc, y2 - yc);
835
+ const approxLen = (chord + contLen) / 2;
836
+ const steps = Math.max(16, Math.ceil(approxLen / 1));
837
+ const tol2 = tol * tol;
838
+ for (let i = 0; i <= steps; i++) {
839
+ const t = i / steps;
840
+ const mt = 1 - t;
841
+ const x = mt * mt * x0 + 2 * mt * t * xc + t * t * x2;
842
+ const y = mt * mt * y0 + 2 * mt * t * yc + t * t * y2;
843
+ const dx = px - x;
844
+ const dy = py - y;
845
+ if (dx * dx + dy * dy <= tol2)
846
+ return true;
847
+ }
848
+ return false;
849
+ }
850
+ function bezierBBox(x0, y0, x1, y1, x2, y2) {
851
+ function extrema(p0, p1, p2) {
852
+ const denom = p0 - 2 * p1 + p2;
853
+ if (denom === 0)
854
+ return [];
855
+ const t = (p0 - p1) / denom;
856
+ return t > 0 && t < 1 ? [t] : [];
857
+ }
858
+ const ts = [...extrema(x0, x1, x2), ...extrema(y0, y1, y2), 0, 1];
859
+ let maxX = -Infinity, maxY = -Infinity, minX = Infinity, minY = Infinity;
860
+ for (const t of ts) {
861
+ const mt = 1 - t;
862
+ const x = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
863
+ const y = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
864
+ if (x < minX)
865
+ minX = x;
866
+ if (x > maxX)
867
+ maxX = x;
868
+ if (y < minY)
869
+ minY = y;
870
+ if (y > maxY)
871
+ maxY = y;
872
+ }
873
+ return { minX, minY, maxX, maxY };
822
874
  }
823
875
 
824
876
  function nodeHighlight(opts) {
@@ -1067,6 +1119,7 @@ const HIGHLIGHT_SETTINGS = {
1067
1119
 
1068
1120
  const LINK_SETTINGS = {
1069
1121
  prettyDraw: true,
1122
+ curve: false,
1070
1123
  linkScaleSwitch: 1,
1071
1124
  linkColorAfterScaleSwitch: "#000000FF",
1072
1125
  linkColorBeforeScaleSwitch: "#999",
@@ -1234,6 +1287,42 @@ function initDnd() {
1234
1287
  d3Selection.select(this.area).call(dragHandler);
1235
1288
  }
1236
1289
 
1290
+ function calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, groupIndex, arrowSize = 0) {
1291
+ const sourceRadius = (sourceNode._radius ?? COMMON_SETTINGS.nodeRadius) + (sourceNode._borderWidth ?? 0) * 0.5;
1292
+ const targetRadius = (targetNode._radius ?? COMMON_SETTINGS.nodeRadius) + (targetNode._borderWidth ?? 0) * 0.5;
1293
+ const effectiveTargetRadius = targetRadius + arrowSize * 0.9;
1294
+ const mx = (xStart + xEnd) * 0.5;
1295
+ const my = (yStart + yEnd) * 0.5;
1296
+ const dx = xEnd - xStart;
1297
+ const dy = yEnd - yStart;
1298
+ const lenSq = dx * dx + dy * dy;
1299
+ const len = lenSq > 0 ? Math.sqrt(lenSq) : 1;
1300
+ const nx = -dy / len;
1301
+ const ny = dx / len;
1302
+ const offset = 0.12 * len * groupIndex;
1303
+ const xControl = mx + nx * offset;
1304
+ const yControl = my + ny * offset;
1305
+ const [xStartP, yStartP] = adjustPoint(xStart, yStart, xControl, yControl, sourceRadius);
1306
+ const [xEndP, yEndP] = adjustPoint(xEnd, yEnd, xControl, yControl, effectiveTargetRadius);
1307
+ const [xEndArrow, yEndArrow] = adjustPoint(xEnd, yEnd, xControl, yControl, targetRadius);
1308
+ return {
1309
+ xStart: xStartP,
1310
+ yStart: yStartP,
1311
+ xEnd: xEndP,
1312
+ yEnd: yEndP,
1313
+ xControl,
1314
+ yControl,
1315
+ xEndArrow,
1316
+ yEndArrow,
1317
+ };
1318
+ }
1319
+ function adjustPoint(x, y, xControl, yControl, radius) {
1320
+ const dxToControl = xControl - x;
1321
+ const dyToControl = yControl - y;
1322
+ const distToControl = Math.sqrt(dxToControl * dxToControl + dyToControl * dyToControl) || 1;
1323
+ return [x + (dxToControl / distToControl) * radius, y + (dyToControl / distToControl) * radius];
1324
+ }
1325
+
1237
1326
  function getDrawLink() {
1238
1327
  return function drawLink(link) {
1239
1328
  if (!this.context ||
@@ -1349,23 +1438,62 @@ function getDrawLink() {
1349
1438
  let yStart = link.source.y;
1350
1439
  let xEnd = link.target.x;
1351
1440
  let yEnd = link.target.y;
1441
+ let xControl = 0;
1442
+ let yControl = 0;
1443
+ let xEndArrow = xEnd;
1444
+ let yEndArrow = yEnd;
1352
1445
  let linkDistance = 0;
1353
- if (this.linkSettings.prettyDraw ||
1354
- this.linkSettings.particleFlexSpeed ||
1355
- (this.linkSettings.arrow && arrowAlpha > 0)) {
1356
- const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1357
- const position = calculateLinkPositionByNode(link, isHasArrow ? arrowSize : 0);
1358
- if (position) {
1359
- xStart = position.x1;
1360
- xEnd = position.x2;
1361
- yStart = position.y1;
1362
- yEnd = position.y2;
1446
+ const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1447
+ if (!this.linkSettings.curve) {
1448
+ if (this.linkSettings.prettyDraw ||
1449
+ this.linkSettings.particleFlexSpeed ||
1450
+ (this.linkSettings.arrow && arrowAlpha > 0)) {
1451
+ const position = calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, isHasArrow ? arrowSize : 0);
1452
+ xStart = position.xStart;
1453
+ xEnd = position.xEnd;
1454
+ yStart = position.yStart;
1455
+ yEnd = position.yEnd;
1456
+ xEndArrow = position.xEndArrow;
1457
+ yEndArrow = position.yEndArrow;
1363
1458
  linkDistance = position.distance;
1364
1459
  }
1460
+ link._x1 = xStart;
1461
+ link._y1 = yStart;
1462
+ link._x2 = xEnd;
1463
+ link._y2 = yEnd;
1464
+ link._ax = xEndArrow;
1465
+ link._ay = yEndArrow;
1466
+ this.context.moveTo(xStart, yStart);
1467
+ this.context.lineTo(xEnd, yEnd);
1468
+ this.context.stroke();
1469
+ }
1470
+ else {
1471
+ const position = calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, link._groupIndex ?? 0, isHasArrow ? arrowSize : 0);
1472
+ xStart = position.xStart;
1473
+ yStart = position.yStart;
1474
+ xEnd = position.xEnd;
1475
+ yEnd = position.yEnd;
1476
+ xControl = position.xControl;
1477
+ yControl = position.yControl;
1478
+ xEndArrow = position.xEndArrow;
1479
+ yEndArrow = position.yEndArrow;
1480
+ link._x1 = position.xStart;
1481
+ link._y1 = position.yStart;
1482
+ link._x2 = position.xEnd;
1483
+ link._y2 = position.yEnd;
1484
+ link._cx = position.xControl;
1485
+ link._cy = position.yControl;
1486
+ link._ax = position.xEndArrow;
1487
+ link._ay = position.yEndArrow;
1488
+ this.context.beginPath();
1489
+ this.context.moveTo(position.xStart, position.yStart);
1490
+ this.context.quadraticCurveTo(position.xControl, position.yControl, position.xEnd, position.yEnd);
1491
+ this.context.setLineDash([]);
1492
+ // if (isDashed) {
1493
+ // this.context.setLineDash([10, 5]);
1494
+ // }
1495
+ this.context.stroke();
1365
1496
  }
1366
- this.context.moveTo(xStart, yStart);
1367
- this.context.lineTo(xEnd, yEnd);
1368
- this.context.stroke();
1369
1497
  /** Particle */
1370
1498
  if (this.linkSettings.particles &&
1371
1499
  ((this.highlightedNode &&
@@ -1433,15 +1561,28 @@ function getDrawLink() {
1433
1561
  }
1434
1562
  /** Arrow */
1435
1563
  if (this.linkSettings.arrow && arrowAlpha > 0) {
1564
+ let angle = 0;
1565
+ if (!this.linkSettings.curve) {
1566
+ angle = Math.atan2(yEndArrow - yStart, xEndArrow - xStart);
1567
+ }
1568
+ else {
1569
+ const tangentX = 2 * (xEndArrow - xControl);
1570
+ const tangentY = 2 * (yEndArrow - yControl);
1571
+ if (Math.abs(tangentX) < 0.001 && Math.abs(tangentY) < 0.001) {
1572
+ angle = Math.atan2(yEndArrow - yControl, xEndArrow - xControl);
1573
+ }
1574
+ else {
1575
+ angle = Math.atan2(tangentY, tangentX);
1576
+ }
1577
+ }
1436
1578
  this.context.beginPath();
1437
1579
  this.context.globalAlpha = arrowAlpha;
1438
1580
  this.context.strokeStyle = arrowBorderColor;
1439
1581
  this.context.lineWidth = arrowBorderWidth;
1440
1582
  this.context.fillStyle = arrowColor;
1441
- const angle = Math.atan2(yEnd - yStart, xEnd - xStart);
1442
- this.context.moveTo(xEnd, yEnd);
1443
- this.context.lineTo(xEnd - arrowSize * Math.cos(angle - Math.PI / 6), yEnd - arrowSize * Math.sin(angle - Math.PI / 6));
1444
- this.context.lineTo(xEnd - arrowSize * Math.cos(angle + Math.PI / 6), yEnd - arrowSize * Math.sin(angle + Math.PI / 6));
1583
+ this.context.moveTo(xEndArrow, yEndArrow);
1584
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle - Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle - Math.PI / 6));
1585
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle + Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle + Math.PI / 6));
1445
1586
  this.context.closePath();
1446
1587
  this.context.fill();
1447
1588
  if (arrowBorderWidth > 0) {
@@ -1965,6 +2106,7 @@ function initPointer() {
1965
2106
  areaTransform: this.areaTransform,
1966
2107
  mouseEvent: event,
1967
2108
  links: this.links,
2109
+ curve: this.linkSettings.curve,
1968
2110
  });
1969
2111
  if (currentLink?.highlight != undefined)
1970
2112
  highlightLink = currentLink?.highlight;
@@ -2026,6 +2168,7 @@ function initPointer() {
2026
2168
  areaTransform: this.areaTransform,
2027
2169
  mouseEvent: event,
2028
2170
  links: this.links,
2171
+ curve: this.linkSettings.curve,
2029
2172
  });
2030
2173
  }
2031
2174
  if (!currentNode)
@@ -2050,6 +2193,7 @@ function initPointer() {
2050
2193
  areaTransform: this.areaTransform,
2051
2194
  mouseEvent: event,
2052
2195
  links: this.links,
2196
+ curve: this.linkSettings.curve,
2053
2197
  });
2054
2198
  return void this.listeners.onWheelClick.call(this, event, undefined, currentLink);
2055
2199
  }
@@ -2071,6 +2215,7 @@ function initPointer() {
2071
2215
  areaTransform: this.areaTransform,
2072
2216
  mouseEvent: event,
2073
2217
  links: this.links,
2218
+ curve: this.linkSettings.curve,
2074
2219
  });
2075
2220
  return void this.listeners.onContextMenu.call(this, event, undefined, currentLink);
2076
2221
  }
@@ -2092,6 +2237,7 @@ function initPointer() {
2092
2237
  areaTransform: this.areaTransform,
2093
2238
  mouseEvent: event,
2094
2239
  links: this.links,
2240
+ curve: this.linkSettings.curve,
2095
2241
  });
2096
2242
  return void this.listeners.onDoubleClick.call(this, event, undefined, currentLink);
2097
2243
  }
@@ -2113,6 +2259,7 @@ function initPointer() {
2113
2259
  areaTransform: this.areaTransform,
2114
2260
  mouseEvent: event,
2115
2261
  links: this.links,
2262
+ curve: this.linkSettings.curve,
2116
2263
  });
2117
2264
  return void this.listeners.onClick.call(this, event, undefined, currentLink);
2118
2265
  }
@@ -2158,12 +2305,19 @@ function initResize() {
2158
2305
  return;
2159
2306
  }
2160
2307
  this.updateSize();
2308
+ requestAnimationFrame(() => {
2309
+ this.updateSize();
2310
+ });
2161
2311
  });
2162
2312
  document.addEventListener("scroll", this.updateRect.bind(this), {
2163
2313
  capture: true,
2164
2314
  passive: true,
2165
2315
  signal: abortController.signal,
2166
2316
  });
2317
+ document.addEventListener("visibilitychange", () => {
2318
+ if (!document.hidden)
2319
+ this.updateRect();
2320
+ }, { signal: abortController.signal });
2167
2321
  observer.observe(this.area);
2168
2322
  }
2169
2323
 
@@ -2284,14 +2438,44 @@ function initCollideForce(forceUpdate) {
2284
2438
 
2285
2439
  function updateLinkCache() {
2286
2440
  this.linkOptionsCache = {};
2441
+ const groupMap = {};
2442
+ const groupSelfMap = {};
2287
2443
  for (let i = 0; i < this.links.length; i++) {
2288
2444
  const link = this.links[i];
2289
2445
  const { sourceId, targetId } = extractLinkPointIds(link);
2290
2446
  const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2291
2447
  const id = `${targetId}${sourceId}`;
2292
2448
  this.linkOptionsCache[id] = linkOptions;
2449
+ if (this.linkSettings.curve) {
2450
+ if (sourceId === targetId) {
2451
+ groupSelfMap[id] ??= 0;
2452
+ link._groupIndex = ++groupSelfMap[id];
2453
+ link._self = true;
2454
+ continue;
2455
+ }
2456
+ const reverseId = `${sourceId}${targetId}`;
2457
+ groupMap[reverseId] ??= 0;
2458
+ groupMap[reverseId]++;
2459
+ groupMap[id] ??= 0;
2460
+ link._groupIndex = indexToPosition(++groupMap[id]);
2461
+ link._self = false;
2462
+ }
2463
+ }
2464
+ if (this.linkSettings.curve) {
2465
+ for (const link of this.links) {
2466
+ const { sourceId, targetId } = extractLinkPointIds(link);
2467
+ const id = `${targetId}${sourceId}`;
2468
+ link._groupSize = link._self ? (groupSelfMap[id] ?? 1) : (groupMap[id] ?? 1);
2469
+ }
2293
2470
  }
2294
2471
  }
2472
+ function indexToPosition(n) {
2473
+ if (n === 1)
2474
+ return 0;
2475
+ const index = n >> 1;
2476
+ return n & 1 ? -(index * 2) : index * 2;
2477
+ // return n & 1 ? -(n >> 1) : n >> 1;
2478
+ }
2295
2479
 
2296
2480
  function updateNodeCache(types) {
2297
2481
  this.nodeOptionsCache = {};