Skip to content

Commit 9c93a78

Browse files
committed
directed graph with viewGenerator
1 parent ca84242 commit 9c93a78

File tree

5 files changed

+205
-87
lines changed

5 files changed

+205
-87
lines changed

Diff for: sandbox/data/custom-node/custom-node.config.js

+42-42
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,47 @@ import React from "react";
22
import CustomNode from "./CustomNode";
33

44
export default {
5-
automaticRearrangeAfterDropNode: false,
6-
collapsible: false,
7-
height: 400,
8-
highlightDegree: 1,
9-
highlightOpacity: 0.2,
10-
linkHighlightBehavior: true,
11-
maxZoom: 8,
12-
minZoom: 0.1,
13-
nodeHighlightBehavior: true,
14-
panAndZoom: false,
15-
staticGraph: false,
16-
width: 800,
17-
node: {
18-
color: "#d3d3d3",
19-
fontColor: "black",
20-
fontSize: 12,
21-
fontWeight: "normal",
22-
highlightColor: "red",
23-
highlightFontSize: 12,
24-
highlightFontWeight: "bold",
25-
highlightStrokeColor: "SAME",
26-
highlightStrokeWidth: 1.5,
27-
labelProperty: "name",
28-
mouseCursor: "pointer",
29-
opacity: 1,
30-
renderLabel: false,
31-
size: {
32-
width: 700,
33-
height: 900,
34-
},
35-
strokeColor: "none",
36-
strokeWidth: 1.5,
37-
svg: "",
38-
symbolType: "circle",
39-
viewGenerator: node => <CustomNode person={node} />,
40-
},
41-
link: {
42-
color: "#d3d3d3",
43-
opacity: 1,
44-
semanticStrokeWidth: false,
45-
strokeWidth: 4,
46-
highlightColor: "blue",
5+
automaticRearrangeAfterDropNode: false,
6+
collapsible: false,
7+
height: 400,
8+
highlightDegree: 1,
9+
highlightOpacity: 0.2,
10+
linkHighlightBehavior: true,
11+
maxZoom: 8,
12+
minZoom: 0.1,
13+
nodeHighlightBehavior: true,
14+
panAndZoom: false,
15+
staticGraph: false,
16+
width: 800,
17+
node: {
18+
color: "#d3d3d3",
19+
fontColor: "black",
20+
fontSize: 12,
21+
fontWeight: "normal",
22+
highlightColor: "red",
23+
highlightFontSize: 12,
24+
highlightFontWeight: "bold",
25+
highlightStrokeColor: "SAME",
26+
highlightStrokeWidth: 1.5,
27+
labelProperty: "name",
28+
mouseCursor: "pointer",
29+
opacity: 1,
30+
renderLabel: false,
31+
size: {
32+
width: 700,
33+
height: 900,
4734
},
35+
strokeColor: "none",
36+
strokeWidth: 1.5,
37+
svg: "",
38+
symbolType: "rectangle",
39+
viewGenerator: node => <CustomNode person={node} />,
40+
},
41+
link: {
42+
color: "#d3d3d3",
43+
opacity: 1,
44+
semanticStrokeWidth: false,
45+
strokeWidth: 4,
46+
highlightColor: "blue",
47+
},
4848
};

Diff for: src/components/graph/graph.const.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export default {
1212
LINK_CLASS_NAME: "link",
1313
NODE_CLASS_NAME: "node",
1414
TTL_DOUBLE_CLICK_IN_MS: 300,
15+
SYMBOLS_WITH_OPTIMIZED_POSITIONING: new Set([CONST.SYMBOLS.CIRCLE, CONST.SYMBOLS.SQUARE, CONST.SYMBOLS.RECTANGLE]),
1516
...CONST,
1617
};

Diff for: src/components/graph/graph.helper.js

+156-36
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,143 @@ function normalize(vector) {
469469
return norm === 0 ? vector : { x: vector.x / norm, y: vector.y / norm };
470470
}
471471

472-
const SYMBOLS_WITH_OPTIMIZED_POSITIONING = new Set([CONST.SYMBOLS.CIRCLE]);
472+
/**
473+
* Calculate the length of a vector, from the center of a rectangle,
474+
* to one of it's edges.
475+
*
476+
* @param {Object.<string, number>} RectangleCoords, The coords of the left-top vertex, and the right-bottom vertex.
477+
* @param {Object.<string, number>} VectorOriginCoords The center of the rectangle coords.
478+
* @param {Object.<string, number>} directionVector a 2D vector with x and y components
479+
*/
480+
function calcRectangleVectorLengthFromCoords({ x1, y1, x2, y2 }, { px, py }, directionVector) {
481+
const angle = Math.atan(directionVector.y / directionVector.x);
482+
483+
const vx = Math.cos(angle);
484+
const vy = Math.sin(angle);
485+
486+
if (vx === 0) {
487+
return x2 - x1;
488+
} else if (vy === 0) {
489+
return y2 - y1;
490+
}
491+
492+
const leftEdge = (x1 - px) / vx;
493+
const rightEdge = (x2 - px) / vx;
494+
495+
const topEdge = (y1 - py) / vy;
496+
const bottomEdge = (y2 - py) / vy;
497+
498+
return Math.min(...[leftEdge, rightEdge, topEdge, bottomEdge].filter(edge => edge > 0));
499+
}
500+
501+
/**
502+
* Calculate a the vector length from the center of a circle to it's perimeter.
503+
*
504+
* @param {number} nodeSize The size of the circle, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
505+
* @param {boolean} isCustomNode is viewGenerator specified.
506+
* @returns {number}
507+
*/
508+
function calcCircleVectorLength(nodeSize, isCustomNode) {
509+
let radiusLength;
510+
if (isCustomNode) {
511+
// nodeSize equals the Diameter in the case of custome-node.
512+
radiusLength = nodeSize / 10 / 2;
513+
} else {
514+
// because this is a circle and A = pi * r^2
515+
// we multiply by 0.95, because if we don't the link is not melting properly
516+
radiusLength = Math.sqrt(nodeSize / Math.PI);
517+
}
518+
return radiusLength;
519+
}
520+
521+
/**
522+
* Calculate a the vector length from the center of a square to it's perimeter.
523+
*
524+
* @param {number} nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
525+
* @param {Object.<string, number>} nodeCoords The coords of a the square node.
526+
* @param {Object.<string, number>} directionVector a 2D vector with x and y components
527+
* @param {boolean} isCustomNode is viewGenerator specified.
528+
* @returns {number}
529+
*/
530+
function calcSquareVectorLength(nodeSize, { x, y }, directionVector, isCustomNode) {
531+
let edgeSize;
532+
if (isCustomNode) {
533+
// nodeSize equals the edgeSize in the case of custome-node.
534+
edgeSize = nodeSize / 10;
535+
} else {
536+
// All the edges of a square are equal, inorder to calc it's size we multplie two edges.
537+
edgeSize = Math.sqrt(nodeSize);
538+
}
539+
540+
// The x and y coords in this library, represent the top center of the component.
541+
const leftSquareX = x - edgeSize / 2;
542+
const topSquareY = y - edgeSize / 2;
543+
544+
return calcRectangleVectorLengthFromCoords(
545+
{ x1: leftSquareX, y1: topSquareY, x2: leftSquareX + edgeSize, y2: topSquareY + edgeSize },
546+
{ px: x, py: y },
547+
directionVector
548+
);
549+
}
550+
551+
/**
552+
* Calculate a the vector length from the center of a rectangle to it's perimeter.
553+
*
554+
* @param {number} nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
555+
* @param {Object.<string, number>} nodeCoords The coords of a the square node.
556+
* @param {Object.<string, number>} directionVector a 2D vector with x and y components.
557+
* @returns {number}
558+
*/
559+
function calcRectangleVectorLength(nodeSize, { x, y }, directionVector) {
560+
const horizEdgeSize = nodeSize.width / 10;
561+
const vertEdgeSize = nodeSize.height / 10;
562+
563+
// The x and y coords in this library, represent the top center of the component.
564+
const leftSquareX = x - horizEdgeSize / 2;
565+
const topSquareY = y - vertEdgeSize / 2;
566+
567+
// The size between the square center, to the appropriate square edges
568+
return calcRectangleVectorLengthFromCoords(
569+
{ x1: leftSquareX, y1: topSquareY, x2: leftSquareX + horizEdgeSize, y2: topSquareY + vertEdgeSize },
570+
{ px: x, py: y },
571+
directionVector
572+
);
573+
}
574+
575+
/**
576+
* Calculate a the vector length of symbol that included in symbols with optimized positioning.
577+
*
578+
* @param {string} symbolType the string that specifies the symbol type (should be one of {@link #node-symbol-type|node.symbolType})
579+
* @param {(number | Object.<string, number>)} nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
580+
* @param {Object.<string, number>} nodeCoords The coords of a the square node.
581+
* @param {Object.<string, number>} directionVector a 2D vector with x and y components.
582+
* @param {boolean} isCustomNode is viewGenerator specified.
583+
* @returns {number}
584+
*/
585+
function calcVectorLength(symbolType, nodeSize, { x, y }, directionVector, isCustomNode) {
586+
switch (symbolType) {
587+
case CONST.SYMBOLS.CIRCLE:
588+
if (typeof nodeSize === "number") {
589+
return calcCircleVectorLength(nodeSize, isCustomNode);
590+
}
591+
console.warn("When specifiying 'circle' as node symbol, the size of the node must be a number.");
592+
593+
case CONST.SYMBOLS.SQUARE:
594+
if (typeof nodeSize === "number") {
595+
return calcSquareVectorLength(nodeSize, { x, y }, directionVector, isCustomNode);
596+
}
597+
console.warn("When specifiying 'square' as node symbol, the size of the node must be a number.");
598+
599+
case CONST.SYMBOLS.RECTANGLE:
600+
if (typeof nodeSize === "object" && nodeSize?.width && nodeSize?.height) {
601+
return calcRectangleVectorLength(nodeSize, { x, y }, directionVector);
602+
}
603+
console.warn("When specifiying 'rectangle' as node symbol, width and height must be specified in the node size.");
604+
605+
default:
606+
return 1;
607+
}
608+
}
473609

474610
/**
475611
* Computes new node coordinates to make arrowheads point at nodes.
@@ -498,16 +634,12 @@ function getNormalizedNodeCoordinates(
498634
return { sourceCoords, targetCoords };
499635
}
500636

501-
if (config.node?.viewGenerator || sourceNode?.viewGenerator || targetNode?.viewGenerator) {
502-
return { sourceCoords, targetCoords };
503-
}
504-
505637
const sourceSymbolType = sourceNode.symbolType || config.node?.symbolType;
506638
const targetSymbolType = targetNode.symbolType || config.node?.symbolType;
507639

508640
if (
509-
!SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(sourceSymbolType) &&
510-
!SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(targetSymbolType)
641+
!CONST.SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(sourceSymbolType) &&
642+
!CONST.SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(targetSymbolType)
511643
) {
512644
// if symbols don't have optimized positioning implementations fallback to input coords
513645
return { sourceCoords, targetCoords };
@@ -517,38 +649,26 @@ function getNormalizedNodeCoordinates(
517649
let { x: x2, y: y2 } = targetCoords;
518650
const directionVector = normalize({ x: x2 - x1, y: y2 - y1 });
519651

520-
switch (sourceSymbolType) {
521-
case CONST.SYMBOLS.CIRCLE: {
522-
let sourceNodeSize = sourceNode?.size || config.node.size;
652+
const isSourceCustomNode = sourceNode.viewGenerator || config.node.viewGenerator;
653+
const sourceNodeSize = sourceNode?.size || config.node.size;
523654

524-
// because this is a circle and A = pi * r^2
525-
// we multiply by 0.95, because if we don't the link is not melting properly
526-
sourceNodeSize = Math.sqrt(sourceNodeSize / Math.PI) * 0.95;
655+
const sourceVectorLength =
656+
calcVectorLength(sourceSymbolType, sourceNodeSize, { x: x1, y: y1 }, directionVector, isSourceCustomNode) * 0.95;
527657

528-
// points from the sourceCoords, we move them not to begin in the circle but outside
529-
x1 += sourceNodeSize * directionVector.x;
530-
y1 += sourceNodeSize * directionVector.y;
531-
break;
532-
}
533-
}
658+
x1 += sourceVectorLength * directionVector.x;
659+
y1 += sourceVectorLength * directionVector.y;
534660

535-
switch (targetSymbolType) {
536-
case CONST.SYMBOLS.CIRCLE: {
537-
// it's fine `markerWidth` or `markerHeight` we just want to fallback to a number
538-
// to avoid NaN on `Math.min(undefined, undefined) > NaN
539-
let strokeSize = strokeWidth * Math.min(config.link?.markerWidth || 0, config.link?.markerHeight || 0);
540-
let targetNodeSize = targetNode?.size || config.node.size;
541-
542-
// because this is a circle and A = pi * r^2
543-
// we multiply by 0.95, because if we don't the link is not melting properly
544-
targetNodeSize = Math.sqrt(targetNodeSize / Math.PI) * 0.95;
545-
546-
// points from the targetCoords, we move the by the size of the radius of the circle + the size of the arrow
547-
x2 -= (targetNodeSize + (config.directed ? strokeSize : 0)) * directionVector.x;
548-
y2 -= (targetNodeSize + (config.directed ? strokeSize : 0)) * directionVector.y;
549-
break;
550-
}
551-
}
661+
// it's fine `markerWidth` or `markerHeight` we just want to fallback to a number
662+
// to avoid NaN on `Math.min(undefined, undefined) > NaN
663+
const strokeSize = strokeWidth * Math.min(config.link?.markerWidth || 5, config.link?.markerHeight || 5);
664+
const isTargetCustomNode = targetNode.viewGenerator || config.node.viewGenerator;
665+
const targetNodeSize = targetNode?.size || config.node.size;
666+
667+
const targetVectorLength =
668+
calcVectorLength(targetSymbolType, targetNodeSize, { x: x2, y: y2 }, directionVector, isTargetCustomNode) * 0.95;
669+
670+
x2 -= (targetVectorLength + (config.directed ? strokeSize : 0)) * directionVector.x;
671+
y2 -= (targetVectorLength + (config.directed ? strokeSize : 0)) * directionVector.y;
552672

553673
return { sourceCoords: { x: x1, y: y1 }, targetCoords: { x: x2, y: y2 } };
554674
}

Diff for: src/components/marker/marker.helper.js

+5-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
MARKER_MEDIUM_OFFSET,
1212
MARKER_LARGE_OFFSET,
1313
} from "./marker.const";
14-
import CONST from "../graph/graph.const";
14+
import GRAPH_CONST from "../graph/graph.const";
1515

1616
/**
1717
* This function is a key template builder to access MARKERS structure.
@@ -114,14 +114,10 @@ function getMarkerSize(config) {
114114
let medium = small + (MARKER_MEDIUM_OFFSET * config.maxZoom) / 3;
115115
let large = small + (MARKER_LARGE_OFFSET * config.maxZoom) / 3;
116116

117-
if (config.node && !config.node.viewGenerator) {
118-
switch (config.node.symbolType) {
119-
case CONST.SYMBOLS.CIRCLE:
120-
small = 0;
121-
medium = 0;
122-
large = 0;
123-
break;
124-
}
117+
if (GRAPH_CONST.SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(config.node?.symbolType)) {
118+
small = 0;
119+
medium = 0;
120+
large = 0;
125121
}
126122

127123
return { small, medium, large };

Diff for: src/const.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export default {
1212
STAR: "star",
1313
TRIANGLE: "triangle",
1414
WYE: "wye",
15+
RECTANGLE: "rectangle",
1516
},
1617
};

0 commit comments

Comments
 (0)