react-network-diagrams
Version:
677 lines (586 loc) • 32.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.BaseMap = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _underscore = require("underscore");
var _underscore2 = _interopRequireDefault(_underscore);
var _react = require("react");
var _react2 = _interopRequireDefault(_react);
var _propTypes = require("prop-types");
var _propTypes2 = _interopRequireDefault(_propTypes);
var _d3Scale = require("d3-scale");
var _BidirectionalEdge = require("./BidirectionalEdge");
var _NodeLabel = require("./NodeLabel");
var _MapLegend = require("./MapLegend");
var _Node = require("./Node");
var _SimpleEdge = require("./SimpleEdge");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /**
* 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 '../map.css';
function getElementOffset(element) {
var de = document.documentElement;
var box = element.getBoundingClientRect();
var top = box.top + window.pageYOffset - de.clientTop;
var left = box.left + window.pageXOffset - de.clientLeft;
return { top: top, left: 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.
*/
var BaseMap = exports.BaseMap = function (_React$Component) {
_inherits(BaseMap, _React$Component);
function BaseMap(props) {
_classCallCheck(this, BaseMap);
var _this = _possibleConstructorReturn(this, (BaseMap.__proto__ || Object.getPrototypeOf(BaseMap)).call(this, props));
_this.state = {
dragging: null
};
return _this;
}
_createClass(BaseMap, [{
key: "handleNodeMouseDown",
value: function handleNodeMouseDown(id, e) {
var _scale = this.scale(),
xScale = _scale.xScale,
yScale = _scale.yScale;
var _getOffsetMousePositi = this.getOffsetMousePosition(e),
x = _getOffsetMousePositi.x,
y = _getOffsetMousePositi.y;
var drag = {
id: id,
x0: xScale.invert(x),
y0: yScale.invert(y)
};
this.setState({ dragging: drag });
}
}, {
key: "handleSelectionChange",
value: function 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);
}
}
}, {
key: "handleMouseMove",
value: function handleMouseMove(e) {
e.preventDefault();
if (this.state.dragging) {
var id = this.state.dragging.id;
var _scale2 = this.scale(),
xScale = _scale2.xScale,
yScale = _scale2.yScale;
var _getOffsetMousePositi2 = this.getOffsetMousePosition(e),
x = _getOffsetMousePositi2.x,
y = _getOffsetMousePositi2.y;
if (this.props.onNodeDrag) {
this.props.onNodeDrag(id, xScale.invert(x), yScale.invert(y));
}
}
}
}, {
key: "handleMouseUp",
value: function handleMouseUp(e) {
e.stopPropagation();
this.setState({ dragging: null });
}
}, {
key: "handleClick",
value: function handleClick(e) {
if (this.props.onNodeSelected || this.props.onEdgeSelected) {
return;
}
if (this.props.onPositionSelected) {
var _scale3 = this.scale(),
xScale = _scale3.xScale,
yScale = _scale3.yScale;
var _getOffsetMousePositi3 = this.getOffsetMousePosition(e),
x = _getOffsetMousePositi3.x,
y = _getOffsetMousePositi3.y;
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
*/
}, {
key: "getOffsetMousePosition",
value: function getOffsetMousePosition(e) {
var trackerRect = this.map;
var offset = getElementOffset(trackerRect);
var x = e.pageX - offset.left;
var y = e.pageY - offset.top;
return { x: Math.round(x), y: Math.round(y) };
}
}, {
key: "scale",
value: function scale() {
return {
xScale: (0, _d3Scale.scaleLinear)().domain([this.props.bounds.x1, this.props.bounds.x2]).range([this.props.margin, this.props.width - this.props.margin * 2]),
yScale: (0, _d3Scale.scaleLinear)().domain([this.props.bounds.y1, this.props.bounds.y2]).range([this.props.margin, this.props.height - this.props.margin * 2])
};
}
}, {
key: "render",
value: function render() {
var _this2 = this;
var _scale4 = this.scale(),
xScale = _scale4.xScale,
yScale = _scale4.yScale;
var hasSelectedNode = this.props.selection.nodes.length;
var hasSelectedEdge = this.props.selection.edges.length;
//
// Build a mapping of edge names to the edges themselves
//
var edgeMap = {};
_underscore2.default.each(this.props.topology.edges, function (edge) {
edgeMap[edge.source + "--" + edge.target] = edge;
edgeMap[edge.target + "--" + edge.source] = edge;
});
//
// Build a list of nodes (each a Node) from our topology
//
var secondarySelectedNodes = [];
_underscore2.default.each(this.props.selection.edges, function (edgeName) {
var edge = edgeMap[edgeName];
if (edge) {
secondarySelectedNodes.push(edge.source);
secondarySelectedNodes.push(edge.target);
}
});
var nodeCoordinates = {};
var nodes = _underscore2.default.map(this.props.topology.nodes, function (node) {
var name = node.name,
id = node.id,
label = node.label,
props = _objectWithoutProperties(node, ["name", "id", "label"]);
props.id = id || name;
props.x = xScale(node.x);
props.y = yScale(node.y);
props.label = label || name;
var nodeSelected = _underscore2.default.contains(_this2.props.selection.nodes, props.id);
var edgeSelected = _underscore2.default.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 _react2.default.createElement(_Node.Node, _extends({
key: props.id
}, props, {
onSelectionChange: function onSelectionChange(type, i) {
return _this2.handleSelectionChange(type, i);
},
onMouseDown: function onMouseDown(id, e) {
return _this2.handleNodeMouseDown(id, e);
},
onMouseMove: function onMouseMove(type, i, xx, yy) {
return _this2.props.onNodeMouseMove(i, xx, yy);
},
onMouseUp: function onMouseUp(type, i, e) {
return _this2.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]
var nodePaths = {};
_underscore2.default.each(this.props.paths, function (path) {
var pathName = path.name;
var pathSteps = path.steps;
for (var i = 0; i < pathSteps.length - 1; i++) {
var node = pathSteps[i];
var next = pathSteps[i + 1];
var a = void 0;
var z = void 0;
// We store our target based on geography, west to east etc A->Z
if (_underscore2.default.has(nodeCoordinates, node) && _underscore2.default.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 (!_underscore2.default.has(nodePaths, a)) {
nodePaths[a] = { targetMap: {} };
}
if (!_underscore2.default.has(nodePaths[a].targetMap, z)) {
nodePaths[a].targetMap[z] = [];
}
nodePaths[a].targetMap[z].push(pathName);
} else {
if (!_underscore2.default.has(nodeCoordinates, node)) {
throw new Error("Missing node in path '" + pathName + "': " + node);
}
if (!_underscore2.default.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
//
var edgePathMap = {};
_underscore2.default.each(this.props.paths, function (path) {
var pathSteps = path.steps;
if (pathSteps.length > 1) {
for (var i = 0; i < pathSteps.length - 1; i++) {
var source = pathSteps[i];
var destination = pathSteps[i + 1];
var sourceToDestinationName = source + "--" + destination;
var destinationToSourceName = destination + "--" + source;
edgePathMap[sourceToDestinationName] = path;
edgePathMap[destinationToSourceName] = path;
}
}
});
var edges = _underscore2.default.map(this.props.topology.edges, function (edge) {
var selected = _underscore2.default.contains(_this2.props.selection.edges, edge.name);
if (!_underscore2.default.has(nodeCoordinates, edge.source) || !_underscore2.default.has(nodeCoordinates, edge.target)) {
return;
}
// either 'simple' or 'bi-directional' edges.
var edgeDrawingMethod = _this2.props.edgeDrawingMethod;
// either 'linear' or 'curved'
var edgeShape = "linear";
if (!_underscore2.default.isUndefined(edge.shape) && !_underscore2.default.isNull(edge.shape)) {
edgeShape = edge.shape;
}
var curveDirection = "left";
if (!_underscore2.default.isUndefined(edge.curveDirection) && !_underscore2.default.isNull(edge.curveDirection)) {
curveDirection = edge.curveDirection;
}
var muted_val = hasSelectedEdge && !selected || hasSelectedNode;
var muted = muted_val === 0 ? false : true;
if (edgeDrawingMethod === "simple") {
return _react2.default.createElement(_SimpleEdge.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: function onSelectionChange(type, id) {
return _this2.handleSelectionChange(type, id);
}
});
} else if (edgeDrawingMethod === "bidirectionalArrow") {
return _react2.default.createElement(_BidirectionalEdge.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: function onSelectionChange(type, id) {
return _this2.handleSelectionChange(type, id);
}
});
} else if (edgeDrawingMethod === "pathBidirectionalArrow") {
if (_underscore2.default.has(edgePathMap, edge.name)) {
return _react2.default.createElement(_BidirectionalEdge.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: function onSelectionChange(type, id) {
return _this2.handleSelectionChange(type, id);
}
});
} else {
return _react2.default.createElement(_SimpleEdge.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: function onSelectionChange(type, id) {
return _this2.handleSelectionChange(type, id);
}
});
}
}
});
//
// Build the paths
//
var paths = _underscore2.default.map(this.props.paths, function (path) {
var pathName = path.name;
var pathSteps = path.steps;
var pathSegments = [];
var pathColor = path.color || "steelblue";
var pathWidth = path.width || 1;
if (pathSteps.length > 1) {
for (var i = 0; i < pathSteps.length - 1; i++) {
var a = void 0;
var z = void 0;
var dir = void 0;
var source = pathSteps[i];
var 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;
}
var pathsToDest = nodePaths[a].targetMap[z];
var pathIndex = _underscore2.default.indexOf(pathsToDest, pathName);
var pos = (pathIndex - (pathsToDest.length - 1) / 2) * dir;
// Get the edge from edgeMap
var edgeName = source + "--" + destination;
var edge = edgeMap[edgeName];
// Get the shape of the edge (linear or curved) and if
// curved, get the curve direction
var edgeShape = "linear";
if (edge && !_underscore2.default.isUndefined(edge.shape) && !_underscore2.default.isNull(edge.shape)) {
edgeShape = edge.shape;
}
// either 'left' or 'right'
var curveDirection = "left";
if (edge && !_underscore2.default.isUndefined(edge.curveDirection) && !_underscore2.default.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 (_this2.props.edgeDrawingMethod === "simple") {
pathSegments.push(_react2.default.createElement(_SimpleEdge.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 _react2.default.createElement(
"g",
{ key: pathName },
pathSegments
);
});
//
// Build the labels
//
var labels = _underscore2.default.map(this.props.topology.labels, function (label) {
var x = xScale(label.x);
var y = yScale(label.y);
return _react2.default.createElement(_NodeLabel.NodeLabel, {
x: x,
y: y,
label: label.label,
labelPosition: label.labelPosition,
key: label.label
});
});
//
// Build the legend
//
var legend = null;
if (!_underscore2.default.isNull(this.props.legendItems)) {
legend = _react2.default.createElement(_MapLegend.MapLegend, {
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
});
}
var style = void 0;
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 _react2.default.createElement(
"svg",
{
style: style,
ref: function ref(inst) {
_this2.map = inst;
},
width: this.props.width,
height: this.props.height,
className: "noselect map-container",
onClick: function onClick(e) {
return _this2.handleClick(e);
},
onMouseMove: function onMouseMove(e) {
return _this2.handleMouseMove(e);
},
onMouseUp: function onMouseUp(e) {
return _this2.handleMouseUp(e);
}
},
_react2.default.createElement(
"g",
null,
edges,
paths,
nodes,
labels,
legend
)
);
}
}]);
return BaseMap;
}(_react2.default.Component);
BaseMap.propTypes = {
/**
* The topology structure, as detailed above. This contains the
* descriptions of nodes, edges and paths used to render the topology
*/
topology: _propTypes2.default.object.isRequired,
/** The width of the circuit diagram */
width: _propTypes2.default.number,
/** The height of the circuit diagram */
height: _propTypes2.default.number,
/** The blank margin around the diagram drawing */
margin: _propTypes2.default.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: _propTypes2.default.shape({
x1: _propTypes2.default.number,
y1: _propTypes2.default.number,
x2: _propTypes2.default.number,
y2: _propTypes2.default.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: _propTypes2.default.oneOf(["simple", "bidirectionalArrow", "pathBidirectionalArrow"]),
legendItems: _propTypes2.default.shape({
x: _propTypes2.default.number,
y: _propTypes2.default.number,
edgeTypes: _propTypes2.default.object,
nodeTypes: _propTypes2.default.object,
colorSwatches: _propTypes2.default.object
}),
selection: _propTypes2.default.object,
paths: _propTypes2.default.array,
pathWidth: _propTypes2.default.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
};