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.
23 import {drag} from 'd3-drag';
24 import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force';
25 import {interpolateNumber} from 'd3-interpolate';
26 import {select, event as currentEvent} from 'd3-selection';
27 import React, {Component, PropTypes} from 'react';
28 import {interval, now} from 'd3-timer';
29 import {zoom, zoomIdentity} from 'd3-zoom';
30 import NodeConstants from './NodeVisualElementConstants.js';
32 import {simulationKeys} from './ForceDefinitions.js';
33 import NodeFactory from './NodeFactory.js';
34 import NodeVisualElementFactory from './NodeVisualElementFactory.js';
36 class ForceDirectedGraph extends Component {
38 viewWidth: PropTypes.number,
39 viewHeight: PropTypes.number,
40 graphData: PropTypes.object,
41 nodeIdKey: PropTypes.string,
42 linkIdKey: PropTypes.string,
43 nodeSelectedCallback: PropTypes.func,
44 nodeButtonSelectedCallback: PropTypes.func,
45 currentlySelectedNodeView: PropTypes.string
48 static defaultProps = {
52 graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {}
56 nodeSelectedCallback: undefined,
57 nodeButtonSelectedCallback: undefined,
58 currentlySelectedNodeView: ''
65 nodes: [], links: [], mainGroupTransform: zoomIdentity
68 this.updateSimulationForce = this.updateSimulationForce.bind(this);
69 this.resetTransform = this.resetTransform.bind(this);
70 this.applyBufferDataToState = this.applyBufferDataToState.bind(this);
71 this.createNodePropForState = this.createNodePropForState.bind(this);
72 this.createLinkPropForState = this.createLinkPropForState.bind(this);
73 this.startSimulation = this.startSimulation.bind(this);
74 this.simulationComplete = this.simulationComplete.bind(this);
75 this.simulationTick = this.simulationTick.bind(this);
76 this.nodeSelected = this.nodeSelected.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();
111 zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]);
112 this.svgZoom.clickDistance(2);
113 this.nodeDrag = drag().clickDistance(2);
115 this.updateSimulationForce();
117 if (props.graphData) {
118 if (props.graphData.graphCounter !== -1) {
119 this.startSimulation(props.graphData);
124 componentDidMount() {
125 this.isGraphMounted = true;
128 componentWillReceiveProps(nextProps) {
129 if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
130 this.listenerGraphCounter = this.props.graphData.graphCounter;
131 this.newNodeSelected = true;
132 this.resetTransform();
133 this.startSimulation(nextProps.graphData);
137 componentDidUpdate(prevProps) {
138 let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
139 this.props.graphData.graphCounter);
140 let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
141 let nodeCount = this.state.nodes.length;
144 if (hasNewGraphDataRendered && shouldAttachListeners) {
146 let nodes = select('.fdgMainSvg').select('.fdgMainG')
147 .selectAll('.aai-entity-node')
148 .data(this.nodeDatum);
150 nodes.on('click', (d) => {
151 this.nodeSelected(d);
154 nodes.call(this.nodeDrag.on('drag', (d) => {
155 let xAndY = [currentEvent.x, currentEvent.y];
156 this.onNodeDrag(d, xAndY);
159 let mainSVG = select('.fdgMainSvg');
160 let mainView = mainSVG.select('.fdgMainView');
161 this.svgZoom.transform(mainSVG, zoomIdentity);
162 this.svgZoom.transform(mainView, zoomIdentity);
164 mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
165 this.onZoom(currentEvent.transform);
166 })).on('dblclick.zoom', null); // Ignore the double-click zoom event
168 this.listenerGraphCounter = this.props.graphData.graphCounter;
173 componentWillUnmount() {
174 this.isGraphMounted = false;
176 let nodes = select('.fdgMainSvg').select('.fdgMainG')
177 .selectAll('.aai-entity-node');
178 let nodeButtons = nodes.selectAll('.node-button');
180 nodes.on('click', null);
181 nodeButtons.on('click', null);
183 let mainSVG = select('.fdgMainSvg');
185 mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
186 mainSVG.call(drag().on('drag', null));
189 updateSimulationForce() {
190 this.simulation.force('link', forceLink());
191 this.simulation.force('link').id((d) => {
194 this.simulation.force('link').strength(0.3);
195 this.simulation.force('link').distance(100);
197 this.simulation.force('charge', forceManyBody());
198 this.simulation.force('charge').strength(-1250);
199 this.simulation.alpha(1);
201 this.simulation.force('center',
202 forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
206 if (this.isGraphMounted) {
207 this.setState(() => {
209 mainGroupTransform: zoomIdentity
215 applyBufferDataToState() {
216 this.nodeIndexTracker.clear();
219 this.nodeBuffer.map((node, i) => {
220 let nodeProps = this.createNodePropForState(node);
222 if (nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_NODE_CLASS_NAME ||
223 nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
225 this.nodeButtonDatum[0].data = nodeProps.meta;
229 buttons: [this.nodeButtonDatum[0].isSelected]
233 newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps));
235 this.nodeIndexTracker.set(node.id, i);
239 this.linkBuffer.map((link) => {
241 let linkProps = this.createLinkPropForState(link);
242 newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
245 if (this.isGraphMounted) {
246 this.setState(() => {
248 nodes: newNodes, links: newLinks
254 createNodePropForState(nodeData) {
257 key: nodeData.id, x: nodeData.x, y: nodeData.y
264 createLinkPropForState(linkData) {
266 className: 'aai-entity-link',
267 x1: linkData.source.x,
268 y1: linkData.source.y,
269 x2: linkData.target.x,
270 y2: linkData.target.y
274 startSimulation(graphData) {
275 this.nodeFactory.setNodeMeta(graphData.graphMeta);
277 // Experiment with removing length = 0... might not be needed as new array
278 // assignment will likely destroy old reference
279 this.nodeBuffer.length = 0;
280 this.nodeBuffer = Array.from(graphData.nodeDataArray);
281 this.linkBuffer.length = 0;
282 this.linkBuffer = Array.from(graphData.linkDataArray);
283 this.nodeDatum.length = 0;
284 this.nodeDatum = Array.from(graphData.nodeDataArray);
286 this.nodeButtonDatum.length = 0;
288 let isNodeDetailsSelected = true;
289 this.nodeButtonDatum.push({
290 name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected
293 if (isNodeDetailsSelected) {
294 this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
297 this.updateSimulationForce();
299 this.simulation.nodes(this.nodeBuffer);
300 this.simulation.force('link').links(this.linkBuffer);
301 this.simulation.on('tick', this.simulationTick);
302 this.simulation.restart();
305 simulationComplete() {
306 this.intervalTimer.stop();
307 this.applyBufferDataToState();
311 this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
312 this.simulation.on('tick', null);
315 nodeSelected(datum) {
316 if (this.props.nodeSelectedCallback) {
317 this.props.nodeSelectedCallback(datum);
320 let didUpdateNew = false;
321 let didUpdatePrevious = false;
322 let isSameNodeSelected = true;
324 // Check to see if a default node was previously selected
325 let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
326 .selectAll('.aai-entity-node')
327 .filter('.selected-node');
328 if (!selectedDefaultNode.empty()) {
329 if (selectedDefaultNode.datum().id !== datum.id) {
330 this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
331 NodeConstants.GENERAL_NODE_CLASS_NAME;
332 didUpdatePrevious = true;
333 isSameNodeSelected = false;
337 // Check to see if a searched node was previously selected
338 let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
339 .selectAll('.aai-entity-node')
340 .filter('.selected-search-node');
341 if (!selectedSearchedNode.empty()) {
342 if (selectedSearchedNode.datum().id !== datum.id) {
343 this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
344 NodeConstants.SEARCHED_NODE_CLASS_NAME;
345 didUpdatePrevious = true;
346 isSameNodeSelected = false;
350 if (!isSameNodeSelected) {
351 let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
352 .selectAll('.aai-entity-node')
354 return (datum.id === d.id);
356 if (!newlySelectedNode.empty()) {
358 if (newlySelectedNode.datum().nodeMeta.searchTarget) {
359 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
360 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
362 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
363 NodeConstants.SELECTED_NODE_CLASS_NAME;
369 if (didUpdatePrevious && didUpdateNew) {
370 this.newNodeSelected = true;
371 this.applyBufferDataToState();
375 onZoom(eventTransform) {
376 if (this.isGraphMounted) {
377 this.setState(() => {
379 mainGroupTransform: eventTransform
385 onGraphDrag(xAndYCoords) {
386 let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
387 let oldTransform = this.state.mainGroupTransform;
388 if (this.isGraphMounted) {
389 this.setState(() => {
391 ...oldTransform, translate
397 onNodeDrag(datum, xAndYCoords) {
398 let nodeIndex = this.nodeIndexTracker.get(datum.id);
399 if (this.nodeBuffer[nodeIndex]) {
400 this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
401 this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
402 this.applyBufferDataToState();
406 addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
407 let numberInterpolator = interpolateNumber(startingValue, endingValue);
409 let interpolationObject = {
410 nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
412 this.interpolators.set(nodeId, interpolationObject);
414 if (!this.areInterpolationsRunning) {
415 this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
416 this.areInterpolationsRunning = true;
421 // If we have no more interpolators to run then shut'r down!
422 if (this.interpolators.size === 0) {
423 this.interpolationTimer.stop();
424 this.areInterpolationsRunning = false;
427 let iterpolatorsComplete = [];
428 // Apply interpolation values
429 this.interpolators.forEach((interpolator) => {
430 let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
432 let elapsedTime = now() - interpolator.timeCreated;
433 // Normalize t as D3's interpolateNumber needs a value between 0 and 1
434 let t = elapsedTime / interpolator.duration;
437 iterpolatorsComplete.push(interpolator.nodeId);
439 this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
443 // Remove any interpolators that are complete
444 if (iterpolatorsComplete.length > 0) {
445 for (let i = 0; i < iterpolatorsComplete.length; i++) {
446 this.interpolators.delete(iterpolatorsComplete[i]);
450 this.applyBufferDataToState();
454 // We will be using these values veru shortly, commenting out for eslint
455 // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
456 let {nodes, links, mainGroupTransform} = this.state;
459 <div className='ts-force-selected-graph'>
460 <svg className={'fdgMainSvg'} width='100%' height='100%'>
461 <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
462 height='99%' fill='none'/>
463 <filter id='selected-node-drop-shadow'>
464 <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
465 <feOffset dx='-1' dy='1' result='offsetblur'/>
466 <feFlood floodColor='rgba(0,0,0,0.5)'/>
467 <feComposite in2='offsetblur' operator='in'/>
470 <feMergeNode in='SourceGraphic'/>
473 <g className={'fdgMainG'} transform={mainGroupTransform}>
482 static graphCounter = 0;
484 static generateNewProps(nodeArray, linkArray, metaData) {
485 ForceDirectedGraph.graphCounter += 1;
487 graphCounter: ForceDirectedGraph.graphCounter,
488 nodeDataArray: nodeArray,
489 linkDataArray: linkArray,
495 export default ForceDirectedGraph;