From 58457a8de75959ae07dc09df095d72adc5965a7c Mon Sep 17 00:00:00 2001 From: Pavel Paroulek Date: Tue, 15 Jan 2019 18:53:50 +0100 Subject: Initial commit Java dummy backend and frontend Change-Id: I8c5528fcf8a746154e0463e065238061ddf6b877 Issue-ID: AAI-532 Signed-off-by: Pavel Paroulek --- graphgraph-fe/src/Graph.js | 282 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 graphgraph-fe/src/Graph.js (limited to 'graphgraph-fe/src/Graph.js') diff --git a/graphgraph-fe/src/Graph.js b/graphgraph-fe/src/Graph.js new file mode 100644 index 0000000..c6e5654 --- /dev/null +++ b/graphgraph-fe/src/Graph.js @@ -0,0 +1,282 @@ +import React from 'react' +import * as d3 from 'd3' +import './Graph.css' +import _ from 'underscore' +import * as constants from './constants' + +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 "
" + (_.reduce(properties, (html, e) => { return html + "" + e.propertyName + ': ' + e.propertyValue + '
' }, '')) + '
' +} + +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(4000) + .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) +} + +var addLinks = function (links, g, div, edgePropsLoader) { + 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 zoom_handler = d3.zoom() + .on('zoom', _ => g.attr('transform', d3.event.transform)) + + zoom_handler(svg) +} + +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(-1800)) + .force('link', d3.forceLink(links).distance(400).strength(1).iterations(100)) + .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 width = 100 + var height = 100 + var g = svg.append('g').attr('transform', `translate(${(300 + width / 10)}, ${height / 10})`) + + return { 'g': g, 'svg': svg } +} + +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 (
) + } +} + +export default Graph -- cgit 1.2.3-korg