Skip to content

Commit 23f4c1d

Browse files
committed
directed graph with viewGenerator
1 parent ca84242 commit 23f4c1d

File tree

5 files changed

+206
-87
lines changed

5 files changed

+206
-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

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

474611
/**
475612
* Computes new node coordinates to make arrowheads point at nodes.
@@ -498,16 +635,12 @@ function getNormalizedNodeCoordinates(
498635
return { sourceCoords, targetCoords };
499636
}
500637

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

508641
if (
509-
!SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(sourceSymbolType) &&
510-
!SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(targetSymbolType)
642+
!CONST.SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(sourceSymbolType) &&
643+
!CONST.SYMBOLS_WITH_OPTIMIZED_POSITIONING.has(targetSymbolType)
511644
) {
512645
// if symbols don't have optimized positioning implementations fallback to input coords
513646
return { sourceCoords, targetCoords };
@@ -517,38 +650,26 @@ function getNormalizedNodeCoordinates(
517650
let { x: x2, y: y2 } = targetCoords;
518651
const directionVector = normalize({ x: x2 - x1, y: y2 - y1 });
519652

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

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;
656+
const sourceVectorLength =
657+
calcVectorLength(sourceSymbolType, sourceNodeSize, { x: x1, y: y1 }, directionVector, isSourceCustomNode) * 0.95;
527658

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-
}
659+
x1 += sourceVectorLength * directionVector.x;
660+
y1 += sourceVectorLength * directionVector.y;
534661

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-
}
662+
// it's fine `markerWidth` or `markerHeight` we just want to fallback to a number
663+
// to avoid NaN on `Math.min(undefined, undefined) > NaN
664+
const strokeSize = strokeWidth * Math.min(config.link?.markerWidth || 5, config.link?.markerHeight || 5);
665+
const isTargetCustomNode = targetNode.viewGenerator || config.node.viewGenerator;
666+
const targetNodeSize = targetNode?.size || config.node.size;
667+
668+
const targetVectorLength =
669+
calcVectorLength(targetSymbolType, targetNodeSize, { x: x2, y: y2 }, directionVector, isTargetCustomNode) * 0.95;
670+
671+
x2 -= (targetVectorLength + (config.directed ? strokeSize : 0)) * directionVector.x;
672+
y2 -= (targetVectorLength + (config.directed ? strokeSize : 0)) * directionVector.y;
552673

553674
return { sourceCoords: { x: x1, y: y1 }, targetCoords: { x: x2, y: y2 } };
554675
}

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)