2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
6 * Copyright © 2017 Amdocs
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
21 * ECOMP is a trademark and service mark of AT&T Intellectual Property.
24 import {drag} from 'd3-drag';
25 import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force';
26 import {interpolateNumber} from 'd3-interpolate';
27 import {select, event as currentEvent} from 'd3-selection';
28 import React, {Component, PropTypes} from 'react';
29 import {interval, now} from 'd3-timer';
30 import {zoom, zoomIdentity} from 'd3-zoom';
31 import NodeConstants from './NodeVisualElementConstants.js';
33 import {simulationKeys} from './ForceDefinitions.js';
34 import NodeFactory from './NodeFactory.js';
35 import NodeVisualElementFactory from './NodeVisualElementFactory.js';
37 class ForceDirectedGraph extends Component {
39 viewWidth: PropTypes.number,
40 viewHeight: PropTypes.number,
41 graphData: PropTypes.object,
42 nodeIdKey: PropTypes.string,
43 linkIdKey: PropTypes.string,
44 nodeSelectedCallback: PropTypes.func,
45 nodeButtonSelectedCallback: PropTypes.func,
46 currentlySelectedNodeView: PropTypes.string
49 static defaultProps = {
53 graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {}
57 nodeSelectedCallback: undefined,
58 nodeButtonSelectedCallback: undefined,
59 currentlySelectedNodeView: ''
66 nodes: [], links: [], mainGroupTransform: zoomIdentity
69 this.updateSimulationForce = this.updateSimulationForce.bind(this);
70 this.resetState = this.resetState.bind(this);
71 this.applyBufferDataToState = this.applyBufferDataToState.bind(this);
72 this.createNodePropForState = this.createNodePropForState.bind(this);
73 this.createLinkPropForState = this.createLinkPropForState.bind(this);
74 this.startSimulation = this.startSimulation.bind(this);
75 this.simulationComplete = this.simulationComplete.bind(this);
76 this.simulationTick = this.simulationTick.bind(this);
77 this.nodeSelected = this.nodeSelected.bind(this);
78 this.nodeButtonSelected = this.nodeButtonSelected.bind(this);
79 this.onZoom = this.onZoom.bind(this);
80 this.onGraphDrag = this.onGraphDrag.bind(this);
81 this.onNodeDrag = this.onNodeDrag.bind(this);
82 this.addNodeInterpolator = this.addNodeInterpolator.bind(this);
83 this.runInterpolators = this.runInterpolators.bind(this);
88 this.nodeButtonDatum = [];
89 this.nodeFactory = new NodeFactory();
90 this.visualElementFactory = new NodeVisualElementFactory();
92 this.isGraphMounted = false;
94 this.listenerGraphCounter = -1;
95 this.nodeIndexTracker = new Map();
96 this.interpolators = new Map();
97 this.areInterpolationsRunning = false;
99 this.newNodeSelected = true;
100 this.currentlySelectedNodeButton = undefined;
102 this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
103 this.intervalTimer.stop();
105 this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
106 this.interpolationTimer.stop();
108 this.simulation = forceSimulation();
109 this.simulation.on('end', this.simulationComplete);
110 this.simulation.stop();
116 if(currentEvent.deltaY < 0 ) {
119 return -deltaY * (deltaMode ? 120 : 1) / 1500;
124 zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]).wheelDelta(myDelta);;
125 this.svgZoom.clickDistance(2);
126 this.nodeDrag = drag().clickDistance(2);
128 this.updateSimulationForce();
129 // Temporary code until backend supports NOT displaying the button in the response.
130 if(props.dataOverlayButtons.length === 1) {
131 this.hideButton = true;
133 this.hideButton = false;
136 if (props.graphData) {
137 if (props.graphData.graphCounter !== -1) {
138 this.startSimulation(props.graphData, props.currentlySelectedNodeView, props.dataOverlayButtons);
143 componentDidMount() {
144 this.isGraphMounted = true;
147 componentWillReceiveProps(nextProps) {
148 if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
149 this.listenerGraphCounter = this.props.graphData.graphCounter;
150 this.newNodeSelected = true;
152 this.startSimulation(nextProps.graphData, nextProps.currentlySelectedNodeView, nextProps.dataOverlayButtons);
156 componentDidUpdate(prevProps) {
157 let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
158 this.props.graphData.graphCounter);
159 let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
160 let nodeCount = this.state.nodes.length;
163 if (hasNewGraphDataRendered && shouldAttachListeners) {
164 let nodes = select('.fdgMainSvg').select('.fdgMainG')
165 .selectAll('.aai-entity-node')
166 .data(this.nodeDatum);
168 nodes.on('click', (d) => {
169 this.nodeSelected(d);
172 nodes.call(this.nodeDrag.on('drag', (d) => {
173 let xAndY = [currentEvent.x, currentEvent.y];
174 this.onNodeDrag(d, xAndY);
177 let mainSVG = select('.fdgMainSvg');
178 let mainView = mainSVG.select('.fdgMainView');
179 this.svgZoom.transform(mainSVG, zoomIdentity);
180 this.svgZoom.transform(mainView, zoomIdentity);
182 mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
183 this.onZoom(currentEvent.transform);
184 })).on('dblclick.zoom', null); // Ignore the double-click zoom event
186 this.listenerGraphCounter = this.props.graphData.graphCounter;
189 if (this.newNodeSelected) {
190 let nodeButtons = select('.fdgMainSvg').select('.fdgMainG')
191 .selectAll('.aai-entity-node')
192 .selectAll('.node-button')
193 .data(this.nodeButtonDatum);
194 if (!nodeButtons.empty()) {
195 nodeButtons.on('click', (d) => {
196 this.nodeButtonSelected(d);
198 if (hasNewGraphDataRendered && shouldAttachListeners) {
199 this.newNodeSelected = false;
206 componentWillUnmount() {
207 this.isGraphMounted = false;
209 let nodes = select('.fdgMainSvg').select('.fdgMainG')
210 .selectAll('.aai-entity-node');
211 let nodeButtons = nodes.selectAll('.node-button');
213 nodes.on('click', null);
214 nodeButtons.on('click', null);
216 let mainSVG = select('.fdgMainSvg');
218 mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
219 mainSVG.call(drag().on('drag', null));
222 updateSimulationForce() {
223 this.simulation.force('link', forceLink());
224 this.simulation.force('link').id((d) => {
227 this.simulation.force('link').strength(0.3);
228 this.simulation.force('link').distance(100);
230 this.simulation.force('charge', forceManyBody());
231 this.simulation.force('charge').strength(-1250);
232 this.simulation.alpha(1);
234 this.simulation.force('center',
235 forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
239 if (this.isGraphMounted) {
240 this.setState(() => {
242 mainGroupTransform: zoomIdentity,
249 applyBufferDataToState() {
250 this.nodeIndexTracker.clear();
253 this.nodeBuffer.map((node, i) => {
254 let nodeProps = this.createNodePropForState(node);
256 if (nodeProps.meta.nodeMeta.className ===
257 NodeConstants.SELECTED_NODE_CLASS_NAME ||
258 nodeProps.meta.nodeMeta.className ===
259 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
261 this.nodeButtonDatum[0].data = nodeProps.meta;
263 if(this.nodeButtonDatum.length > 1) {
264 this.nodeButtonDatum[1].data = nodeProps.meta;
267 buttons: [this.nodeButtonDatum[0].isSelected, this.nodeButtonDatum[1].isSelected]
272 buttons: [this.nodeButtonDatum[0].isSelected]
277 newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps, this.hideButton));
280 this.nodeIndexTracker.set(node.id, i);
284 this.linkBuffer.map((link) => {
286 let linkProps = this.createLinkPropForState(link);
287 newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
290 if (this.isGraphMounted) {
291 this.setState(() => {
293 nodes: newNodes, links: newLinks
299 createNodePropForState(nodeData) {
302 key: nodeData.id, x: nodeData.x, y: nodeData.y
309 createLinkPropForState(linkData) {
311 className: 'aai-entity-link',
312 x1: linkData.source.x,
313 y1: linkData.source.y,
314 x2: linkData.target.x,
315 y2: linkData.target.y
319 startSimulation(graphData, currentView, overlayButtons) {
321 this.nodeFactory.setNodeMeta(graphData.graphMeta);
323 // Experiment with removing length = 0... might not be needed as new array
324 // assignment will likely destroy old reference
325 this.nodeBuffer.length = 0;
326 this.nodeBuffer = Array.from(graphData.nodeDataArray);
327 this.linkBuffer.length = 0;
328 this.linkBuffer = Array.from(graphData.linkDataArray);
329 this.nodeDatum.length = 0;
330 this.nodeDatum = Array.from(graphData.nodeDataArray);
332 this.nodeButtonDatum.length = 0;
334 let isNodeDetailsSelected = (currentView ===
338 this.nodeButtonDatum.push({
339 name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected, overlayName: overlayButtons[0]
342 if(overlayButtons.length > 1 ) {
343 let isSecondButtonSelected = (currentView === overlayButtons[1]);
345 this.nodeButtonDatum.push({
346 name: NodeConstants.ICON_TRIANGLE_WARNING, isSelected: isSecondButtonSelected, overlayName: overlayButtons[1]
351 if (isNodeDetailsSelected) {
352 this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
354 this.currentlySelectedNodeButton = NodeConstants.ICON_TRIANGLE_WARNING;
357 this.updateSimulationForce();
359 this.simulation.nodes(this.nodeBuffer);
360 this.simulation.force('link').links(this.linkBuffer);
361 this.simulation.on('tick', this.simulationTick);
362 this.simulation.restart();
365 simulationComplete() {
366 this.intervalTimer.stop();
367 this.applyBufferDataToState();
371 this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
372 this.simulation.on('tick', null);
375 nodeSelected(datum) {
376 if (this.props.nodeSelectedCallback) {
377 this.props.nodeSelectedCallback(datum);
380 let didUpdateNew = false;
381 let didUpdatePrevious = false;
382 let isSameNodeSelected = true;
384 // Check to see if a default node was previously selected
385 let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
386 .selectAll('.aai-entity-node')
387 .filter('.selected-node');
388 if (!selectedDefaultNode.empty()) {
389 if (selectedDefaultNode.datum().id !== datum.id) {
390 this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
391 NodeConstants.GENERAL_NODE_CLASS_NAME;
392 didUpdatePrevious = true;
393 isSameNodeSelected = false;
397 // Check to see if a searched node was previously selected
398 let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
399 .selectAll('.aai-entity-node')
400 .filter('.selected-search-node');
401 if (!selectedSearchedNode.empty()) {
402 if (selectedSearchedNode.datum().id !== datum.id) {
403 this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
404 NodeConstants.SEARCHED_NODE_CLASS_NAME;
405 didUpdatePrevious = true;
406 isSameNodeSelected = false;
410 if (!isSameNodeSelected) {
411 let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
412 .selectAll('.aai-entity-node')
414 return (datum.id === d.id);
416 if (!newlySelectedNode.empty()) {
417 if (newlySelectedNode.datum().nodeMeta.searchTarget) {
418 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
419 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
421 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
422 NodeConstants.SELECTED_NODE_CLASS_NAME;
428 if (didUpdatePrevious && didUpdateNew) {
429 this.newNodeSelected = true;
430 this.applyBufferDataToState();
434 nodeButtonSelected(datum) {
435 if (this.props.nodeButtonSelectedCallback) {
436 let buttonClickEvent = {
437 buttonId: datum.overlayName
439 this.props.nodeButtonSelectedCallback(buttonClickEvent);
442 if (this.currentlySelectedNodeButton !== datum.name) {
443 if (datum.name === this.nodeButtonDatum[0].name) {
444 this.nodeButtonDatum[0].isSelected = true;
445 this.nodeButtonDatum[1].isSelected = false;
447 if (datum.name === this.nodeButtonDatum[1].name) {
448 this.nodeButtonDatum[0].isSelected = false;
449 this.nodeButtonDatum[1].isSelected = true;
451 this.currentlySelectedNodeButton = datum.name;
452 this.applyBufferDataToState();
456 onZoom(eventTransform) {
457 if (this.isGraphMounted) {
458 this.setState(() => {
460 mainGroupTransform: eventTransform
466 onGraphDrag(xAndYCoords) {
467 let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
468 let oldTransform = this.state.mainGroupTransform;
469 if (this.isGraphMounted) {
470 this.setState(() => {
472 ...oldTransform, translate
478 onNodeDrag(datum, xAndYCoords) {
479 let nodeIndex = this.nodeIndexTracker.get(datum.id);
480 if (this.nodeBuffer[nodeIndex]) {
481 this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
482 this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
483 this.applyBufferDataToState();
487 addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
488 let numberInterpolator = interpolateNumber(startingValue, endingValue);
490 let interpolationObject = {
491 nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
493 this.interpolators.set(nodeId, interpolationObject);
495 if (!this.areInterpolationsRunning) {
496 this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
497 this.areInterpolationsRunning = true;
502 // If we have no more interpolators to run then shut'r down!
503 if (this.interpolators.size === 0) {
504 this.interpolationTimer.stop();
505 this.areInterpolationsRunning = false;
508 let iterpolatorsComplete = [];
509 // Apply interpolation values
510 this.interpolators.forEach((interpolator) => {
511 let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
513 let elapsedTime = now() - interpolator.timeCreated;
514 // Normalize t as D3's interpolateNumber needs a value between 0 and 1
515 let t = elapsedTime / interpolator.duration;
518 iterpolatorsComplete.push(interpolator.nodeId);
520 this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
524 // Remove any interpolators that are complete
525 if (iterpolatorsComplete.length > 0) {
526 for (let i = 0; i < iterpolatorsComplete.length; i++) {
527 this.interpolators.delete(iterpolatorsComplete[i]);
531 this.applyBufferDataToState();
535 // We will be using these values veru shortly, commenting out for eslint
536 // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
537 let {nodes, links, mainGroupTransform} = this.state;
540 <div className='ts-force-selected-graph'>
541 <svg className={'fdgMainSvg'} width='100%' height='100%'>
542 <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
543 height='99%' fill='none'/>
544 <filter id='selected-node-drop-shadow'>
545 <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
546 <feOffset dx='-1' dy='1' result='offsetblur'/>
547 <feFlood floodColor='rgba(0,0,0,0.5)'/>
548 <feComposite in2='offsetblur' operator='in'/>
551 <feMergeNode in='SourceGraphic'/>
554 <g className={'fdgMainG'} transform={mainGroupTransform}>
563 static graphCounter = 0;
565 static generateNewProps(nodeArray, linkArray, metaData) {
566 ForceDirectedGraph.graphCounter += 1;
568 graphCounter: ForceDirectedGraph.graphCounter,
569 nodeDataArray: nodeArray,
570 linkDataArray: linkArray,
576 export default ForceDirectedGraph;