From 993d6f949018fea85f3bf1b32554419fcbb18e9f Mon Sep 17 00:00:00 2001 From: feger <marc.feger@hhu.de> Date: Tue, 9 Jul 2019 18:29:35 +0200 Subject: [PATCH] Add visualisation with first effort --- package-lock.json | 76 ++++++++++++ package.json | 3 +- src/app.js | 5 +- src/index.html | 44 ++++--- src/js/graph/graphCreator.js | 38 ++++-- src/js/visualizer/visualization.js | 190 +++++++++++++++++++++++++++++ src/js/visualizer/visualizer.js | 18 ++- 7 files changed, 347 insertions(+), 27 deletions(-) create mode 100644 src/js/visualizer/visualization.js diff --git a/package-lock.json b/package-lock.json index 5d701b8..68987f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -808,6 +808,12 @@ "to-fast-properties": "^2.0.0" } }, + "@types/d3-selection": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.0.10.tgz", + "integrity": "sha1-3PsN3837GtJq6kNRMjdx4a6pboQ=", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -2433,6 +2439,76 @@ "d3-path": "1" } }, + "d3-svg-legend": { + "version": "2.25.6", + "resolved": "https://registry.npmjs.org/d3-svg-legend/-/d3-svg-legend-2.25.6.tgz", + "integrity": "sha1-jY3BvWk8N47ki2+CPook5o8uGtI=", + "dev": true, + "requires": { + "@types/d3-selection": "1.0.10", + "d3-array": "1.0.1", + "d3-dispatch": "1.0.1", + "d3-format": "1.0.2", + "d3-scale": "1.0.3", + "d3-selection": "1.0.2", + "d3-transition": "1.0.3" + }, + "dependencies": { + "d3-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.0.1.tgz", + "integrity": "sha1-N1wCh0/NlsFu2fG89bSnvlPzWOc=", + "dev": true + }, + "d3-dispatch": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.1.tgz", + "integrity": "sha1-S9ZaQ87P9DGN653yRVKqi/KBqEA=", + "dev": true + }, + "d3-format": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.0.2.tgz", + "integrity": "sha1-E4YYMgtLvrQ7XA/zBRkHn7vXN14=", + "dev": true + }, + "d3-scale": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.3.tgz", + "integrity": "sha1-T56PDMLqDzkl/wSsJ63AkEX6TJA=", + "dev": true, + "requires": { + "d3-array": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-selection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.0.2.tgz", + "integrity": "sha1-rmYq/UcCrJxdoDmyEHoXZPockHA=", + "dev": true + }, + "d3-transition": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.0.3.tgz", + "integrity": "sha1-kdyYa92zCXNjkyCoXbcs5KsaJ7s=", + "dev": true, + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-timer": "1" + } + } + } + }, "d3-time": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", diff --git a/package.json b/package.json index d20274b..a1f8829 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "scripts": { "build": "webpack", - "start:dev": "webpack-dev-server" + "start:dev": "webpack-dev-server --host 0.0.0.0" }, "author": "", "license": "ISC", @@ -15,6 +15,7 @@ "babel-loader": "^8.0.6", "css-loader": "^2.1.1", "d3": "^5.9.2", + "d3-svg-legend": "^2.25.6", "file-loader": "^3.0.1", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", diff --git a/src/app.js b/src/app.js index 49f6a77..2b5ee25 100644 --- a/src/app.js +++ b/src/app.js @@ -1,3 +1,2 @@ -import {Visualizer} from './js/visualizer/visualizer' - -new Visualizer().plotGraph('match (a:Issue{id:4})<-[r*..]-(b) return *'); \ No newline at end of file +import {Network} from './js/visualizer/visualization' +new Network().showNetwork('match (a:Issue{id:2})<-[r*..]-(b) return *'); \ No newline at end of file diff --git a/src/index.html b/src/index.html index 580de53..0b9c23a 100644 --- a/src/index.html +++ b/src/index.html @@ -1,22 +1,38 @@ <!DOCTYPE html> <meta charset="utf-8"> <style> - body { - background: antiquewhite; - font-family: "Inconsolata"; - } - circle { - fill: coral; - stroke: grey; + + .link { + stroke: #ffffff; stroke-width: 2px; + pointer-events: all; + } + + .node circle { + pointer-events: all; + stroke: #ffffff; + stroke-width: 1px; + fill: grey; } - text { - fill: slategrey; - font-size: 20px; + + div.tooltip { + position: absolute; + background-color: #ffd899; + max-width; + 200px; + height: auto; + padding: 2px; + border-style: solid; + border-radius: 4px; + border-width: 1px; + box-shadow: 3px 3px 10px rgba(0, 0, 0, .5); + pointer-events: none; } - line { - stroke: slategrey; + + svg { + background: black; } </style> -<svg width="960" height="600"> -</svg> \ No newline at end of file +<body> +<svg width="1400" height="900"></svg> +</body> \ No newline at end of file diff --git a/src/js/graph/graphCreator.js b/src/js/graph/graphCreator.js index d673a33..a4d60c3 100644 --- a/src/js/graph/graphCreator.js +++ b/src/js/graph/graphCreator.js @@ -1,23 +1,45 @@ import {Neo4JAdapter} from '../neo4j/neo' import {v1 as neo4j} from 'neo4j-driver' -export class GraphCreator{ - async fill(graph, withQuery){ +export class GraphCreator { + + async fill(graph, withQuery) { const queryResult = await new Neo4JAdapter('neo4j', 'W7uFSy$ywR3M3ck').ask(withQuery); var records = queryResult.records; records.forEach(function (record) { record.forEach(function (element) { if (element instanceof neo4j.types.Node) { var id = element.identity.toInt(); - var newNode = {"id": id}; + var newNode = null; + if (element.labels.includes("Issue")){ + newNode = { + "id": id, + "labels": element.labels, + "title": element.properties.title + }; + }else if (element.labels.includes("Statement")){ + newNode = { + "id": id, + "labels": element.labels, + "textversion": element.properties.textversion + }; + }else if (element.labels.includes("Argument")){ + newNode = { + "id": id, + "labels": element.labels + }; + } graph.addNode(newNode); - } else if (element instanceof neo4j.types.Relationship) { - var newEdge = {"source": element.start.toInt(), "target": element.end.toInt()}; - graph.addEdge(newEdge); } else if (element instanceof Array) { element.forEach(function (relation) { - var newEdge = {"source": relation.start.toInt(), "target": relation.end.toInt()}; - graph.addEdge(newEdge); + if (relation instanceof neo4j.types.Relationship) { + var newEdge = { + "source": relation.start.toInt(), + "target": relation.end.toInt(), + "type": relation.type + }; + graph.addEdge(newEdge); + } }) } }); diff --git a/src/js/visualizer/visualization.js b/src/js/visualizer/visualization.js new file mode 100644 index 0000000..4108444 --- /dev/null +++ b/src/js/visualizer/visualization.js @@ -0,0 +1,190 @@ +import * as d3 from 'd3' +import {legendColor} from 'd3-svg-legend' +import {Neo4JAdapter} from '../neo4j/neo' +import {Graph} from '../graph/graph' +import {GraphCreator} from '../graph/graphCreator' +import {v1 as neo4j} from 'neo4j-driver' + +export class Network { + async showNetwork(query) { + const graph = await new GraphCreator().fill(new Graph(), query); + + var color = d3.scaleOrdinal(d3.schemeSet3); + + var tooltip = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + const svg = d3.select('svg'), + width = +svg.attr('width'), + height = +svg.attr('height'); + + const simulation = d3.forceSimulation() + .nodes(graph.nodes) + .force('link', d3.forceLink().id(d => d.id).distance(20)) + .force('charge', d3.forceManyBody()) + .force('center', d3.forceCenter(width / 2, height / 2)) + .on('tick', ticked); + + simulation.force('link') + .links(graph.edges); + + const R = 6; + let link = svg.selectAll('line') + .data(graph.edges) + .enter().append('line'); + + link + .attr('class', 'link') + .on('mouseover.tooltip', function (d) { + tooltip.transition() + .duration(300) + .style("opacity", 0.8); + tooltip.html("Source:" + d.source.id + + "<p/>Target:" + d.target.id + + "<p/>Type:" + d.type) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY + 10) + "px"); + }) + .on("mouseout.tooltip", function () { + tooltip.transition() + .duration(100) + .style("opacity", 0); + }) + .on('mouseout.fade', fade(1)) + .on("mousemove", function () { + tooltip.style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY + 10) + "px"); + }); + ; + let node = svg.selectAll('.node') + .data(graph.nodes) + .enter().append('g') + .attr('class', 'node') + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + ; + node.append('circle') + .attr('r', R) + .attr("fill", function (d) { + return color(d.labels); + }) + .on('mouseover.tooltip', function (d) { + tooltip.transition() + .duration(300) + .style("opacity", .8); + let info = ""; + if (d.labels.includes("Issue")) { + info = "id:" + d.id + + "<p/>Labels:" + d.labels + + "<p/>Title:" + d.title; + }else if (d.labels.includes("Statement")){ + info = "id:" + d.id + + "<p/>Labels:" + d.labels + + "<p/>Textversion:" + d.textversion; + } + else if (d.labels.includes("Argument")){ + info = "id:" + d.id + + "<p/>Labels:" + d.labels; + } + tooltip.html(info) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY + 10) + "px"); + }) + .on('mouseover.fade', fade(0.1)) + .on("mouseout.tooltip", function () { + tooltip.transition() + .duration(100) + .style("opacity", 0); + }) + .on('mouseout.fade', fade(1)) + .on("mousemove", function () { + tooltip.style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY + 10) + "px"); + }) + .on('dblclick', releasenode) + + node.append('text') + .attr('x', 0) + .attr('dy', '.35em') + .text(d => d.name); + + function ticked() { + link + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + node + .attr('transform', d => `translate(${d.x},${d.y})`); + } + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + function releasenode(d) { + d.fx = null; + d.fy = null; + } + + const linkedByIndex = {}; + graph.edges.forEach(d => { + linkedByIndex[`${d.source.index},${d.target.index}`] = 1; + }); + + function isConnected(a, b) { + return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index; + } + + function fade(opacity) { + return d => { + node.style('stroke-opacity', function (o) { + const thisOpacity = isConnected(d, o) ? 1 : opacity; + this.setAttribute('fill-opacity', thisOpacity); + return thisOpacity; + }); + + link.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : opacity)); + + }; + } + + var sequentialScale = d3.scaleOrdinal(d3.schemeSet3) + .domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + svg.append("g") + .attr("class", "legendSequential") + .attr("transform", "translate(" + (width - 100) + "," + (height - 300) + ")"); + + + var legendSequential = legendColor() + .shapeWidth(30) + .cells(11) + .orient("vertical") + .title("Group number by color:") + .titleWidth(100) + .scale(sequentialScale); + + svg.select(".legendSequential") + .call(legendSequential); + + } +} \ No newline at end of file diff --git a/src/js/visualizer/visualizer.js b/src/js/visualizer/visualizer.js index e51434d..793e66c 100644 --- a/src/js/visualizer/visualizer.js +++ b/src/js/visualizer/visualizer.js @@ -22,7 +22,7 @@ export class Visualizer { .attr("markerWidth", 13) .attr("markerHeight", 13) .attr("orient", "auto") - .attr('xoverflow','visible') + .attr('xoverflow', 'visible') .append("svg:path") .attr("d", "M0,-5L10,0L0,5"); var simulation = d3.forceSimulation() @@ -52,6 +52,20 @@ export class Visualizer { d.fy = d.y; } + function click() { + d3.select(this).classed("fixed", this.fixed = true).transition() + .duration(1000) + .attr("r", 15) + .style("fill", "lightsteelblue"); + } + + function dbclick() { + d3.select(this).classed("fixed", this.fixed = false).transition() + .duration(1000) + .attr("r", 5) + .style("fill", "#ccc"); + } + function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; @@ -76,6 +90,8 @@ export class Visualizer { .data(graph.nodes) .enter() .append("circle") + .on("click", click) + .on("dblick", dbclick) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) -- GitLab