react-network-diagrams
Version:
393 lines (347 loc) • 12.6 kB
JavaScript
/**
* 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 React from "react";
import _ from "underscore";
import PropTypes from "prop-types";
import { Connection } from "./Connection";
import { Endpoint } from "./Endpoint";
import { Navigate } from "./Navigate";
import { Directions } from "../js/constants";
/**
* Draw a Concatenated circuit
*
* This component determines the (x1, y1), (x2, y2) coordinates on the page
* and then renders a group of circuits by combining the
* connection and endpoint props. Connection shape, style, and label information,
* are passed in as props.
*
* This is of the form:
* [endpoint, connection, endpoint, connection, endpoint, ...]
*/
export class ConcatenatedCircuit extends React.Component {
renderCircuitTitle(title) {
const titleStyle = {
textAnchor: "left",
fill: "#9D9D9D",
fontFamily: "verdana, sans-serif",
fontSize: 14
};
if (!this.props.hideTitle) {
return (
<text
className="circuit-title"
key="circuit-title"
style={titleStyle}
x={this.props.titleOffsetX}
y={this.props.titleOffsetY}
>
{title}
</text>
);
} else {
return null;
}
}
renderParentNavigation(parentId) {
if (parentId) {
return (
<g>
<Navigate
direction={Directions.NORTH}
ypos={0}
width={this.props.width}
height={this.props.height}
id={this.props.parentId}
onSelectionChange={this.props.onSelectionChange}
/>
</g>
);
} else {
return null;
}
}
renderDisabledOverlay(disabled) {
const style = { fill: "#FDFDFD", fillOpacity: 0.65 };
if (disabled) {
return (
<rect
className="circuit-overlay"
style={style}
x="0"
y="0"
width={this.props.width}
height={this.props.height}
/>
);
} else {
return null;
}
}
renderCircuitElements() {
const elements = [];
// Determine the initial position
const y1 = this.props.height / 4;
const y2 = y1;
let x1 = this.props.margin;
let x2 = this.props.width - this.props.margin;
const memberList = this.props.memberList;
//
// Since squares may be a different width than other connections, and may appear
// at different positions inside the concatenation, we need to determine
// the total combined squareWidth of all the square connectors, then subtract that
// from the total available width. The remaining length divided by the number
// of non-square segments is how long the remaining non-square segments can be
//
const memberCount = memberList.length;
let squareMemberCount = 0;
const totalWidth = this.props.width - this.props.margin * 2;
let totalSquareWidth = 0;
_.each(memberList, member => {
if (_.has(member.styleProperties, "squareWidth")) {
totalSquareWidth += member.styleProperties.squareWidth;
squareMemberCount += 1;
}
});
const lineWidth = (totalWidth - totalSquareWidth) / (memberCount - squareMemberCount);
// Draw the first endpoint
elements.push(
<Endpoint
x={x1}
y={y1}
key={"endpoint-0"}
style={memberList[0].endpointStyle}
labelPosition={this.props.endpointLabelPosition}
offset={this.props.endpointLabelOffset}
label={memberList[0].endpointLabelA}
/>
);
/* since the Z of each member is shared with the A of the next member, render only
* the Z for each member starting with the first member
*/
_.each(memberList, (member, memberIndex) => {
if (member.styleProperties.lineShape === "square") {
x2 = x1 + member.styleProperties.squareWidth;
} else {
x2 = x1 + lineWidth;
}
elements.push(
<Endpoint
x={x2}
y={y2}
key={"endpoint-" + (memberIndex + 1)}
style={member.endpointStyle}
labelPosition={this.props.endpointLabelPosition}
offset={this.props.endpointLabelOffset}
label={member.endpointLabelZ}
/>
);
x1 = x2;
});
// reset x1
x1 = this.props.margin;
// Collect all the connections
_.each(memberList, (member, memberIndex) => {
const roundedX = member.styleProperties.roundedX || this.props.roundedX;
const roundedY = member.styleProperties.roundedY || this.props.roundedY;
if (member.styleProperties.lineShape === "square") {
x2 = x1 + member.styleProperties.squareWidth;
} else {
x2 = x1 + lineWidth;
}
elements.push(
<Connection
x1={x1}
x2={x2}
y1={y1}
y2={y2}
key={"circuit-" + memberIndex}
roundedX={roundedX}
roundedY={roundedY}
style={member.styleProperties.style}
lineShape={member.styleProperties.lineShape}
label={member.circuitLabel}
labelPosition={this.props.connectionLabelPosition}
labelOffsetY={this.props.yOffset}
noNavigate={member.styleProperties.noNavigate}
navTo={member.navTo}
size={member.styleProperties.size}
centerLine={member.styleProperties.centerLine}
onSelectionChange={this.props.onSelectionChange}
/>
);
x1 = x2;
});
return <g>{elements}</g>;
}
render() {
const circuitContainer = {
normal: {
borderTopStyle: "solid",
borderBottomStyle: "solid",
borderWidth: 1,
borderTopColor: "#FFFFFF",
borderBottomColor: "#EFEFEF",
width: "100%",
height: this.props.height
},
disabled: {
width: "100%",
height: this.props.height
}
};
let className = "circuit-container";
let svgStyle;
if (this.props.disabled) {
className += " disabled";
svgStyle = circuitContainer.disabled;
} else {
svgStyle = circuitContainer.normal;
}
return (
<svg className={className} style={svgStyle} onClick={this._deselect}>
{this.renderCircuitTitle(this.props.title)}
{this.renderCircuitElements()}
{this.renderParentNavigation(this.props.parentId)}
{this.renderDisabledOverlay(this.props.disabled)}
</svg>
);
}
}
ConcatenatedCircuit.propTypes = {
/** The width of the circuit diagram */
width: PropTypes.number,
/** The height of the circuit diagram */
height: PropTypes.number,
/** The position of the title relative to the left side of the diagram */
titleOffsetX: PropTypes.number,
/** The position of the title relative to the top of the diagram */
titleOffsetY: PropTypes.number,
/** The blank margin around the diagram drawing */
margin: PropTypes.number,
/**
* Controls shape of the line, can be "linear", "square", "angled", "arc".
*/
lineShape: PropTypes.oneOf(["linear", "square", "angled", "arc"]),
/**
* To accurately display each of the member circuits, the concatenated circuit
* requires an ordered array of circuit objects, where each object contains
* the props to be used by the lower level connection and endpoint primitives.
* Since the list renders sequentially, it assumes that the member circuits are in order. The list can be any length and needs to be constructed as such:
*
* ```
* const memberList = [
* {
* styleProperties: darkFiberStyle,
* endpointStyle: stylesMap.endpoint,
* endpointLabelA: "Endpoint 1",
* endpointLabelZ: "Endpoint 2",
* circuitLabel: "Member 1",
* navTo: "Member 1"
* }, {
* styleProperties: couplerStyle,
* endpointStyle: stylesMap.endpoint,
* endpointLabelA: "Endpoint 2",
* endpointLabelZ: "Endpoint 3",
* circuitLabel: "Member 2",
* navTo: "Member 2"
* }, {
* styleProperties: leasedStyle,
* endpointStyle: stylesMap.endpoint,
* endpointLabelA: "Endpoint 3",
* endpointLabelZ: "Endpoint 4",
* circuitLabel: "Member 3",
* navTo: "Member 3"
* }
* ];
* ```
*/
memberList: PropTypes.array.isRequired,
/**
* Described the position of the connection label; accepts **"top"**, **"center"**, or **"bottom"**
*/
connectionLabelPosition: PropTypes.oneOf(["top", "center", "bottom"]),
/**
* The position of the label around the endpoint.
*/
endpointLabelPosition: PropTypes.oneOf([
"left",
"right",
"top",
"topright",
"topleft",
"bottom",
"bottomright",
"bottomleft",
"bottomleftangled",
"bottomrightangled",
"topleftangled",
"toprightangled"
]),
/**
* This is the vertical distance from the center line to offset
* the connection label.
*/
yOffset: PropTypes.number,
/**
* This is the distance from the endpoint that the endpoint
* label will be rendered.
*/
endpointLabelOffset: PropTypes.number,
/**
* The string to display in the top left corner of the diagram
*/
title: PropTypes.string,
/**
* Value that determines whether or not the upper left corner title is displayed
*/
hideTitle: PropTypes.bool,
/**
* Determines if the circuit view is muted. Typically used in
* conjunction with `parentID`
*/
disabled: PropTypes.bool,
/**
* Callback function used to handle clicks.
*/
onSelectionChange: PropTypes.func,
/**
* Value that if provided, will render a small nav arrow that
* when clicked, navigates to that element. Used mainly when we want
* to show a parent / child relationship between two circuits.
*/
parentId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Boolean value that determines if the element uses the onSelectionChange
* change and can be clicked
*/
noNavigate: PropTypes.bool,
/**
* This value is used to determine X coordinates for a square, if you want
* the square to be smaller than the default line width. Overrides the
* margin prop if a square is displayed
*/
squareWidth: PropTypes.number,
/** When the endpoint shape is a `square`, this controls the radius of corners. */
roundedX: PropTypes.number,
/** When the endpoint shape is a `square`, this controls the radius of corners. */
roundedY: PropTypes.number
};
ConcatenatedCircuit.defaultProps = {
width: 851,
height: 250,
disabled: false,
titleOffsetX: 10,
titleOffsetY: 15,
margin: 100,
noNavigate: false,
squareWidth: 25,
roundedX: 5,
roundedY: 5
};