2 * ============LICENSE_START===================================================
3 * SPARKY (AAI UI service)
4 * ============================================================================
5 * Copyright © 2017 AT&T Intellectual Property.
6 * Copyright © 2017 Amdocs
8 * ============================================================================
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ============LICENSE_END=====================================================
22 * ECOMP and OpenECOMP are trademarks
23 * and service marks of AT&T Intellectual Property.
26 import {drag} from 'd3-drag';
27 import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force';
28 import {interpolateNumber} from 'd3-interpolate';
29 import {select, event as currentEvent} from 'd3-selection';
30 import React, {Component, PropTypes} from 'react';
31 import {interval, now} from 'd3-timer';
32 import {zoom, zoomIdentity} from 'd3-zoom';
33 import NodeConstants from './NodeVisualElementConstants.js';
35 import {simulationKeys} from './ForceDefinitions.js';
36 import NodeFactory from './NodeFactory.js';
37 import NodeVisualElementFactory from './NodeVisualElementFactory.js';
39 class ForceDirectedGraph extends Component {
41 viewWidth: PropTypes.number,
42 viewHeight: PropTypes.number,
43 graphData: PropTypes.object,
44 nodeIdKey: PropTypes.string,
45 linkIdKey: PropTypes.string,
46 nodeSelectedCallback: PropTypes.func,
47 nodeButtonSelectedCallback: PropTypes.func,
48 currentlySelectedNodeView: PropTypes.string
51 static defaultProps = {
55 graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {}
59 nodeSelectedCallback: undefined,
60 nodeButtonSelectedCallback: undefined,
61 currentlySelectedNodeView: ''
68 nodes: [], links: [], mainGroupTransform: zoomIdentity
71 this.updateSimulationForce = this.updateSimulationForce.bind(this);
72 this.resetState = this.resetState.bind(this);
73 this.applyBufferDataToState = this.applyBufferDataToState.bind(this);
74 this.createNodePropForState = this.createNodePropForState.bind(this);
75 this.createLinkPropForState = this.createLinkPropForState.bind(this);
76 this.startSimulation = this.startSimulation.bind(this);
77 this.simulationComplete = this.simulationComplete.bind(this);
78 this.simulationTick = this.simulationTick.bind(this);
79 this.nodeSelected = this.nodeSelected.bind(this);
80 this.nodeButtonSelected = this.nodeButtonSelected.bind(this);
81 this.onZoom = this.onZoom.bind(this);
82 this.onGraphDrag = this.onGraphDrag.bind(this);
83 this.onNodeDrag = this.onNodeDrag.bind(this);
84 this.addNodeInterpolator = this.addNodeInterpolator.bind(this);
85 this.runInterpolators = this.runInterpolators.bind(this);
90 this.nodeButtonDatum = [];
91 this.nodeFactory = new NodeFactory();
92 this.visualElementFactory = new NodeVisualElementFactory();
94 this.isGraphMounted = false;
96 this.listenerGraphCounter = -1;
97 this.nodeIndexTracker = new Map();
98 this.interpolators = new Map();
99 this.areInterpolationsRunning = false;
101 this.newNodeSelected = true;
102 this.currentlySelectedNodeButton = undefined;
104 this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
105 this.intervalTimer.stop();
107 this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
108 this.interpolationTimer.stop();
110 this.simulation = forceSimulation();
111 this.simulation.on('end', this.simulationComplete);
112 this.simulation.stop();
115 zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]);
116 this.svgZoom.clickDistance(2);
117 this.nodeDrag = drag().clickDistance(2);
119 this.updateSimulationForce();
120 // Temporary code until backend supports NOT displaying the button in the response.
121 if(props.dataOverlayButtons.length === 1) {
122 this.hideButton = true;
124 this.hideButton = false;
126 if (props.graphData) {
127 if (props.graphData.graphCounter !== -1) {
128 this.startSimulation(props.graphData, props.currentlySelectedNodeView, props.dataOverlayButtons);
133 componentDidMount() {
134 this.isGraphMounted = true;
137 componentWillReceiveProps(nextProps) {
138 if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
139 this.listenerGraphCounter = this.props.graphData.graphCounter;
140 this.newNodeSelected = true;
142 this.startSimulation(nextProps.graphData, nextProps.currentlySelectedNodeView, nextProps.dataOverlayButtons);
147 componentDidUpdate(prevProps) {
148 let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
149 this.props.graphData.graphCounter);
150 let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
151 let nodeCount = this.state.nodes.length;
154 if (hasNewGraphDataRendered && shouldAttachListeners) {
155 let nodes = select('.fdgMainSvg').select('.fdgMainG')
156 .selectAll('.aai-entity-node')
157 .data(this.nodeDatum);
158 nodes.on('click', (d) => {
159 this.nodeSelected(d);
162 nodes.call(this.nodeDrag.on('drag', (d) => {
163 let xAndY = [currentEvent.x, currentEvent.y];
164 this.onNodeDrag(d, xAndY);
167 let mainSVG = select('.fdgMainSvg');
168 let mainView = mainSVG.select('.fdgMainView');
169 this.svgZoom.transform(mainSVG, zoomIdentity);
170 this.svgZoom.transform(mainView, zoomIdentity);
172 mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
173 this.onZoom(currentEvent.transform);
174 })).on('dblclick.zoom', null); // Ignore the double-click zoom event
176 this.listenerGraphCounter = this.props.graphData.graphCounter;
179 if (this.newNodeSelected) {
180 let nodeButtons = select('.fdgMainSvg').select('.fdgMainG')
181 .selectAll('.aai-entity-node')
182 .selectAll('.node-button')
183 .data(this.nodeButtonDatum);
184 if (!nodeButtons.empty()) {
185 nodeButtons.on('click', (d) => {
186 this.nodeButtonSelected(d);
188 if (hasNewGraphDataRendered && shouldAttachListeners) {
189 this.newNodeSelected = false;
196 componentWillUnmount() {
197 this.isGraphMounted = false;
199 let nodes = select('.fdgMainSvg').select('.fdgMainG')
200 .selectAll('.aai-entity-node');
201 let nodeButtons = nodes.selectAll('.node-button');
203 nodes.on('click', null);
204 nodeButtons.on('click', null);
206 let mainSVG = select('.fdgMainSvg');
208 mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
209 mainSVG.call(drag().on('drag', null));
212 updateSimulationForce() {
213 this.simulation.force('link', forceLink());
214 this.simulation.force('link').id((d) => {
217 this.simulation.force('link').strength(0.3);
218 this.simulation.force('link').distance(100);
220 this.simulation.force('charge', forceManyBody());
221 this.simulation.force('charge').strength(-1250);
222 this.simulation.alpha(1);
224 this.simulation.force('center',
225 forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
229 if (this.isGraphMounted) {
230 this.setState(() => {
232 mainGroupTransform: zoomIdentity,
239 applyBufferDataToState() {
240 this.nodeIndexTracker.clear();
243 this.nodeBuffer.map((node, i) => {
244 let nodeProps = this.createNodePropForState(node);
246 if (nodeProps.meta.nodeMeta.className ===
247 NodeConstants.SELECTED_NODE_CLASS_NAME ||
248 nodeProps.meta.nodeMeta.className ===
249 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
251 this.nodeButtonDatum[0].data = nodeProps.meta;
252 if(this.nodeButtonDatum.length > 1) {
253 this.nodeButtonDatum[1].data = nodeProps.meta;
256 buttons: [this.nodeButtonDatum[0].isSelected, this.nodeButtonDatum[1].isSelected]
261 buttons: [this.nodeButtonDatum[0].isSelected]
266 newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps, this.hideButton));
268 this.nodeIndexTracker.set(node.id, i);
272 this.linkBuffer.map((link) => {
274 let linkProps = this.createLinkPropForState(link);
275 newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
278 if (this.isGraphMounted) {
279 this.setState(() => {
281 nodes: newNodes, links: newLinks
287 createNodePropForState(nodeData) {
290 key: nodeData.id, x: nodeData.x, y: nodeData.y
297 createLinkPropForState(linkData) {
299 className: 'aai-entity-link',
300 x1: linkData.source.x,
301 y1: linkData.source.y,
302 x2: linkData.target.x,
303 y2: linkData.target.y
307 startSimulation(graphData, currentView, overlayButtons) {
308 this.nodeFactory.setNodeMeta(graphData.graphMeta);
310 // Experiment with removing length = 0... might not be needed as new array
311 // assignment will likely destroy old reference
312 this.nodeBuffer.length = 0;
313 this.nodeBuffer = Array.from(graphData.nodeDataArray);
314 this.linkBuffer.length = 0;
315 this.linkBuffer = Array.from(graphData.linkDataArray);
316 this.nodeDatum.length = 0;
317 this.nodeDatum = Array.from(graphData.nodeDataArray);
319 this.nodeButtonDatum.length = 0;
321 let isNodeDetailsSelected = (currentView ===
325 this.nodeButtonDatum.push({
326 name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected, overlayName: overlayButtons[0]
330 if(overlayButtons.length > 1 ) {
331 let isSecondButtonSelected = (currentView === overlayButtons[1]);
333 this.nodeButtonDatum.push({
334 name: NodeConstants.ICON_TRIANGLE_WARNING, isSelected: isSecondButtonSelected, overlayName: overlayButtons[1]
339 if (isNodeDetailsSelected) {
340 this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
342 this.currentlySelectedNodeButton = NodeConstants.ICON_TRIANGLE_WARNING;
345 this.updateSimulationForce();
347 this.simulation.nodes(this.nodeBuffer);
348 this.simulation.force('link').links(this.linkBuffer);
349 this.simulation.on('tick', this.simulationTick);
350 this.simulation.restart();
353 simulationComplete() {
354 this.intervalTimer.stop();
355 this.applyBufferDataToState();
359 this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
360 this.simulation.on('tick', null);
363 nodeSelected(datum) {
364 if (this.props.nodeSelectedCallback) {
365 this.props.nodeSelectedCallback(datum);
368 let didUpdateNew = false;
369 let didUpdatePrevious = false;
370 let isSameNodeSelected = true;
372 // Check to see if a default node was previously selected
373 let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
374 .selectAll('.aai-entity-node')
375 .filter('.selected-node');
376 if (!selectedDefaultNode.empty()) {
377 if (selectedDefaultNode.datum().id !== datum.id) {
378 this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
379 NodeConstants.GENERAL_NODE_CLASS_NAME;
380 didUpdatePrevious = true;
381 isSameNodeSelected = false;
385 // Check to see if a searched node was previously selected
386 let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
387 .selectAll('.aai-entity-node')
388 .filter('.selected-search-node');
389 if (!selectedSearchedNode.empty()) {
390 if (selectedSearchedNode.datum().id !== datum.id) {
391 this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
392 NodeConstants.SEARCHED_NODE_CLASS_NAME;
393 didUpdatePrevious = true;
394 isSameNodeSelected = false;
398 if (!isSameNodeSelected) {
399 let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
400 .selectAll('.aai-entity-node')
402 return (datum.id === d.id);
404 if (!newlySelectedNode.empty()) {
406 if (newlySelectedNode.datum().nodeMeta.searchTarget) {
407 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
408 NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
410 this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
411 NodeConstants.SELECTED_NODE_CLASS_NAME;
417 if (didUpdatePrevious && didUpdateNew) {
418 this.newNodeSelected = true;
419 this.applyBufferDataToState();
423 nodeButtonSelected(datum) {
424 if (this.props.nodeButtonSelectedCallback) {
425 let buttonClickEvent = {
426 buttonId: datum.overlayName
428 this.props.nodeButtonSelectedCallback(buttonClickEvent);
431 if (this.currentlySelectedNodeButton !== datum.name) {
432 if (datum.name === this.nodeButtonDatum[0].name) {
433 this.nodeButtonDatum[0].isSelected = true;
434 this.nodeButtonDatum[1].isSelected = false;
436 if (datum.name === this.nodeButtonDatum[1].name) {
437 this.nodeButtonDatum[0].isSelected = false;
438 this.nodeButtonDatum[1].isSelected = true;
440 this.currentlySelectedNodeButton = datum.name;
441 this.applyBufferDataToState();
445 onZoom(eventTransform) {
446 if (this.isGraphMounted) {
447 this.setState(() => {
449 mainGroupTransform: eventTransform
455 onGraphDrag(xAndYCoords) {
456 let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
457 let oldTransform = this.state.mainGroupTransform;
458 if (this.isGraphMounted) {
459 this.setState(() => {
461 ...oldTransform, translate
467 onNodeDrag(datum, xAndYCoords) {
468 let nodeIndex = this.nodeIndexTracker.get(datum.id);
469 if (this.nodeBuffer[nodeIndex]) {
470 this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
471 this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
472 this.applyBufferDataToState();
476 addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
477 let numberInterpolator = interpolateNumber(startingValue, endingValue);
479 let interpolationObject = {
480 nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
482 this.interpolators.set(nodeId, interpolationObject);
484 if (!this.areInterpolationsRunning) {
485 this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
486 this.areInterpolationsRunning = true;
491 // If we have no more interpolators to run then shut'r down!
492 if (this.interpolators.size === 0) {
493 this.interpolationTimer.stop();
494 this.areInterpolationsRunning = false;
497 let iterpolatorsComplete = [];
498 // Apply interpolation values
499 this.interpolators.forEach((interpolator) => {
500 let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
502 let elapsedTime = now() - interpolator.timeCreated;
503 // Normalize t as D3's interpolateNumber needs a value between 0 and 1
504 let t = elapsedTime / interpolator.duration;
507 iterpolatorsComplete.push(interpolator.nodeId);
509 this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
513 // Remove any interpolators that are complete
514 if (iterpolatorsComplete.length > 0) {
515 for (let i = 0; i < iterpolatorsComplete.length; i++) {
516 this.interpolators.delete(iterpolatorsComplete[i]);
520 this.applyBufferDataToState();
524 // We will be using these values veru shortly, commenting out for eslint
525 // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
526 let {nodes, links, mainGroupTransform} = this.state;
529 <div className='ts-force-selected-graph'>
530 <svg className={'fdgMainSvg'} width='100%' height='100%'>
531 <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
532 height='99%' fill='none'/>
533 <filter id='selected-node-drop-shadow'>
534 <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
535 <feOffset dx='-1' dy='1' result='offsetblur'/>
536 <feFlood floodColor='rgba(0,0,0,0.5)'/>
537 <feComposite in2='offsetblur' operator='in'/>
540 <feMergeNode in='SourceGraphic'/>
543 <g className={'fdgMainG'} transform={mainGroupTransform}>
552 static graphCounter = 0;
554 static generateNewProps(nodeArray, linkArray, metaData) {
555 ForceDirectedGraph.graphCounter += 1;
557 graphCounter: ForceDirectedGraph.graphCounter,
558 nodeDataArray: nodeArray,
559 linkDataArray: linkArray,
565 export default ForceDirectedGraph;