diff options
Diffstat (limited to 'graphgraph-fe/src/graph.js')
-rw-r--r-- | graphgraph-fe/src/graph.js | 372 |
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; |