aboutsummaryrefslogtreecommitdiffstats
path: root/graphgraph-fe/src/graph.js
diff options
context:
space:
mode:
Diffstat (limited to 'graphgraph-fe/src/graph.js')
-rw-r--r--graphgraph-fe/src/graph.js372
1 files changed, 372 insertions, 0 deletions
diff --git a/graphgraph-fe/src/graph.js b/graphgraph-fe/src/graph.js
new file mode 100644
index 0000000..492959a
--- /dev/null
+++ b/graphgraph-fe/src/graph.js
@@ -0,0 +1,372 @@
+/*
+ * ============LICENSE_START=======================================================
+ * org.onap.aai
+ * ================================================================================
+ * Copyright © 2019-2020 Orange Intellectual Property. 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
+ *
+ * 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=========================================================
+ */
+
+import React from 'react';
+import * as d3 from 'd3';
+import './graph.css';
+import _ from 'underscore';
+import * as constants from './constants.js';
+
+const CLICKED_COLOR = 'blue';
+const PATH_COLOR = 'red';
+const EDGE_MOUSE_OVER_COLOR = 'yellow';
+const EDGE_NORMAL_COLOR = '#ccc';
+const EDGE_LABEL_COLOR = 'black';
+const NODE_NORMAL_COLOR = '#E3E3E3';
+const NODE_LABEL_COLOR = 'black';
+const NODE_MOUSE_OVER_COLOR = '#F6F6F6';
+const NODE_BORDER_COLOR = 'lightgray';
+
+// variable holds state in order to determine if graph should be redrawn completely
+// it breaks the react concept, so a better approach is needed
+var graphFingerprint = '';
+
+var htmlfyProperties = function (properties) {
+ return "<div class='d3-tip'>" + (_.reduce(properties, (html, e) => {
+ return html + "<span style='color: lightgray'>" +
+ e.propertyName + ':</span> <span>' +
+ e.propertyValue + '</span><br/>';
+ }, '')) + '</div>';
+};
+
+var mouseOverEdge = function (edge, div) {
+ div.transition()
+ .duration(20)
+ .style('opacity', 0.9);
+ div.html(htmlfyProperties(edge.tooltipProperties))
+ .style('left', `${d3.event.pageX}px`)
+ .style('top', `${d3.event.pageY - 28}px`);
+};
+
+var mouseOutEdge = function (edge, div) {
+ div.transition()
+ .duration(6000)
+ .style('opacity', 0);
+};
+
+var addEdgePaths = function (links, g) {
+ d3.select('body').append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0);
+ g.selectAll('.edgepath')
+ .data(links)
+ .enter()
+ .append('path')
+ .attr('d', d => `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`)
+ .attr('class', 'edgepath')
+ .attr('fill-opacity', 0)
+ .attr('stroke-opacity', 0)
+ .attr('fill', EDGE_LABEL_COLOR)
+ .attr('id', (d, i) => `edgepath${i}`);
+};
+
+var chooseColor = function (state) {
+ if (state === constants.CLICKED) {
+ return CLICKED_COLOR;
+ }
+ if (state === constants.PATH) {
+ return PATH_COLOR;
+ }
+ return NODE_NORMAL_COLOR;
+};
+
+var redrawNodeColors = function (nodeStates, selectedEdge) {
+ d3.selectAll('svg').selectAll('g').selectAll('circle')
+ .style('fill', n => chooseColor(nodeStates[n.id]));
+ d3.selectAll('svg').selectAll('g').selectAll('line')
+ .style('stroke', EDGE_NORMAL_COLOR).attr('oldStroke', EDGE_NORMAL_COLOR);
+ d3.selectAll('svg').selectAll('g').selectAll('line')
+ .filter(edge => edge.source.id === selectedEdge.source && edge.target.id === selectedEdge.target)
+ .attr('oldStroke', CLICKED_COLOR)
+ .style('stroke', CLICKED_COLOR);
+};
+
+var addEdgeLabels = function (links, g, div) {
+ var edgelabels = g.selectAll('.edgelabel')
+ .data(links)
+ .enter()
+ .append('text')
+ .attr('class', 'edgelabel')
+ .attr('text-anchor', 'middle')
+ .attr('dx', 200)
+ .attr('dy', 0)
+ .attr('font-size', '22px')
+ .attr('id', (d, i) => `edgelabel${i}`);
+
+ edgelabels.append('textPath')
+ .attr('xlink:href', (d, i) => `#edgepath${i}`)
+ .text((d, i) => d.type);
+};
+
+var addNodeLabels = function (nodes, g) {
+ g.selectAll('.nodelabel')
+ .data(nodes)
+ .enter()
+ .append('text')
+ .attr('x', d => d.x - 14)
+ .attr('y', d => d.y - 17)
+ .attr('class', 'nodelabel')
+ .attr('fill', NODE_LABEL_COLOR)
+ .attr('font-size', '32px')
+ .text(d => d.id)
+ .on('mouseenter', onNodeLabelMouseOver)
+ .on('mouseout', onNodeLabelMouseOut);
+};
+
+var addLinks = function (links, g, div, edgePropsLoader) {
+ let ss = _.filter(links, l => l.source.id === l.target.id);
+
+ let selfLinks = _.isUndefined(ss) ? [] : ss;
+
+ g.selectAll('ellipse')
+ .data(selfLinks)
+ .enter().append('ellipse')
+ .attr('fill-opacity', 0)
+ .attr('rx', d => 100)
+ .attr('ry', d => 16)
+ .attr('cx', d => d.target.x + 80)
+ .attr('cy', d => d.target.y)
+ .style('stroke', NODE_BORDER_COLOR)
+ .attr('stroke-width', 5)
+ .on('click', edge => edgePropsLoader(edge.source.id, edge.target.id))
+ .on('mouseenter', function (edge) {
+ mouseOverEdge(edge, div);
+ d3.select(this)
+ .transition()
+ .attr('oldStroke', EDGE_NORMAL_COLOR)
+ .duration(10)
+ .style('stroke', EDGE_MOUSE_OVER_COLOR);
+ })
+ .on('mouseleave', function (edge) {
+ mouseOutEdge(edge, div);
+ var strokeColor = d3.select(this).attr('oldStroke');
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style('stroke', strokeColor);
+ });
+
+ g.selectAll('.edgelooplabel')
+ .data(selfLinks)
+ .enter()
+ .append('text')
+ .attr('x', d => d.source.x + 35)
+ .attr('y', d => d.source.y + 50)
+ .attr('class', 'edgelooplabel')
+ .attr('fill', NODE_LABEL_COLOR)
+ .attr('font-size', '22px')
+ .text(d => d.type);
+
+ g.selectAll('line')
+ .data(links)
+ .enter().append('line')
+ .attr('stroke-width', 5)
+ .attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => d.target.x)
+ .attr('y2', d => d.target.y)
+ .attr('id', (d, i) => `edge${i}`)
+ .attr('marker-end', 'url(#arrowhead)')
+ .style('stroke', EDGE_NORMAL_COLOR)
+ .on('click', edge => edgePropsLoader(edge.source.id, edge.target.id))
+ .on('mouseenter', function (edge) {
+ mouseOverEdge(edge, div);
+ d3.select(this)
+ .transition()
+ .attr('oldStroke', EDGE_NORMAL_COLOR)
+ .duration(10)
+ .style('stroke', EDGE_MOUSE_OVER_COLOR);
+ })
+ .on('mouseleave', function (edge) {
+ mouseOutEdge(edge, div);
+ var strokeColor = d3.select(this).attr('oldStroke');
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style('stroke', strokeColor);
+ });
+};
+
+var addMarkers = function (g, svg) {
+ g.append('defs').append('marker')
+ .attr('id', 'arrowhead')
+ .attr('viewBox', '-0 -5 10 10')
+ .attr('refX', '20')
+ .attr('refY', '0')
+ .attr('orient', 'auto')
+ .attr('markerWidth', '4')
+ .attr('markerHeight', '4')
+ .attr('xoverflow', 'visible')
+ .append('svg:path')
+ .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
+ .attr('fill', EDGE_NORMAL_COLOR)
+ .attr('stroke', EDGE_NORMAL_COLOR);
+
+ var zoomHandler = d3.zoom()
+ .on('zoom', _ => g.attr('transform', d3.event.transform));
+
+ zoomHandler(svg);
+ zoomHandler.translateTo(svg, -7000, -4000);
+ zoomHandler.scaleTo(svg, 0.08);
+};
+
+var drawGraph = function (nodes, links, g, simulation, svg, addNodes, edgePropsLoader) {
+ for (var i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
+ i < n; ++i) {
+ simulation.tick();
+ }
+
+ var div = d3.select('body').append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0);
+
+ addLinks(links, g, div, edgePropsLoader);
+ addNodes(nodes, g);
+ addNodeLabels(nodes, g);
+ addEdgePaths(links, g);
+ addEdgeLabels(links, g, div);
+ addMarkers(g, svg);
+};
+
+var prepareLinks = function (nodes, links) {
+ var result = [];
+ links.forEach(e => {
+ var sourceNode = nodes.filter(n => n.id === e.source)[0];
+ var targetNode = nodes.filter(n => n.id === e.target)[0];
+
+ result.push({
+ source: sourceNode,
+ target: targetNode,
+ type: e.type,
+ tooltipProperties: e.tooltipProperties
+ });
+ });
+
+ return result;
+};
+
+var createSimulation = function (nodes, links) {
+ return d3.forceSimulation(nodes)
+ .force('charge', d3.forceManyBody().strength(-201))
+ .force('link', d3.forceLink(links).distance(1200).strength(1).iterations(400))
+ .force('collision', d3.forceCollide().radius(d => 310))
+ .force('x', d3.forceX())
+ .force('y', d3.forceY())
+ .stop();
+};
+
+var createSvg = function () {
+ var svg = d3.select('#graph').append('svg').attr('height', '100%').attr('width', '100%');
+ var g = svg.append('g');
+
+ return { 'g': g, 'svg': svg };
+};
+
+var onNodeLabelMouseOut = function () {
+ d3.select(this)
+ .transition()
+ .duration(600)
+ .attr('font-size', '32px');
+};
+
+var onNodeLabelMouseOver = function () {
+ d3.select(this)
+ .transition()
+ .duration(200)
+ .attr('font-size', '232px');
+};
+
+var onNodeMouseOver = function () {
+ var oldFill = d3.select(this).style('fill');
+ d3.select(this)
+ .transition()
+ .duration(200)
+ .attr('r', 31)
+ .attr('oldFill', oldFill)
+ .style('fill', NODE_MOUSE_OVER_COLOR);
+};
+
+var onNodeMouseOut = function (nodeStates) {
+ var oldFill = d3.select(this).attr('oldFill');
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .attr('r', 23)
+ .style('fill', oldFill);
+};
+
+class Graph extends React.Component {
+ onNodeClick (x) {
+ // on mouse out the node will change color read from 'oldFill' attribute
+ d3.selectAll('svg').selectAll('g').selectAll('circle')
+ .filter(c => c.id === x.id)
+ .attr('oldFill', CLICKED_COLOR);
+ this.props.nodePropsLoader(x.id);
+ }
+
+ addNodes (nodes, g) {
+ g.selectAll('circle')
+ .data(nodes)
+ .enter().append('circle')
+ .attr('cx', d => d.x)
+ .attr('cy', d => d.y)
+ .style('fill', n => {
+ return chooseColor(this.props.nodeStates[n.id]);
+ })
+ .style('stroke', NODE_BORDER_COLOR)
+ .attr('stroke-width', 3)
+ .attr('r', 23)
+ .on('click', this.onNodeClick)
+ .on('mouseover', onNodeMouseOver)
+ .on('mouseout', onNodeMouseOut);
+ }
+
+ reCreateGraph () {
+ d3.select('#graph').selectAll('*').remove();
+
+ var nodes = this.props.nodes;
+ var links = prepareLinks(this.props.nodes, this.props.edges);
+ var o = createSvg();
+ var simulation = createSimulation(nodes, links);
+
+ drawGraph(nodes, links, o.g, simulation, o.svg, this.addNodes, this.props.edgePropsLoader);
+ }
+
+ constructor (props, context) {
+ super(props, context);
+
+ this.reCreateGraph = this.reCreateGraph.bind(this);
+ this.addNodes = this.addNodes.bind(this);
+ this.onNodeClick = this.onNodeClick.bind(this);
+ }
+
+ render () {
+ if (this.props.graphFingerprint !== graphFingerprint) {
+ this.reCreateGraph();
+ graphFingerprint = this.props.graphFingerprint;
+ } else {
+ redrawNodeColors(this.props.nodeStates, this.props.selectedEdge);
+ }
+
+ return <div id="graph"/>;
+ }
+}
+
+export default Graph;