react-occult
Version:
Layered Information Visualization based on React and D3
1,326 lines (1,179 loc) • 39.9 kB
JavaScript
import React from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
import {
/*forceCenter,*/ forceSimulation,
forceX,
forceY,
/*forceCollide,*/ forceLink,
forceManyBody
} from 'd3-force';
import { scaleLinear } from 'd3-scale';
import { min, max } from 'd3-array';
import {
sankeyLeft,
sankeyRight,
sankeyCenter,
sankeyJustify,
sankeyCircular
} from 'd3-sankey-circular';
import { chord, ribbon } from 'd3-chord';
import { arc } from 'd3-shape';
import {
tree,
hierarchy,
pack,
cluster,
treemap,
partition
} from 'd3-hierarchy';
import pathBounds from 'svg-path-bounding-box';
import AnnotationLabel from 'react-annotation/lib/Types/AnnotationLabel';
import topologicalSort from './layout/topologicalSort';
import areaLink from './layout/areaLink';
import ribbonLink from './layout/ribbonLink';
import circularAreaLink from './layout/circularAreaLink';
import radialLabelGenerator from './layout/radialLabelGenerator';
import softStack from './layout/softStack';
import circleNodeGenerator from '../../plots/Network/CirclePack/circleNodeGenerator';
import pointOnArcAtAngle from '../../utils/pointOnArcAtAngle';
import stringToFn from '../../utils/stringToFn';
import { getAdjustedPositionSize, toMarginGraphic } from '../utils';
import { nodesEdgesFromHierarchy } from './nodesEdgesFromHierarchy';
import toPipeline from '../../pipeline/network/toPipeline';
const emptyArray = [];
const genericFunction = value => () => value;
const baseNodeProps = {
id: undefined,
degree: 0,
inDegree: 0,
outDegree: 0,
x: 0,
y: 0,
x1: 0,
x0: 0,
y1: 0,
y0: 0,
height: 0,
width: 0,
radius: 0,
r: 0,
direction: undefined,
textHeight: 0,
textWidth: 0,
fontSize: 0,
scale: 1,
nodeSize: 0,
component: -99,
shapeNode: false
};
const baseGraphSettings = {
nodeHash: new Map(),
edgeHash: new Map(),
nodes: [],
edges: [],
hierarchicalNetwork: false,
type: 'force'
};
const basicMiddle = d => ({
edge: d,
x: (d.source.x + d.target.x) / 2,
y: (d.source.y + d.target.y) / 2
});
const edgePointHash = {
sankey: d => ({
edge: d,
x: (d.source.x1 + d.target.x0) / 2,
y: d.circularPathData
? d.circularPathData.verticalFullExtent
: ((d.y0 + d.y1) / 2 + (d.y0 + d.y1) / 2) / 2
}),
force: basicMiddle,
tree: basicMiddle,
cluster: basicMiddle
};
const hierarchicalTypeHash = {
dendrogram: tree,
tree,
circlepack: pack,
cluster,
treemap,
partition
};
const hierarchicalProjectable = {
partition: true,
cluster: true,
tree: true,
dendrogram: true
};
const radialProjectable = {
partition: true,
cluster: true,
tree: true,
dendrogram: true
};
function breadthFirstCompontents(baseNodes, hash) {
const componentHash = {
'0': { componentNodes: [], componentEdges: [] }
};
const components = [componentHash['0']];
let componentID = 0;
traverseNodesBF(baseNodes, true);
function traverseNodesBF(nodes, top) {
nodes.forEach(node => {
const hashNode = hash.get(node);
if (!hashNode) {
componentHash['0'].componentNodes.push(node);
} else if (hashNode.component === -99) {
if (top === true) {
componentID++;
componentHash[componentID] = {
componentNodes: [],
componentEdges: []
};
components.push(componentHash[componentID]);
}
hashNode.component = componentID;
componentHash[componentID].componentNodes.push(node);
componentHash[componentID].componentEdges.push(...hashNode.edges);
const traversibleNodes = [...hashNode.connectedNodes];
traverseNodesBF(traversibleNodes, hash);
}
});
}
return components.sort(
(a, b) => b.componentNodes.length - a.componentNodes.length
);
}
const sankeyOrientHash = {
left: sankeyLeft,
right: sankeyRight,
center: sankeyCenter,
justify: sankeyJustify
};
const matrixify = ({ edgeHash, nodes, edgeWidthAccessor, nodeIDAccessor }) => {
const matrix = [];
nodes.forEach(nodeSource => {
const nodeSourceID = nodeIDAccessor(nodeSource);
const sourceRow = [];
matrix.push(sourceRow);
nodes.forEach(nodeTarget => {
const nodeTargetID = nodeIDAccessor(nodeTarget);
const theEdge = edgeHash.get(`${nodeSourceID}|${nodeTargetID}`);
if (theEdge) {
sourceRow.push(edgeWidthAccessor(theEdge));
} else {
sourceRow.push(0);
}
});
});
return matrix;
};
const computeNetworkFrameData = props => {
const [frameData, updateFrameData] = useState({
nodeData: [],
edgeData: [],
adjustedPosition: [],
adjustedSize: [],
backgroundGraphics: null,
foregroundGraphics: null,
projectedNodes: [],
projectedEdges: [],
renderNumber: 0,
nodeLabelAnnotations: [],
graphSettings: {
type: 'empty-start',
nodes: [],
edges: [],
nodeHash: new Map(),
edgeHash: new Map(),
hierarchicalNetwork: false
},
edgeWidthAccessor: stringToFn('weight'),
margin: { top: 0, left: 0, right: 0, bottom: 0 },
nodeIDAccessor: stringToFn('id'),
nodeSizeAccessor: genericFunction(5),
overlay: [],
screenCoordinates: [],
sourceAccessor: stringToFn('source'),
targetAccessor: stringToFn('target'),
svgPipeline: [],
canvasPipeline: [],
marginGraphic: null
});
const computeFrame = props => {
const {
margin: baseMargin,
width,
height,
matte,
hoverAnnotation,
name,
plotChildren
} = props;
const singlePlot = plotChildren[0];
const {
graph,
nodes = Array.isArray(graph) || typeof graph === 'function'
? emptyArray
: (graph && graph.nodes) || emptyArray,
edges = typeof graph === 'function'
? emptyArray
: Array.isArray(graph)
? graph
: (graph && graph.edges) || emptyArray,
nodeStyle: baseNodeStyle,
nodeClass,
edgeStyle,
edgeClass,
nodeRenderMode,
edgeRenderMode,
nodeLabels,
customNodeIcon: baseCustomNodeIcon,
customEdgeIcon: baseCustomEdgeIcon,
filterRenderedNodes,
nodeUseCanvas,
edgeUseCanvas,
edgeRenderKey,
nodeRenderKey,
nodeIDAccessor: baseNodeIDAccessor,
sourceAccessor: baseSourceAccessor,
targetAccessor: baseTargetAccessor,
nodeSizeAccessor: baseNodeSizeAccessor,
edgeWidthAccessor: baseEdgeWidthAccessor,
nodeSize,
hierarchyChildren,
hierarchySum,
nodePadding,
angleRange
} = singlePlot.props;
let { edgeType, projection, direction } = singlePlot.props;
const nodeStyle = stringToFn(baseNodeStyle, () => ({}), true);
const { nodeGenerator, edgeGenerator } = singlePlot.type;
const size = [width, height];
const center = [size[0] / 2, size[1] / 2];
const margin =
typeof baseMargin !== 'object'
? {
top: baseMargin,
bottom: baseMargin,
left: baseMargin,
right: baseMargin
}
: Object.assign({ top: 0, bottom: 0, left: 0, right: 0 }, baseMargin);
const marginGraphic = toMarginGraphic({ matte, size, margin, name });
const { adjustedPosition, adjustedSize } = getAdjustedPositionSize({
size: [width, height],
margin
});
const nodeHierarchicalIDFill = {};
const networkSettings = {
type: singlePlot.type.name.toLowerCase(),
graphSettings: baseGraphSettings
};
if (projection === 'vertical' && networkSettings.type === 'sankey') {
direction = 'down';
}
networkSettings.graphSettings.nodes = nodes;
networkSettings.graphSettings.edges = edges;
let { edgeHash, nodeHash } = networkSettings.graphSettings;
const createPointLayer =
networkSettings.type === 'treemap' ||
networkSettings.type === 'partition' ||
networkSettings.type === 'sankey';
const nodeIDAccessor = stringToFn(baseNodeIDAccessor, d => d.id);
const sourceAccessor = stringToFn(baseSourceAccessor, d => d.source);
const targetAccessor = stringToFn(baseTargetAccessor, d => d.target);
const nodeSizeAccessor =
typeof baseNodeSizeAccessor === 'number'
? genericFunction(baseNodeSizeAccessor)
: stringToFn(baseNodeSizeAccessor, d => d.r || 5);
const edgeWidthAccessor = stringToFn(
baseEdgeWidthAccessor,
d => d.weight || 1
);
let projectedNodes = [];
let projectedEdges = [];
const isHierarchical = hierarchicalTypeHash[networkSettings.type];
const changedData =
!frameData.projectedNodes ||
!frameData.projectedEdges ||
frameData.graphSettings.nodes !== nodes ||
frameData.graphSettings.edges !== edges ||
isHierarchical;
if (networkSettings.type === 'dagre') {
const dagreGraph = graph;
const dagreNodeHash = {};
projectedNodes = dagreGraph.nodes().map(n => {
const baseNode = dagreGraph.node(n);
dagreNodeHash[n] = {
...baseNode,
x0: baseNode.x - baseNode.width / 2,
x1: baseNode.x + baseNode.width / 2,
y0: baseNode.y - baseNode.height / 2,
y1: baseNode.y + baseNode.height / 2,
id: n,
shapeNode: true,
sourceLinks: [],
targetLinks: []
};
return dagreNodeHash[n];
});
projectedEdges = dagreGraph.edges().map(e => {
const dagreEdge = dagreGraph.edge(e);
const baseEdge = {
...dagreEdge,
points: dagreEdge.points.map(d => ({ ...d }))
};
baseEdge.source = projectedNodes.find(p => p.id === e.v);
baseEdge.target = projectedNodes.find(p => p.id === e.w);
baseEdge.points.unshift({ x: baseEdge.source.x, y: baseEdge.source.y });
baseEdge.points.push({ x: baseEdge.target.x, y: baseEdge.target.y });
dagreNodeHash[e.v].targetLinks.push(baseEdge);
dagreNodeHash[e.w].sourceLinks.push(baseEdge);
return baseEdge;
});
} else if (changedData) {
edgeHash = new Map();
nodeHash = new Map();
networkSettings.graphSettings.edgeHash = edgeHash;
networkSettings.graphSettings.nodeHash = nodeHash;
projectedNodes = [];
projectedEdges = [];
nodes.forEach(node => {
const projectedNode = { ...node };
const id = nodeIDAccessor(projectedNode);
nodeHash.set(id, projectedNode);
nodeHash.set(node, projectedNode);
projectedNodes.push(projectedNode);
projectedNode.id = id;
projectedNode.inDegree = 0;
projectedNode.outDegree = 0;
projectedNode.degree = 0;
});
let operationalEdges = edges;
let baseEdges = edges;
if (isHierarchical && Array.isArray(edges)) {
const createdHierarchicalData = softStack(
edges,
projectedNodes,
sourceAccessor,
targetAccessor,
nodeIDAccessor
);
if (createdHierarchicalData.isHierarchical) {
baseEdges = createdHierarchicalData.hierarchy;
projectedNodes = [];
} else {
console.error(
"You've sent an edge list that is not strictly hierarchical (there are nodes with multiple parents) defaulting to force-directed network layout"
);
networkSettings.type = 'force';
}
}
if (!Array.isArray(baseEdges)) {
networkSettings.hierarchicalNetwork = true;
const rootNode = hierarchy(baseEdges, hierarchyChildren);
rootNode.sum(hierarchySum || (d => d.value));
if (isHierarchical) {
const layout = networkSettings.layout || isHierarchical;
const hierarchicalLayout = layout();
const networkSettingKeys = Object.keys(networkSettings);
if (
(networkSettings.type === 'dendrogram' ||
networkSettings.type === 'tree' ||
networkSettings.type === 'cluster') &&
hierarchicalLayout.separation
) {
hierarchicalLayout.separation(
(a, b) =>
(nodeSizeAccessor({ ...a, ...a.data }) || 1) +
(nodePadding || 0) +
(nodeSizeAccessor({ ...b, ...b.data }) || 1)
);
}
networkSettingKeys.forEach(key => {
if (hierarchicalLayout[key]) {
hierarchicalLayout[key](networkSettings[key]);
}
});
const layoutSize =
projection === 'horizontal' && isHierarchical
? [adjustedSize[1], adjustedSize[0]]
: adjustedSize;
if (!nodeSize && hierarchicalLayout.size) {
hierarchicalLayout.size(layoutSize);
}
hierarchicalLayout(rootNode);
}
operationalEdges = nodesEdgesFromHierarchy(rootNode, nodeIDAccessor)
.edges;
}
baseNodeProps.shapeNode = createPointLayer;
if (Array.isArray(operationalEdges)) {
operationalEdges.forEach(edge => {
const source = sourceAccessor(edge);
const target = targetAccessor(edge);
const sourceTarget = [source, target];
sourceTarget.forEach(nodeDirection => {
if (!nodeHash.get(nodeDirection)) {
const nodeObject =
typeof nodeDirection === 'object'
? {
...baseNodeProps,
...nodeDirection
}
: {
...baseNodeProps,
id: nodeDirection,
createdByFrame: true
};
const nodeIDValue = nodeObject.id || nodeIDAccessor(nodeObject);
nodeHierarchicalIDFill[nodeIDValue]
? (nodeHierarchicalIDFill[nodeIDValue] += 1)
: (nodeHierarchicalIDFill[nodeIDValue] = 1);
if (!nodeObject.id) {
const nodeSuffix =
nodeHierarchicalIDFill[nodeIDValue] === 1
? ''
: `-${nodeHierarchicalIDFill[nodeIDValue]}`;
nodeObject.id = `${nodeIDValue}${nodeSuffix}`;
}
nodeHash.set(nodeDirection, nodeObject);
projectedNodes.push(nodeObject);
}
});
const edgeWeight = edge.weight || 1;
const sourceNode = nodeHash.get(source);
const targetNode = nodeHash.get(target);
targetNode.inDegree += edgeWeight;
sourceNode.outDegree += edgeWeight;
targetNode.degree += edgeWeight;
sourceNode.degree += edgeWeight;
const edgeKey = `${nodeIDAccessor(sourceNode) ||
source}|${nodeIDAccessor(targetNode) || target}`;
const newEdge = Object.assign({}, edge, {
source: nodeHash.get(source),
target: nodeHash.get(target)
});
edgeHash.set(edgeKey, newEdge);
projectedEdges.push(newEdge);
});
}
} else {
edgeHash = new Map();
networkSettings.graphSettings.edgeHash = edgeHash;
projectedEdges.forEach(edge => {
const edgeSource =
typeof edge.source === 'string'
? edge.source
: nodeIDAccessor(edge.source);
const edgeTarget =
typeof edge.target === 'string'
? edge.target
: nodeIDAccessor(edge.target);
const edgeKey = `${edgeSource}|${edgeTarget}`;
edgeHash.set(edgeKey, edge);
});
}
const customNodeIcon = baseCustomNodeIcon
? baseCustomNodeIcon
: nodeGenerator
? nodeGenerator({
center,
angleRange,
projection,
direction,
size: adjustedSize,
nodes: projectedNodes
})
: circleNodeGenerator();
const customEdgeIcon = baseCustomEdgeIcon
? baseCustomEdgeIcon
: edgeGenerator
? edgeGenerator({
direction,
size: adjustedSize,
nodes: projectedNodes,
graph
})
: undefined;
if (
(networkSettings.type === 'sankey' ||
networkSettings.type === 'flowchart') &&
topologicalSort(projectedNodes, projectedEdges) === null
) {
networkSettings.customSankey = sankeyCircular;
}
networkSettings.width = size[0];
networkSettings.height = size[1];
let networkSettingsChanged = false;
['type', 'height', 'width'].forEach(key => {
if (
key !== 'edgeType' &&
key !== 'graphSettings' &&
networkSettings[key] !== frameData.graphSettings[key]
) {
networkSettingsChanged = true;
}
});
//Support bubble chart with circle pack and with force
if (networkSettings.type === 'sankey') {
edgeType = d =>
d.circular
? circularAreaLink(d)
: edgeType === 'angled'
? ribbonLink(d)
: areaLink(d);
} else if (isHierarchical) {
projectedNodes.forEach(node => {
if (createPointLayer) {
node.x = (node.x0 + node.x1) / 2;
node.y = (node.y0 + node.y1) / 2;
}
if (
typeof networkSettings.type === 'string' &&
hierarchicalProjectable[networkSettings.type] &&
projection === 'horizontal'
) {
const ox = node.x;
node.x = node.y;
node.y = ox;
if (createPointLayer) {
const ox0 = node.x0;
const ox1 = node.x1;
node.x0 = node.y0;
node.x1 = node.y1;
node.y0 = ox0;
node.y1 = ox1;
}
} else if (
typeof networkSettings.type === 'string' &&
radialProjectable[networkSettings.type] &&
projection === 'radial'
) {
const radialPoint =
node.depth === 0
? [adjustedSize[0] / 2, adjustedSize[1] / 2]
: pointOnArcAtAngle(
[adjustedSize[0] / 2, adjustedSize[1] / 2],
node.x / adjustedSize[0],
node.y / 2
);
node.x = radialPoint[0];
node.y = radialPoint[1];
} else {
node.x = node.x;
node.y = node.y;
if (createPointLayer) {
node.x0 = node.x0;
node.x1 = node.x1;
node.y0 = node.y0;
node.y1 = node.y1;
}
}
});
}
if (
networkSettings.type !== 'static' &&
(changedData || networkSettingsChanged)
) {
let components = [
{
componentNodes: projectedNodes,
componentEdges: projectedEdges
}
];
if (networkSettings.type === 'chord') {
const radius = adjustedSize[1] / 2;
const {
groupWidth = 20,
padAngle = 0.01,
sortGroups
} = singlePlot.props;
const arcGenerator = arc()
.innerRadius(radius - groupWidth)
.outerRadius(radius);
const ribbonGenerator = ribbon().radius(radius - groupWidth);
const matrixifiedNetwork = matrixify({
edgeHash: edgeHash,
nodes: projectedNodes,
edgeWidthAccessor,
nodeIDAccessor
});
const chordLayout = chord().padAngle(padAngle);
if (sortGroups) {
chordLayout.sortGroups(sortGroups);
}
const chords = chordLayout(matrixifiedNetwork);
const groups = chords.groups;
groups.forEach(group => {
const groupCentroid = arcGenerator.centroid(group);
const groupD = arcGenerator(group);
const groupNode = projectedNodes[group.index];
groupNode.d = groupD;
groupNode.index = group.index;
groupNode.x = groupCentroid[0] + adjustedSize[0] / 2;
groupNode.y = groupCentroid[1] + adjustedSize[1] / 2;
});
chords.forEach(generatedChord => {
const chordD = ribbonGenerator(generatedChord);
//this is incorrect should use edgeHash
const nodeSourceID = nodeIDAccessor(
projectedNodes[generatedChord.source.index]
);
const nodeTargetID = nodeIDAccessor(
projectedNodes[generatedChord.target.index]
);
const chordEdge = edgeHash.get(`${nodeSourceID}|${nodeTargetID}`);
chordEdge.d = chordD;
const chordBounds = pathBounds(chordD);
chordEdge.x =
adjustedSize[0] / 2 + (chordBounds.x1 + chordBounds.x2) / 2;
chordEdge.y =
adjustedSize[1] / 2 + (chordBounds.y1 + chordBounds.y2) / 2;
});
} else if (
networkSettings.type === 'sankey' ||
networkSettings.type === 'flowchart'
) {
const {
orient = 'center',
iterations = 100,
nodePadding,
nodePaddingRatio = nodePadding ? undefined : 0.5,
nodeWidth = networkSettings.type === 'flowchart' ? 2 : 24,
customSankey,
direction = 'right'
} = singlePlot.props;
const sankeyOrient = sankeyOrientHash[orient];
const actualSankey = customSankey || sankeyCircular;
let frameExtent = [[0, 0], adjustedSize];
if (direction === 'up' || direction === 'down') {
frameExtent = [
[0, 0],
[adjustedSize[1], adjustedSize[0]]
];
}
const frameSankey = actualSankey()
.extent(frameExtent)
.links(projectedEdges)
.nodes(projectedNodes)
.nodeAlign(sankeyOrient)
.nodeId(nodeIDAccessor)
.nodeWidth(nodeWidth)
.iterations(iterations);
if (frameSankey.nodePaddingRatio && nodePaddingRatio) {
frameSankey.nodePaddingRatio(nodePaddingRatio);
} else if (nodePadding) {
frameSankey.nodePadding(nodePadding);
}
frameSankey();
projectedNodes.forEach(d => {
d.height = d.y1 - d.y0;
d.width = d.x1 - d.x0;
d.x = d.x0 + d.width / 2;
d.y = d.y0 + d.height / 2;
d.radius = d.height / 2;
d.direction = direction;
});
projectedEdges.forEach(d => {
d.sankeyWidth = d.width;
d.direction = direction;
d.width = undefined;
});
} else if (networkSettings.type === 'force') {
const {
iterations = 500,
edgeStrength = 0.1,
distanceMax = Infinity,
edgeDistance
} = singlePlot.props;
const linkForce = forceLink().strength(d =>
Math.min(2.5, d.weight ? d.weight * edgeStrength : edgeStrength)
);
if (edgeDistance) {
linkForce.distance(edgeDistance);
}
const simulation =
networkSettings.simulation ||
forceSimulation().force(
'charge',
forceManyBody()
.distanceMax(distanceMax)
.strength(
networkSettings.forceManyBody ||
(d => -25 * nodeSizeAccessor(d))
)
);
// simulation.force("link", linkForce).nodes(projectedNodes)
simulation.nodes(projectedNodes);
const forceMod = adjustedSize[1] / adjustedSize[0];
if (!simulation.force('x')) {
simulation.force(
'x',
forceX(adjustedSize[0] / 2).strength(forceMod * 0.1)
);
}
if (!simulation.force('y')) {
simulation.force('y', forceY(adjustedSize[1] / 2).strength(0.1));
}
if (projectedEdges.length !== 0 && !simulation.force('link')) {
simulation.force('link', linkForce);
simulation.force('link').links(projectedEdges);
}
//reset alpha if it's too cold
if (simulation.alpha() < 0.1) {
simulation.alpha(1);
}
simulation.stop();
for (let i = 0; i < iterations; ++i) simulation.tick();
} else if (networkSettings.type === 'motifs') {
const componentHash = new Map();
projectedEdges.forEach(edge => {
[edge.source, edge.target].forEach(node => {
if (!componentHash.get(node)) {
componentHash.set(node, {
node,
component: -99,
connectedNodes: [],
edges: []
});
}
});
componentHash.get(edge.source).connectedNodes.push(edge.target);
componentHash.get(edge.target).connectedNodes.push(edge.source);
componentHash.get(edge.source).edges.push(edge);
});
components = breadthFirstCompontents(projectedNodes, componentHash);
const largestComponent = Math.max(
projectedNodes.length / 3,
components[0].componentNodes.length
);
const layoutSize = size[0] > size[1] ? size[1] : size[0];
const layoutDirection = size[0] > size[1] ? 'horizontal' : 'vertical';
const {
iterations = 500,
edgeStrength = 0.1,
edgeDistance,
padding = 0
} = networkSettings;
let currentX = padding;
let currentY = padding;
components.forEach(({ componentNodes, componentEdges }) => {
const linkForce = forceLink().strength(d =>
Math.min(2.5, d.weight ? d.weight * edgeStrength : edgeStrength)
);
if (edgeDistance) {
linkForce.distance(edgeDistance);
}
const componentLayoutSize =
Math.max(componentNodes.length / largestComponent, 0.2) *
layoutSize;
const xBound = componentLayoutSize + currentX;
const yBound = componentLayoutSize + currentY;
if (layoutDirection === 'horizontal') {
if (yBound > size[1]) {
currentX = componentLayoutSize + currentX + padding;
currentY = componentLayoutSize + padding;
} else {
currentY = componentLayoutSize + currentY + padding;
}
} else {
if (xBound > size[0]) {
currentY = componentLayoutSize + currentY + padding;
currentX = componentLayoutSize + padding;
} else {
currentX = componentLayoutSize + currentX + padding;
}
}
const xCenter = currentX - componentLayoutSize / 2;
const yCenter = currentY - componentLayoutSize / 2;
const simulation = forceSimulation()
.force(
'charge',
forceManyBody().strength(
networkSettings.forceManyBody ||
(d => -25 * nodeSizeAccessor(d))
)
)
.force('link', linkForce);
simulation
.force('x', forceX(xCenter))
.force('y', forceY(yCenter))
.nodes(componentNodes);
simulation.force('link').links(componentEdges);
simulation.stop();
for (let i = 0; i < iterations; ++i) simulation.tick();
const maxX = max(componentNodes.map(d => d.x));
const maxY = max(componentNodes.map(d => d.y));
const minX = min(componentNodes.map(d => d.x));
const minY = min(componentNodes.map(d => d.y));
const resetX = scaleLinear()
.domain([minX, maxX])
.range([currentX - componentLayoutSize, currentX - 20]);
const resetY = scaleLinear()
.domain([minY, maxY])
.range([currentY - componentLayoutSize, currentY - 20]);
componentNodes.forEach(node => {
node.x = resetX(node.x);
node.y = resetY(node.y);
});
});
} else if (networkSettings.type === 'matrix') {
if (networkSettings.sort) {
projectedNodes = projectedNodes.sort(networkSettings.sort);
}
const gridSize = Math.min(...adjustedSize);
const stepSize = gridSize / (projectedNodes.length + 1);
projectedNodes.forEach((node, index) => {
node.x = 0;
node.y = (index + 1) * stepSize;
});
} else if (networkSettings.type === 'arc') {
if (networkSettings.sort) {
projectedNodes = projectedNodes.sort(networkSettings.sort);
}
const stepSize = adjustedSize[0] / (projectedNodes.length + 2);
projectedNodes.forEach((node, index) => {
node.x = (index + 1) * stepSize;
node.y = adjustedSize[1] / 2;
});
} else if (typeof networkSettings.type === 'function') {
networkSettings.type({
nodes: projectedNodes,
edges: projectedEdges
});
} else {
projectedNodes.forEach(node => {
node.x = node.x === undefined ? (node.x0 + node.x1) / 2 : node.x;
node.y = node.y === undefined ? node.y0 : node.y;
});
}
updateFrameData({
...frameData,
graphSettings: {
...frameData.graphSettings,
nodes: singlePlot.props.nodes,
edges: singlePlot.props.edges
}
});
}
//filter out user-defined nodes
projectedNodes = projectedNodes.filter(filterRenderedNodes);
projectedEdges = projectedEdges.filter(
d =>
projectedNodes.indexOf(d.target) !== -1 &&
projectedNodes.indexOf(d.source) !== -1
);
if (networkSettings.direction === 'flip') {
projectedNodes.forEach(node => {
// const ox = node.x
// const oy = node.y
node.x = adjustedSize[0] - node.x;
node.y = adjustedSize[1] - node.y;
});
} else if (
networkSettings.direction === 'up' ||
networkSettings.direction === 'down'
) {
const mod =
networkSettings.direction === 'up'
? value => adjustedSize[1] - value
: value => value;
projectedNodes.forEach(node => {
const ox = node.x;
const ox0 = node.x0;
const ox1 = node.x1;
node.x = mod(node.y);
node.x0 = mod(node.y0);
node.x1 = mod(node.y1);
node.y = ox;
node.y0 = ox0;
node.y1 = ox1;
});
} else if (networkSettings.direction === 'left') {
projectedNodes.forEach(node => {
node.x = adjustedSize[0] - node.x;
node.x0 = adjustedSize[0] - node.x0;
node.x1 = adjustedSize[0] - node.x1;
});
}
if (typeof networkSettings.zoom === 'function') {
networkSettings.zoom(projectedNodes, adjustedSize);
} else if (
networkSettings.zoom !== false &&
networkSettings.type !== 'matrix' &&
networkSettings.type !== 'chord' &&
networkSettings.type !== 'sankey' &&
networkSettings.type !== 'partition' &&
networkSettings.type !== 'treemap' &&
networkSettings.type !== 'circlepack' &&
networkSettings.type !== 'dagre'
) {
// ZOOM SHOULD MAINTAIN ASPECT RATIO, ADD "stretch" to fill whole area
const xMin = min(projectedNodes.map(p => p.x - nodeSizeAccessor(p)));
const xMax = max(projectedNodes.map(p => p.x + nodeSizeAccessor(p)));
const yMin = min(projectedNodes.map(p => p.y - nodeSizeAccessor(p)));
const yMax = max(projectedNodes.map(p => p.y + nodeSizeAccessor(p)));
const xSize = Math.abs(xMax - xMin);
const ySize = Math.abs(yMax - yMin);
const networkAspectRatio = xSize / ySize;
const baseAspectRatio = adjustedSize[0] / adjustedSize[1];
let yMod, xMod;
if (networkSettings.zoom === 'stretch') {
yMod = 0;
xMod = 0;
} else if (xSize > ySize) {
if (networkAspectRatio > baseAspectRatio) {
xMod = 0;
yMod = (adjustedSize[1] - (adjustedSize[0] / xSize) * ySize) / 2;
} else {
yMod = 0;
xMod = (adjustedSize[0] - (adjustedSize[1] / ySize) * xSize) / 2;
}
} else {
if (networkAspectRatio > baseAspectRatio) {
xMod = 0;
yMod = (adjustedSize[1] - (adjustedSize[0] / xSize) * ySize) / 2;
} else {
yMod = 0;
xMod = (adjustedSize[0] - (adjustedSize[1] / ySize) * xSize) / 2;
}
}
const projectionScaleX = scaleLinear()
.domain([xMin, xMax])
.range([xMod, adjustedSize[0] - xMod]);
const projectionScaleY = scaleLinear()
.domain([yMin, yMax])
.range([yMod, adjustedSize[1] - yMod]);
projectedNodes.forEach(node => {
node.x = projectionScaleX(node.x);
node.y = projectionScaleY(node.y);
});
} else if (
networkSettings.zoom !== false &&
networkSettings.projection !== 'radial' &&
(networkSettings.type === 'partition' ||
networkSettings.type === 'treemap' ||
networkSettings.type === 'dagre')
) {
const xMin = min(projectedNodes.map(p => p.x0));
const xMax = max(projectedNodes.map(p => p.x1));
const yMin = min(projectedNodes.map(p => p.y0));
const yMax = max(projectedNodes.map(p => p.y1));
const projectionScaleX = scaleLinear()
.domain([xMin, xMax])
.range([margin.left, adjustedSize[0] - margin.right]);
const projectionScaleY = scaleLinear()
.domain([yMin, yMax])
.range([margin.top, adjustedSize[1] - margin.bottom]);
projectedNodes.forEach(node => {
node.x = projectionScaleX(node.x);
node.y = projectionScaleY(node.y);
node.x0 = projectionScaleX(node.x0);
node.y0 = projectionScaleY(node.y0);
node.x1 = projectionScaleX(node.x1);
node.y1 = projectionScaleY(node.y1);
node.zoomedHeight = node.y1 - node.y0;
node.zoomedWidth = node.x1 - node.x0;
});
projectedEdges.forEach(edge => {
if (edge.points) {
edge.points.forEach(p => {
p.x = projectionScaleX(p.x);
p.y = projectionScaleY(p.y);
});
}
});
}
projectedNodes.forEach(node => {
node.nodeSize = nodeSizeAccessor(node);
});
projectedEdges.forEach(edge => {
edge.width = edgeWidthAccessor(edge);
});
let legendSettings;
if (props.legend) {
legendSettings = props.legend;
if (!legendSettings.legendGroups) {
///Something auto for networks
const legendGroups = [
{
styleFn: nodeStyle,
type: 'fill',
items: []
}
];
legendSettings.legendGroups = legendGroups;
}
}
const nodeLabelAnnotations = [];
if (nodeLabels && projectedNodes) {
projectedNodes.forEach((node, nodei) => {
const feasibleLabel =
nodeLabels && nodeLabels !== true && nodeLabels(node);
if (nodeLabels === true || feasibleLabel) {
const actualLabel =
networkSettings.projection === 'radial' && node.depth !== 0
? radialLabelGenerator(
node,
nodei,
nodeLabels === true ? nodeIDAccessor : nodeLabels,
adjustedSize
)
: nodeLabels === true
? nodeIDAccessor(node, nodei)
: feasibleLabel;
let nodeLabel;
if (React.isValidElement(actualLabel)) {
nodeLabel = {
key: `node-label-${nodei}`,
type: 'basic-node-label',
x: node.x,
y: node.y,
element: actualLabel
};
} else {
nodeLabel = {
key: `node-label-${nodei}`,
className: 'node-label',
dx: 0,
dy: 0,
x: node.x,
y: node.y,
note: { label: actualLabel },
connector: { end: 'none' },
type: AnnotationLabel,
subject: { radius: nodeSizeAccessor(node) + 2 }
};
}
nodeLabelAnnotations.push(nodeLabel);
}
});
}
let screenCoordinates;
const overlay = [];
const areaBasedTypes = ['circlepack', 'treemap', 'partition', 'chord'];
if (
(hoverAnnotation &&
areaBasedTypes.find(d => d === networkSettings.type)) ||
hoverAnnotation === 'area'
) {
if (hoverAnnotation !== 'edge') {
const renderedNodeOverlays = projectedNodes.map((d, i) => ({
overlayData: d,
...customNodeIcon({
d,
i,
transform: `translate(${d.x},${d.y})`,
styleFn: () => ({ fill: 'pink', opacity: 0 })
}).props
}));
overlay.push(...renderedNodeOverlays);
}
if (hoverAnnotation !== 'node') {
projectedEdges.forEach((d, i) => {
const generatedIcon = customEdgeIcon({
d,
i,
transform: `translate(${d.x},${d.y})`,
styleFn: () => ({ fill: 'pink', opacity: 0 })
});
if (generatedIcon) {
overlay.push({
overlayData: {
...d,
x: d.x || (d.source.x + d.target.x) / 2,
y: d.y || (d.source.y + d.target.y) / 2,
edge: true
},
...generatedIcon.props
});
}
});
}
} else if (
hoverAnnotation === 'edge' &&
typeof networkSettings.type === 'string' &&
edgePointHash[networkSettings.type]
) {
screenCoordinates = projectedEdges.map(
edgePointHash[networkSettings.type]
);
} else if (
Array.isArray(hoverAnnotation) ||
hoverAnnotation === true ||
hoverAnnotation === 'node'
) {
screenCoordinates = projectedNodes;
if (changedData || networkSettingsChanged)
screenCoordinates = [...projectedNodes];
} else if (
hoverAnnotation === 'all' &&
typeof networkSettings.type === 'string'
) {
screenCoordinates = [
...projectedEdges.map(edgePointHash[networkSettings.type]),
...projectedNodes
];
}
const { svgPipe, canvasPipe } = toPipeline({
projectedEdges,
projectedNodes,
edgeStyle: stringToFn(edgeStyle, () => ({}), true),
edgeClass: stringToFn(edgeClass, () => '', true),
edgeRenderMode: stringToFn(edgeRenderMode, undefined, true),
edgeUseCanvas,
edgeRenderKey: edgeRenderKey
? edgeRenderKey
: d => d._NWFEdgeKey || `${d.source.id}-${d.target.id}`,
projection: networkSettings.projection,
edgeType,
customEdgeIcon,
networkType: networkSettings.type,
direction: networkSettings.direction,
nodeStyle,
nodeClass: stringToFn(nodeClass, () => '', true),
nodeRenderMode: stringToFn(nodeRenderMode, undefined, true),
nodeUseCanvas,
customNodeIcon,
nodeRenderKey
});
const svgPipeline = [...svgPipe];
const canvasPipeline = canvasPipe.slice();
updateFrameData({
marginGraphic,
adjustedPosition: adjustedPosition,
adjustedSize: adjustedSize,
renderNumber: frameData.renderNumber + 1,
projectedNodes,
projectedEdges,
screenCoordinates,
overlay,
nodeIDAccessor,
sourceAccessor,
targetAccessor,
nodeSizeAccessor,
edgeWidthAccessor,
margin,
nodeLabelAnnotations,
svgPipeline,
canvasPipeline,
graphSettings: {
...networkSettings.graphSettings,
...networkSettings,
direction,
projection
},
customNodeIcon,
customEdgeIcon,
nodeStyle
});
};
useEffect(() => {
computeFrame(props);
}, []);
return frameData;
};
export default computeNetworkFrameData;