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(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 ()
}
}
export default Graph