2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6 * Copyright © 2017-2018 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 import {drag} from 'd3-drag';
22 import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force';
23 import {interpolateNumber} from 'd3-interpolate';
24 import {select, event as currentEvent} from 'd3-selection';
25 import React, {Component} from 'react';
26 import { PropTypes } from 'prop-types';
27 import {interval, now} from 'd3-timer';
28 import {zoom, zoomIdentity} from 'd3-zoom';
29 import NodeConstants from './NodeVisualElementConstants.js';
31 import {simulationKeys} from './ForceDefinitions.js';
32 import NodeFactory from './NodeFactory.js';
33 import NodeVisualElementFactory from './NodeVisualElementFactory.js';
35 class ForceDirectedGraph extends Component {
37 viewWidth: PropTypes.number,
38 viewHeight: PropTypes.number,
39 graphData: PropTypes.object,
40 nodeIdKey: PropTypes.string,
41 linkIdKey: PropTypes.string,
42 nodeSelectedCallback: PropTypes.func,
43 nodeButtonSelectedCallback: PropTypes.func,
44 currentlySelectedNodeView: PropTypes.string
47 static defaultProps = {
51 graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {}
55 nodeSelectedCallback: undefined,
56 nodeButtonSelectedCallback: undefined,
57 currentlySelectedNodeView: ''
64 nodes: [], links: [], mainGroupTransform: zoomIdentity
67 this.updateSimulationForce = this.updateSimulationForce.bind(this);
68 this.resetState = this.resetState.bind(this);
69 this.applyBufferDataToState = this.applyBufferDataToState.bind(this);
70 this.createNodePropForState = this.createNodePropForState.bind(this);
71 this.createLinkPropForState = this.createLinkPropForState.bind(this);
72 this.startSimulation = this.startSimulation.bind(this);
73 this.simulationComplete = this.simulationComplete.bind(this);
74 this.simulationTick = this.simulationTick.bind(this);
75 this.nodeSelected = this.nodeSelected.bind(this);
76 this.nodeButtonSelected = this.nodeButtonSelected.bind(this);
77 this.onZoom = this.onZoom.bind(this);
78 this.onGraphDrag = this.onGraphDrag.bind(this);
79 this.onNodeDrag = this.onNodeDrag.bind(this);
80 this.addNodeInterpolator = this.addNodeInterpolator.bind(this);
81 this.runInterpolators = this.runInterpolators.bind(this);
86 this.nodeButtonDatum = [];
87 this.nodeFactory = new NodeFactory();
88 this.visualElementFactory = new NodeVisualElementFactory();
90 this.isGraphMounted = false;
92 this.listenerGraphCounter = -1;
93 this.nodeIndexTracker = new Map();
94 this.interpolators = new Map();
95 this.areInterpolationsRunning = false;
97 this.newNodeSelected = true;
98 this.currentlySelectedNodeButton = undefined;
100 this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
101 this.intervalTimer.stop();
103 this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
104 this.interpolationTimer.stop();
106 this.simulation = forceSimulation();
107 this.simulation.on('end', this.simulationComplete);
108 this.simulation.stop();
114 if(currentEvent.deltaY < 0 ) {
117 return -deltaY * (deltaMode ? 120 : 1) / 1500;
122 zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]).wheelDelta(myDelta);;
123 this.svgZoom.clickDistance(2);
124 this.nodeDrag = drag().clickDistance(2);
126 this.updateSimulationForce();
127 // Temporary code until backend supports NOT displaying the button in the response.
128 if(props.dataOverlayButtons.length === 1) {
129 this.hideButton = true;
131 this.hideButton = false;
134 if (props.graphData) {
135 if (props.graphData.graphCounter !== -1) {
136 this.startSimulation(props.graphData, props.currentlySelectedNodeView, props.dataOverlayButtons);
141 componentDidMount() {
142 this.isGraphMounted = true;
145 componentWillReceiveProps(nextProps) {
146 if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
147 this.listenerGraphCounter = this.props.graphData.graphCounter;
148 this.newNodeSelected = true;
150 this.startSimulation(nextProps.graphData, nextProps.currentlySelectedNodeView, nextProps.dataOverlayButtons);
154 componentDidUpdate(prevProps) {
155 let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
156 this.props.graphData.graphCounter);
157 let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
158 let nodeCount = this.state.nodes.length;
161 if (hasNewGraphDataRendered && shouldAttachListeners) {
162 let nodes = select('.fdgMainSvg').select('.fdgMainG')
163 .selectAll('.aai-entity-node')
164 .data(this.nodeDatum);
166 nodes.on('click', (d) => {
167 this.nodeSelected(d);
170 nodes.call(this.nodeDrag.on('drag', (d) => {
171 let xAndY = [currentEvent.x, currentEvent.y];
172 this.onNodeDrag(d, xAndY);
175 let mainSVG = select('.fdgMainSvg');
176 let mainView = mainSVG.select('.fdgMainView');
177 this.svgZoom.transform(mainSVG, zoomIdentity);
178 this.svgZoom.transform(mainView, zoomIdentity);
180 mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
181 this.onZoom(currentEvent.transform);
182 })).on('dblclick.zoom', null); // Ignore the double-click zoom event
184 this.listenerGraphCounter = this.props.graphData.graphCounter;
187 if (this.newNodeSelected) {
188 let nodeButtons = select('.fdgMainSvg').select('.fdgMainG')
189 .selectAll('.aai-entity-node')
190 .selectAll('.node-button')
191 .data(this.nodeButtonDatum);
192 if (!nodeButtons.empty()) {
193 nodeButtons.on('click', (d) => {
194 this.nodeButtonSelected(d);
196 if (hasNewGraphDataRendered && shouldAttachListeners) {
197 this.newNodeSelected = false;
204 componentWillUnmount() {
205 this.isGraphMounted = false;
207 let nodes = select('.fdgMainSvg').select('.fdgMainG')
208 .selectAll('.aai-entity-node');
209 let nodeButtons = nodes.selectAll('.node-button');
211 nodes.on('click', null);
212 nodeButtons.on('click', null);
214 let mainSVG = select('.fdgMainSvg');
216 mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
217 mainSVG.call(drag().on('drag', null));
220 updateSimulationForce() {
221 this.simulation.force('link', forceLink());
222 this.simulation.force('link').id((d) => {
225 this.simulation.force('link').strength(0.3);
226 this.simulation.force('link').distance(100);
228 this.simulation.force('charge', forceManyBody());
229 this.simulation.force('charge').strength(-1250);
230 this.simulation.alpha(1);
232 this.simulation.force('center',
233 forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
237 if (this.isGraphMounted) {
238 this.setState(() => {
240 mainGroupTransform: zoomIdentity,
247 applyBufferDataToState() {
248 this.nodeIndexTracker.clear();
251 this.nodeBuffer.map((node, i) => {
252 let nodeProps = this.createNodePropForState(node);
254 if (nodeProps.meta.nodeMeta.className ===
255 NodeConstants.SELECTED_NODE_CLASS_NAME ||
256 nodeProps.meta.nodeMeta.className ===
257 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
259 this.nodeButtonDatum[0].data = nodeProps.meta;
261 if(this.nodeButtonDatum.length > 1) {
262 this.nodeButtonDatum[1].data = nodeProps.meta;
265 buttons: [this.nodeButtonDatum[0].isSelected, this.nodeButtonDatum[1].isSelected]
270 buttons: [this.nodeButtonDatum[0].isSelected]
275 newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps, this.hideButton));
278 this.nodeIndexTracker.set(node.id, i);
282 this.linkBuffer.map((link) => {
284 let linkProps = this.createLinkPropForState(link);
285 newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
288 if (this.isGraphMounted) {
289 this.setState(() => {
291 nodes: newNodes, links: newLinks
297 createNodePropForState(nodeData) {
300 key: nodeData.id, x: nodeData.x, y: nodeData.y
307 createLinkPropForState(linkData) {
309 className: 'aai-entity-link',
310 x1: linkData.source.x,
311 y1: linkData.source.y,
312 x2: linkData.target.x,
313 y2: linkData.target.y
317 startSimulation(graphData, currentView, overlayButtons) {
319 this.nodeFactory.setNodeMeta(graphData.graphMeta);
321 // Experiment with removing length = 0... might not be needed as new array
322 // assignment will likely destroy old reference
323 this.nodeBuffer.length = 0;
324 this.nodeBuffer = Array.from(graphData.nodeDataArray);
325 this.linkBuffer.length = 0;
326 this.linkBuffer = Array.from(graphData.linkDataArray);
327 this.nodeDatum.length = 0;
328 this.nodeDatum = Array.from(graphData.nodeDataArray);
330 this.nodeButtonDatum.length = 0;
332 let isNodeDetailsSelected = (currentView ===
336 this.nodeButtonDatum.push({
337 name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected, overlayName: overlayButtons[0]
340 if(overlayButtons.length > 1 ) {
341 let isSecondButtonSelected = (currentView === overlayButtons[1]);
343 this.nodeButtonDatum.push({
344 name: NodeConstants.ICON_TRIANGLE_WARNING, isSelected: isSecondButtonSelected, overlayName: overlayButtons[1]
349 if (isNodeDetailsSelected) {
350 this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
352 this.currentlySelectedNodeButton = NodeConstants.ICON_TRIANGLE_WARNING;
355 this.updateSimulationForce();
357 this.simulation.nodes(this.nodeBuffer);
358 this.simulation.force('link').links(this.linkBuffer);
359 this.simulation.on('tick', this.simulationTick);
360 this.simulation.restart();
363 simulationComplete() {
364 this.intervalTimer.stop();
365 this.applyBufferDataToState();
369 this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
370 this.simulation.on('tick', null);
373 nodeSelected(datum) {
374 if (this.props.nodeSelectedCallback) {
375 this.props.nodeSelectedCallback(datum);
378 let didUpdateNew = false;
379 let didUpdatePrevious = false;
380 let isSameNodeSelected = true;
382 // Check to see if a default node was previously selected
383 let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
384 .selectAll('.aai-entity-node')
385 .filter('.selected-node');
386 if (!selectedDefaultNode.empty()) {
387 if (selectedDefaultNode.datum().id !== datum.id) {
388 this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
389 NodeConstants.GENERAL_NODE_CLASS_NAME;
390 didUpdatePrevious = true;
391 isSameNodeSelected = false;
395 // Check to see if a searched node was previously selected
396 let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
397 .selectAll('.aai-entity-node')
398 .filter('.selected-search-node');
399 if (!selectedSearchedNode.empty()) {
400 if (selectedSearchedNode.datum().id !== datum.id) {
401 this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
402 NodeConstants.SEARCHED_NODE_CLASS_NAME;
403 didUpdatePrevious = true;
404 isSameNodeSelected = false;
408 if (!isSameNodeSelected) {
409 let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
410 .selectAll('.aai-entity-node')
412 return (datum.id === d.id);
414 if (!newlySelectedNode.empty()) {
415 if (newlySelectedNode.datum().nodeMeta.searchTarget) {
416 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
417 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
419 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
420 NodeConstants.SELECTED_NODE_CLASS_NAME;
426 if (didUpdatePrevious && didUpdateNew) {
427 this.newNodeSelected = true;
428 this.applyBufferDataToState();
432 nodeButtonSelected(datum) {
433 if (this.props.nodeButtonSelectedCallback) {
434 let buttonClickEvent = {
435 buttonId: datum.overlayName
437 this.props.nodeButtonSelectedCallback(buttonClickEvent);
440 if (this.currentlySelectedNodeButton !== datum.name) {
441 if (datum.name === this.nodeButtonDatum[0].name) {
442 this.nodeButtonDatum[0].isSelected = true;
443 this.nodeButtonDatum[1].isSelected = false;
445 if (datum.name === this.nodeButtonDatum[1].name) {
446 this.nodeButtonDatum[0].isSelected = false;
447 this.nodeButtonDatum[1].isSelected = true;
449 this.currentlySelectedNodeButton = datum.name;
450 this.applyBufferDataToState();
454 onZoom(eventTransform) {
455 if (this.isGraphMounted) {
456 this.setState(() => {
458 mainGroupTransform: eventTransform
464 onGraphDrag(xAndYCoords) {
465 let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
466 let oldTransform = this.state.mainGroupTransform;
467 if (this.isGraphMounted) {
468 this.setState(() => {
470 ...oldTransform, translate
476 onNodeDrag(datum, xAndYCoords) {
477 let nodeIndex = this.nodeIndexTracker.get(datum.id);
478 if (this.nodeBuffer[nodeIndex]) {
479 this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
480 this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
481 this.applyBufferDataToState();
485 addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
486 let numberInterpolator = interpolateNumber(startingValue, endingValue);
488 let interpolationObject = {
489 nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
491 this.interpolators.set(nodeId, interpolationObject);
493 if (!this.areInterpolationsRunning) {
494 this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
495 this.areInterpolationsRunning = true;
500 // If we have no more interpolators to run then shut'r down!
501 if (this.interpolators.size === 0) {
502 this.interpolationTimer.stop();
503 this.areInterpolationsRunning = false;
506 let iterpolatorsComplete = [];
507 // Apply interpolation values
508 this.interpolators.forEach((interpolator) => {
509 let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
511 let elapsedTime = now() - interpolator.timeCreated;
512 // Normalize t as D3's interpolateNumber needs a value between 0 and 1
513 let t = elapsedTime / interpolator.duration;
516 iterpolatorsComplete.push(interpolator.nodeId);
518 this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
522 // Remove any interpolators that are complete
523 if (iterpolatorsComplete.length > 0) {
524 for (let i = 0; i < iterpolatorsComplete.length; i++) {
525 this.interpolators.delete(iterpolatorsComplete[i]);
529 this.applyBufferDataToState();
533 // We will be using these values veru shortly, commenting out for eslint
534 // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
535 let {nodes, links, mainGroupTransform} = this.state;
538 <div className='ts-force-selected-graph'>
539 <svg className={'fdgMainSvg'} width='100%' height='100%'>
540 <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
541 height='99%' fill='none'/>
542 <filter id='selected-node-drop-shadow'>
543 <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
544 <feOffset dx='-1' dy='1' result='offsetblur'/>
545 <feFlood floodColor='rgba(0,0,0,0.5)'/>
546 <feComposite in2='offsetblur' operator='in'/>
549 <feMergeNode in='SourceGraphic'/>
552 <g className={'fdgMainG'} transform={mainGroupTransform}>
561 static graphCounter = 0;
563 static generateNewProps(nodeArray, linkArray, metaData) {
564 ForceDirectedGraph.graphCounter += 1;
566 graphCounter: ForceDirectedGraph.graphCounter,
567 nodeDataArray: nodeArray,
568 linkDataArray: linkArray,
574 export default ForceDirectedGraph;