UNPKG

react-network-diagrams

Version:
604 lines (540 loc) 22.2 kB
/** * Copyright (c) 2018, The Regents of the University of California, * through Lawrence Berkeley National Laboratory (subject to receipt * of any required approvals from the U.S. Dept. of Energy). * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ import _ from "underscore"; import React from "react"; import PropTypes from "prop-types"; import { scaleLinear } from "d3-scale"; import { BidirectionalEdge } from "./BidirectionalEdge"; import { NodeLabel as Label } from "./NodeLabel"; import { MapLegend as Legend } from "./MapLegend"; import { Node } from "./Node"; import { SimpleEdge } from "./SimpleEdge"; // import '../map.css'; function getElementOffset(element) { const de = document.documentElement; const box = element.getBoundingClientRect(); const top = box.top + window.pageYOffset - de.clientTop; const left = box.left + window.pageXOffset - de.clientLeft; return { top, left }; } /** * The BaseMap forms a general network drawing component which * doesn't assume that the drawing is of a Network. It is used * by the `<TrafficMap>` to produce the chart seen of the front * page of my.es.net. */ export class BaseMap extends React.Component { constructor(props) { super(props); this.state = { dragging: null }; } handleNodeMouseDown(id, e) { const { xScale, yScale } = this.scale(); const { x, y } = this.getOffsetMousePosition(e); const drag = { id, x0: xScale.invert(x), y0: yScale.invert(y) }; this.setState({ dragging: drag }); } handleSelectionChange(type, id) { if (this.props.onNodeSelected) { if (type === "node") { this.props.onNodeSelected(id); } } else if (this.props.onEdgeSelected) { if (type === "edge") { this.props.onEdgeSelected(id); } } else if (this.props.onSelectionChange) { this.props.onSelectionChange(type, id); } } handleMouseMove(e) { e.preventDefault(); if (this.state.dragging) { const { id } = this.state.dragging; const { xScale, yScale } = this.scale(); const { x, y } = this.getOffsetMousePosition(e); if (this.props.onNodeDrag) { this.props.onNodeDrag(id, xScale.invert(x), yScale.invert(y)); } } } handleMouseUp(e) { e.stopPropagation(); this.setState({ dragging: null }); } handleClick(e) { if (this.props.onNodeSelected || this.props.onEdgeSelected) { return; } if (this.props.onPositionSelected) { const { xScale, yScale } = this.scale(); const { x, y } = this.getOffsetMousePosition(e); this.props.onPositionSelected(xScale.invert(x), yScale.invert(y)); } if (this.props.onSelectionChange) { this.props.onSelectionChange(null); } } /** * Get the event mouse position relative to the event rect */ getOffsetMousePosition(e) { const trackerRect = this.map; const offset = getElementOffset(trackerRect); const x = e.pageX - offset.left; const y = e.pageY - offset.top; return { x: Math.round(x), y: Math.round(y) }; } scale() { return { xScale: scaleLinear() .domain([this.props.bounds.x1, this.props.bounds.x2]) .range([this.props.margin, this.props.width - this.props.margin * 2]), yScale: scaleLinear() .domain([this.props.bounds.y1, this.props.bounds.y2]) .range([this.props.margin, this.props.height - this.props.margin * 2]) }; } render() { const { xScale, yScale } = this.scale(); const hasSelectedNode = this.props.selection.nodes.length; const hasSelectedEdge = this.props.selection.edges.length; // // Build a mapping of edge names to the edges themselves // const edgeMap = {}; _.each(this.props.topology.edges, edge => { edgeMap[`${edge.source}--${edge.target}`] = edge; edgeMap[`${edge.target}--${edge.source}`] = edge; }); // // Build a list of nodes (each a Node) from our topology // const secondarySelectedNodes = []; _.each(this.props.selection.edges, edgeName => { const edge = edgeMap[edgeName]; if (edge) { secondarySelectedNodes.push(edge.source); secondarySelectedNodes.push(edge.target); } }); const nodeCoordinates = {}; const nodes = _.map(this.props.topology.nodes, node => { const { name, id, label, ...props } = node; props.id = id || name; props.x = xScale(node.x); props.y = yScale(node.y); props.label = label || name; const nodeSelected = _.contains(this.props.selection.nodes, props.id); const edgeSelected = _.contains(secondarySelectedNodes, node.name); props.selected = nodeSelected || edgeSelected; props.muted = (hasSelectedNode && !props.selected) || (hasSelectedEdge && !props.selected); nodeCoordinates[node.name] = { x: props.x, y: props.y }; return ( <Node key={props.id} {...props} onSelectionChange={(type, i) => this.handleSelectionChange(type, i)} onMouseDown={(id, e) => this.handleNodeMouseDown(id, e)} onMouseMove={(type, i, xx, yy) => this.props.onNodeMouseMove(i, xx, yy)} onMouseUp={(type, i, e) => this.props.onNodeMouseUp(i, e)} /> ); }); // // Build a axillary structure to help us build the paths // // For each node, we need a map of sources and destinations // for each path e.g. If DENV has two incoming paths, both // from SACR and one out going path to KANS the that would // be represented like this: // // nodePathMap[DENV].targetMap[SACR] => [PATH1, PATH2] // [KANS] => [PATH2] const nodePaths = {}; _.each(this.props.paths, path => { const pathName = path.name; const pathSteps = path.steps; for (let i = 0; i < pathSteps.length - 1; i++) { const node = pathSteps[i]; const next = pathSteps[i + 1]; let a; let z; // We store our target based on geography, west to east etc A->Z if (_.has(nodeCoordinates, node) && _.has(nodeCoordinates, next)) { if ( nodeCoordinates[node].x < nodeCoordinates[next].x || nodeCoordinates[node].y < nodeCoordinates[next].y ) { a = node; z = next; } else { a = next; z = node; } if (!_.has(nodePaths, a)) { nodePaths[a] = { targetMap: {} }; } if (!_.has(nodePaths[a].targetMap, z)) { nodePaths[a].targetMap[z] = []; } nodePaths[a].targetMap[z].push(pathName); } else { if (!_.has(nodeCoordinates, node)) { throw new Error(`Missing node in path '${pathName}': ${node}`); } if (!_.has(nodeCoordinates, next)) { throw new Error(`Missing node in path '${pathName}': ${next}`); } } } }); // // For drawing path bidirectional only, we build up a map first to // tell us which edges are touched by a path // const edgePathMap = {}; _.each(this.props.paths, path => { const pathSteps = path.steps; if (pathSteps.length > 1) { for (let i = 0; i < pathSteps.length - 1; i++) { const source = pathSteps[i]; const destination = pathSteps[i + 1]; const sourceToDestinationName = `${source}--${destination}`; const destinationToSourceName = `${destination}--${source}`; edgePathMap[sourceToDestinationName] = path; edgePathMap[destinationToSourceName] = path; } } }); const edges = _.map(this.props.topology.edges, edge => { const selected = _.contains(this.props.selection.edges, edge.name); if (!_.has(nodeCoordinates, edge.source) || !_.has(nodeCoordinates, edge.target)) { return; } // either 'simple' or 'bi-directional' edges. const edgeDrawingMethod = this.props.edgeDrawingMethod; // either 'linear' or 'curved' let edgeShape = "linear"; if (!_.isUndefined(edge.shape) && !_.isNull(edge.shape)) { edgeShape = edge.shape; } let curveDirection = "left"; if (!_.isUndefined(edge.curveDirection) && !_.isNull(edge.curveDirection)) { curveDirection = edge.curveDirection; } const muted_val = (hasSelectedEdge && !selected) || hasSelectedNode; const muted = muted_val === 0 ? false : true; if (edgeDrawingMethod === "simple") { return ( <SimpleEdge x1={nodeCoordinates[edge.source].x} x2={nodeCoordinates[edge.target].x} y1={nodeCoordinates[edge.source].y} y2={nodeCoordinates[edge.target].y} source={edge.source} target={edge.target} shape={edgeShape} curveDirection={curveDirection} color={edge.stroke} width={edge.width} classed={edge.classed} key={edge.name} name={edge.name} selected={selected} muted={muted} onSelectionChange={(type, id) => this.handleSelectionChange(type, id)} /> ); } else if (edgeDrawingMethod === "bidirectionalArrow") { return ( <BidirectionalEdge x1={nodeCoordinates[edge.source].x} x2={nodeCoordinates[edge.target].x} y1={nodeCoordinates[edge.source].y} y2={nodeCoordinates[edge.target].y} source={edge.source} target={edge.target} shape={edgeShape} curveDirection={curveDirection} offset={edge.offset} sourceTargetColor={edge.sourceTargetColor} targetSourceColor={edge.targetSourceColor} width={edge.width} classed={edge.classed} key={edge.name} name={edge.name} selected={selected} muted={muted} onSelectionChange={(type, id) => this.handleSelectionChange(type, id)} /> ); } else if (edgeDrawingMethod === "pathBidirectionalArrow") { if (_.has(edgePathMap, edge.name)) { return ( <BidirectionalEdge x1={nodeCoordinates[edge.source].x} x2={nodeCoordinates[edge.target].x} y1={nodeCoordinates[edge.source].y} y2={nodeCoordinates[edge.target].y} source={edge.source} target={edge.target} shape={edgeShape} curveDirection={curveDirection} sourceTargetColor={edge.sourceTargetColor} targetSourceColor={edge.targetSourceColor} width={edge.width} classed={edge.classed} key={edge.name} name={edge.name} selected={selected} muted={muted} onSelectionChange={(type, id) => this.handleSelectionChange(type, id)} /> ); } else { return ( <SimpleEdge x1={nodeCoordinates[edge.source].x} x2={nodeCoordinates[edge.target].x} y1={nodeCoordinates[edge.source].y} y2={nodeCoordinates[edge.target].y} source={edge.source} target={edge.target} shape={edgeShape} curveDirection={curveDirection} color={edge.stroke} width={1} classed={edge.classed} key={edge.name} name={edge.name} selected={selected} muted={muted} onSelectionChange={(type, id) => this.handleSelectionChange(type, id)} /> ); } } }); // // Build the paths // const paths = _.map(this.props.paths, path => { const pathName = path.name; const pathSteps = path.steps; const pathSegments = []; const pathColor = path.color || "steelblue"; const pathWidth = path.width || 1; if (pathSteps.length > 1) { for (let i = 0; i < pathSteps.length - 1; i++) { let a; let z; let dir; const source = pathSteps[i]; const destination = pathSteps[i + 1]; // Get the position of path (if multiple paths run parallel) if ( nodeCoordinates[source].x < nodeCoordinates[destination].x || nodeCoordinates[source].y < nodeCoordinates[destination].y ) { a = source; z = destination; dir = 1; } else { a = destination; z = source; dir = -1; } const pathsToDest = nodePaths[a].targetMap[z]; const pathIndex = _.indexOf(pathsToDest, pathName); const pos = (pathIndex - (pathsToDest.length - 1) / 2) * dir; // Get the edge from edgeMap const edgeName = `${source}--${destination}`; const edge = edgeMap[edgeName]; // Get the shape of the edge (linear or curved) and if // curved, get the curve direction let edgeShape = "linear"; if (edge && !_.isUndefined(edge.shape) && !_.isNull(edge.shape)) { edgeShape = edge.shape; } // either 'left' or 'right' let curveDirection = "left"; if ( edge && !_.isUndefined(edge.curveDirection) && !_.isNull(edge.curveDirection) ) { curveDirection = edge.curveDirection; } // // Construct this path segment as a simple (i.e. line only) // path piece. The width of the path is currently a prop of // the map, but it would be nice to expand this to // optionally be a prop of that line segement // if (this.props.edgeDrawingMethod === "simple") { pathSegments.push( <SimpleEdge x1={nodeCoordinates[source].x} y1={nodeCoordinates[source].y} x2={nodeCoordinates[destination].x} y2={nodeCoordinates[destination].y} position={pos * 6} source={source} color={pathColor} target={destination} shape={edgeShape} curveDirection={curveDirection} width={pathWidth} classed={`path-${pathName}`} key={`${pathName}--${edgeName}`} name={`${pathName}--${edgeName}`} /> ); } } } return <g key={pathName}>{pathSegments}</g>; }); // // Build the labels // const labels = _.map(this.props.topology.labels, label => { const x = xScale(label.x); const y = yScale(label.y); return ( <Label x={x} y={y} label={label.label} labelPosition={label.labelPosition} key={label.label} /> ); }); // // Build the legend // let legend = null; if (!_.isNull(this.props.legendItems)) { legend = ( <Legend x={this.props.legendItems.x} y={this.props.legendItems.y} edgeTypes={this.props.legendItems.edgeTypes} nodeTypes={this.props.legendItems.nodeTypes} colorSwatches={this.props.legendItems.colorSwatches} /> ); } let style; if (this.state.dragging) { style = { cursor: "pointer" }; } else if ( this.props.onPositionSelected || this.props.onNodeSelected || this.props.onEdgeSelected ) { style = { cursor: "crosshair" }; } else { style = { cursor: "default" }; } return ( <svg style={style} ref={inst => { this.map = inst; }} width={this.props.width} height={this.props.height} className="noselect map-container" onClick={e => this.handleClick(e)} onMouseMove={e => this.handleMouseMove(e)} onMouseUp={e => this.handleMouseUp(e)} > <g> {edges} {paths} {nodes} {labels} {legend} </g> </svg> ); } } BaseMap.propTypes = { /** * The topology structure, as detailed above. This contains the * descriptions of nodes, edges and paths used to render the topology */ topology: PropTypes.object.isRequired, /** The width of the circuit diagram */ width: PropTypes.number, /** The height of the circuit diagram */ height: PropTypes.number, /** The blank margin around the diagram drawing */ margin: PropTypes.number, /** * Specified as an object containing x1, y1 and x2, y2. This is the region * to display on the map. If this isn't specified the bounds will be * calculated from the nodes in the Map. */ bounds: PropTypes.shape({ x1: PropTypes.number, y1: PropTypes.number, x2: PropTypes.number, y2: PropTypes.number }), /** * The is the overall rendering style for the edge connections. Maybe * one of the following strings: * * * "simple" - simple line connections between nodes * * "bidirectionalArrow" - network traffic represented by bi-directional arrows * * "pathBidirectionalArrow" - similar to "bidirectionalArrow", but only for * edges that are used in the currently displayed path(s). */ edgeDrawingMethod: PropTypes.oneOf(["simple", "bidirectionalArrow", "pathBidirectionalArrow"]), legendItems: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number, edgeTypes: PropTypes.object, nodeTypes: PropTypes.object, colorSwatches: PropTypes.object }), selection: PropTypes.object, paths: PropTypes.array, pathWidth: PropTypes.number }; BaseMap.defaultProps = { width: 800, height: 600, margin: 20, bounds: { x1: 0, y1: 0, x2: 1, y2: 1 }, edgeDrawingMethod: "simple", legendItems: null, selection: { nodes: {}, edges: {} }, paths: [], pathWidth: 5 };