diff options
Diffstat (limited to 'src/generic-components/graph/ForceDirectedGraph.jsx')
-rw-r--r-- | src/generic-components/graph/ForceDirectedGraph.jsx | 262 |
1 files changed, 166 insertions, 96 deletions
diff --git a/src/generic-components/graph/ForceDirectedGraph.jsx b/src/generic-components/graph/ForceDirectedGraph.jsx index 872b2d5..00c2575 100644 --- a/src/generic-components/graph/ForceDirectedGraph.jsx +++ b/src/generic-components/graph/ForceDirectedGraph.jsx @@ -1,25 +1,28 @@ /* - * ============LICENSE_START======================================================= - * org.onap.aai - * ================================================================================ - * Copyright © 2017 AT&T Intellectual Property. All rights reserved. + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. * Copyright © 2017 Amdocs - * ================================================================================ + * All rights reserved. + * ============================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * ============LICENSE_END========================================================= + * ============LICENSE_END===================================================== * - * ECOMP is a trademark and service mark of AT&T Intellectual Property. + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. */ + import {drag} from 'd3-drag'; import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force'; import {interpolateNumber} from 'd3-interpolate'; @@ -44,7 +47,7 @@ class ForceDirectedGraph extends Component { nodeButtonSelectedCallback: PropTypes.func, currentlySelectedNodeView: PropTypes.string }; - + static defaultProps = { viewWidth: 0, viewHeight: 0, @@ -57,16 +60,16 @@ class ForceDirectedGraph extends Component { nodeButtonSelectedCallback: undefined, currentlySelectedNodeView: '' }; - + constructor(props) { super(props); - + this.state = { nodes: [], links: [], mainGroupTransform: zoomIdentity }; - + this.updateSimulationForce = this.updateSimulationForce.bind(this); - this.resetTransform = this.resetTransform.bind(this); + this.resetState = this.resetState.bind(this); this.applyBufferDataToState = this.applyBufferDataToState.bind(this); this.createNodePropForState = this.createNodePropForState.bind(this); this.createLinkPropForState = this.createLinkPropForState.bind(this); @@ -74,118 +77,138 @@ class ForceDirectedGraph extends Component { this.simulationComplete = this.simulationComplete.bind(this); this.simulationTick = this.simulationTick.bind(this); this.nodeSelected = this.nodeSelected.bind(this); + this.nodeButtonSelected = this.nodeButtonSelected.bind(this); this.onZoom = this.onZoom.bind(this); this.onGraphDrag = this.onGraphDrag.bind(this); this.onNodeDrag = this.onNodeDrag.bind(this); this.addNodeInterpolator = this.addNodeInterpolator.bind(this); this.runInterpolators = this.runInterpolators.bind(this); - + this.nodeBuffer = []; this.linkBuffer = []; this.nodeDatum = []; this.nodeButtonDatum = []; this.nodeFactory = new NodeFactory(); this.visualElementFactory = new NodeVisualElementFactory(); - + this.isGraphMounted = false; - + this.listenerGraphCounter = -1; this.nodeIndexTracker = new Map(); this.interpolators = new Map(); this.areInterpolationsRunning = false; - + this.newNodeSelected = true; this.currentlySelectedNodeButton = undefined; - + this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL); this.intervalTimer.stop(); - + this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL); this.interpolationTimer.stop(); - + this.simulation = forceSimulation(); this.simulation.on('end', this.simulationComplete); this.simulation.stop(); - + this.svgZoom = zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]); this.svgZoom.clickDistance(2); this.nodeDrag = drag().clickDistance(2); - + this.updateSimulationForce(); - + // Temporary code until backend supports NOT displaying the button in the response. + if(props.dataOverlayButtons.length === 1) { + this.hideButton = true; + } else { + this.hideButton = false; + } if (props.graphData) { if (props.graphData.graphCounter !== -1) { - this.startSimulation(props.graphData); + this.startSimulation(props.graphData, props.currentlySelectedNodeView, props.dataOverlayButtons); } } } - + componentDidMount() { this.isGraphMounted = true; } - + componentWillReceiveProps(nextProps) { if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) { this.listenerGraphCounter = this.props.graphData.graphCounter; this.newNodeSelected = true; - this.resetTransform(); - this.startSimulation(nextProps.graphData); + this.resetState(); + this.startSimulation(nextProps.graphData, nextProps.currentlySelectedNodeView, nextProps.dataOverlayButtons); } } - + + componentDidUpdate(prevProps) { let hasNewGraphDataRendered = (prevProps.graphData.graphCounter === this.props.graphData.graphCounter); let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter); let nodeCount = this.state.nodes.length; - + if (nodeCount > 0) { if (hasNewGraphDataRendered && shouldAttachListeners) { - let nodes = select('.fdgMainSvg').select('.fdgMainG') .selectAll('.aai-entity-node') .data(this.nodeDatum); - nodes.on('click', (d) => { this.nodeSelected(d); }); - + nodes.call(this.nodeDrag.on('drag', (d) => { let xAndY = [currentEvent.x, currentEvent.y]; this.onNodeDrag(d, xAndY); })); - + let mainSVG = select('.fdgMainSvg'); let mainView = mainSVG.select('.fdgMainView'); this.svgZoom.transform(mainSVG, zoomIdentity); this.svgZoom.transform(mainView, zoomIdentity); - + mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning this.onZoom(currentEvent.transform); })).on('dblclick.zoom', null); // Ignore the double-click zoom event - + this.listenerGraphCounter = this.props.graphData.graphCounter; } + + if (this.newNodeSelected) { + let nodeButtons = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node') + .selectAll('.node-button') + .data(this.nodeButtonDatum); + if (!nodeButtons.empty()) { + nodeButtons.on('click', (d) => { + this.nodeButtonSelected(d); + }); + if (hasNewGraphDataRendered && shouldAttachListeners) { + this.newNodeSelected = false; + } + } + } } } - + componentWillUnmount() { this.isGraphMounted = false; - + let nodes = select('.fdgMainSvg').select('.fdgMainG') .selectAll('.aai-entity-node'); let nodeButtons = nodes.selectAll('.node-button'); - + nodes.on('click', null); nodeButtons.on('click', null); - + let mainSVG = select('.fdgMainSvg'); - + mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null); mainSVG.call(drag().on('drag', null)); } - + updateSimulationForce() { this.simulation.force('link', forceLink()); this.simulation.force('link').id((d) => { @@ -193,55 +216,65 @@ class ForceDirectedGraph extends Component { }); this.simulation.force('link').strength(0.3); this.simulation.force('link').distance(100); - + this.simulation.force('charge', forceManyBody()); this.simulation.force('charge').strength(-1250); this.simulation.alpha(1); - + this.simulation.force('center', forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2)); } - - resetTransform() { + + resetState() { if (this.isGraphMounted) { this.setState(() => { return { - mainGroupTransform: zoomIdentity + mainGroupTransform: zoomIdentity, + nodes: [], links: [] }; }); } } - + applyBufferDataToState() { this.nodeIndexTracker.clear(); - + let newNodes = []; this.nodeBuffer.map((node, i) => { let nodeProps = this.createNodePropForState(node); - - if (nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_NODE_CLASS_NAME || - nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) { - + + if (nodeProps.meta.nodeMeta.className === + NodeConstants.SELECTED_NODE_CLASS_NAME || + nodeProps.meta.nodeMeta.className === + NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) { + this.nodeButtonDatum[0].data = nodeProps.meta; - - nodeProps = { - ...nodeProps, - buttons: [this.nodeButtonDatum[0].isSelected] - }; + if(this.nodeButtonDatum.length > 1) { + this.nodeButtonDatum[1].data = nodeProps.meta; + nodeProps = { + ...nodeProps, + buttons: [this.nodeButtonDatum[0].isSelected, this.nodeButtonDatum[1].isSelected] + }; + } else { + nodeProps = { + ...nodeProps, + buttons: [this.nodeButtonDatum[0].isSelected] + }; + } } - - newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps)); - + + newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps, this.hideButton)); + this.nodeIndexTracker.set(node.id, i); }); - + let newLinks = []; this.linkBuffer.map((link) => { let key = link.id; let linkProps = this.createLinkPropForState(link); newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key)); }); - + if (this.isGraphMounted) { this.setState(() => { return { @@ -250,7 +283,7 @@ class ForceDirectedGraph extends Component { }); } } - + createNodePropForState(nodeData) { return { renderProps: { @@ -260,7 +293,7 @@ class ForceDirectedGraph extends Component { } }; } - + createLinkPropForState(linkData) { return { className: 'aai-entity-link', @@ -270,10 +303,10 @@ class ForceDirectedGraph extends Component { y2: linkData.target.y }; } - - startSimulation(graphData) { + + startSimulation(graphData, currentView, overlayButtons) { this.nodeFactory.setNodeMeta(graphData.graphMeta); - + // Experiment with removing length = 0... might not be needed as new array // assignment will likely destroy old reference this.nodeBuffer.length = 0; @@ -282,45 +315,60 @@ class ForceDirectedGraph extends Component { this.linkBuffer = Array.from(graphData.linkDataArray); this.nodeDatum.length = 0; this.nodeDatum = Array.from(graphData.nodeDataArray); - + this.nodeButtonDatum.length = 0; - - let isNodeDetailsSelected = true; + + let isNodeDetailsSelected = (currentView === + overlayButtons[0] || + currentView === + ''); this.nodeButtonDatum.push({ - name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected + name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected, overlayName: overlayButtons[0] }); - + + + if(overlayButtons.length > 1 ) { + let isSecondButtonSelected = (currentView === overlayButtons[1]); + + this.nodeButtonDatum.push({ + name: NodeConstants.ICON_TRIANGLE_WARNING, isSelected: isSecondButtonSelected, overlayName: overlayButtons[1] + }); + } + + if (isNodeDetailsSelected) { this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES; + } else { + this.currentlySelectedNodeButton = NodeConstants.ICON_TRIANGLE_WARNING; } - + this.updateSimulationForce(); - + this.simulation.nodes(this.nodeBuffer); this.simulation.force('link').links(this.linkBuffer); this.simulation.on('tick', this.simulationTick); this.simulation.restart(); } - + simulationComplete() { this.intervalTimer.stop(); this.applyBufferDataToState(); } - + simulationTick() { this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL); this.simulation.on('tick', null); } - + nodeSelected(datum) { if (this.props.nodeSelectedCallback) { this.props.nodeSelectedCallback(datum); } - + let didUpdateNew = false; let didUpdatePrevious = false; let isSameNodeSelected = true; - + // Check to see if a default node was previously selected let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG') .selectAll('.aai-entity-node') @@ -333,7 +381,7 @@ class ForceDirectedGraph extends Component { isSameNodeSelected = false; } } - + // Check to see if a searched node was previously selected let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG') .selectAll('.aai-entity-node') @@ -346,7 +394,7 @@ class ForceDirectedGraph extends Component { isSameNodeSelected = false; } } - + if (!isSameNodeSelected) { let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG') .selectAll('.aai-entity-node') @@ -354,7 +402,7 @@ class ForceDirectedGraph extends Component { return (datum.id === d.id); }); if (!newlySelectedNode.empty()) { - + if (newlySelectedNode.datum().nodeMeta.searchTarget) { this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className = NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME; @@ -365,13 +413,35 @@ class ForceDirectedGraph extends Component { didUpdateNew = true; } } - + if (didUpdatePrevious && didUpdateNew) { this.newNodeSelected = true; this.applyBufferDataToState(); } } + nodeButtonSelected(datum) { + if (this.props.nodeButtonSelectedCallback) { + let buttonClickEvent = { + buttonId: datum.overlayName + }; + this.props.nodeButtonSelectedCallback(buttonClickEvent); + } + + if (this.currentlySelectedNodeButton !== datum.name) { + if (datum.name === this.nodeButtonDatum[0].name) { + this.nodeButtonDatum[0].isSelected = true; + this.nodeButtonDatum[1].isSelected = false; + } + if (datum.name === this.nodeButtonDatum[1].name) { + this.nodeButtonDatum[0].isSelected = false; + this.nodeButtonDatum[1].isSelected = true; + } + this.currentlySelectedNodeButton = datum.name; + this.applyBufferDataToState(); + } + } + onZoom(eventTransform) { if (this.isGraphMounted) { this.setState(() => { @@ -381,7 +451,7 @@ class ForceDirectedGraph extends Component { }); } } - + onGraphDrag(xAndYCoords) { let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`; let oldTransform = this.state.mainGroupTransform; @@ -393,7 +463,7 @@ class ForceDirectedGraph extends Component { }); } } - + onNodeDrag(datum, xAndYCoords) { let nodeIndex = this.nodeIndexTracker.get(datum.id); if (this.nodeBuffer[nodeIndex]) { @@ -402,7 +472,7 @@ class ForceDirectedGraph extends Component { this.applyBufferDataToState(); } } - + addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) { let numberInterpolator = interpolateNumber(startingValue, endingValue); let timeNow = now(); @@ -410,20 +480,20 @@ class ForceDirectedGraph extends Component { nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator }; this.interpolators.set(nodeId, interpolationObject); - + if (!this.areInterpolationsRunning) { this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL); this.areInterpolationsRunning = true; } } - + runInterpolators() { // If we have no more interpolators to run then shut'r down! if (this.interpolators.size === 0) { this.interpolationTimer.stop(); this.areInterpolationsRunning = false; } - + let iterpolatorsComplete = []; // Apply interpolation values this.interpolators.forEach((interpolator) => { @@ -439,22 +509,22 @@ class ForceDirectedGraph extends Component { this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t); } }); - + // Remove any interpolators that are complete if (iterpolatorsComplete.length > 0) { for (let i = 0; i < iterpolatorsComplete.length; i++) { this.interpolators.delete(iterpolatorsComplete[i]); } } - + this.applyBufferDataToState(); } - + render() { // We will be using these values veru shortly, commenting out for eslint // reasons so we can build for PV let {viewWidth, viewHeight} = this.props; let {nodes, links, mainGroupTransform} = this.state; - + return ( <div className='ts-force-selected-graph'> <svg className={'fdgMainSvg'} width='100%' height='100%'> @@ -478,9 +548,9 @@ class ForceDirectedGraph extends Component { </div> ); } - + static graphCounter = 0; - + static generateNewProps(nodeArray, linkArray, metaData) { ForceDirectedGraph.graphCounter += 1; return { |