Skip to content

Instantly share code, notes, and snippets.

@tungnk1993
Last active December 23, 2015 09:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tungnk1993/f8380e9ec0763b2e51d8 to your computer and use it in GitHub Desktop.
Save tungnk1993/f8380e9ec0763b2e51d8 to your computer and use it in GitHub Desktop.
[Parallel] Directed / Weighted
  • Click on empty space to add node
  • Drag from node to node to add edge
  • Click/Select + Delete to delete node/edge
  • Click/Select Edge + Enter to change edge's weight
  • Press Ctrl to Drag node around
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Directed Graph Editor</title>
<link rel="stylesheet" href="visual.css">
</head>
<body>
</body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="visual.js"></script>
</html>
body {
background-color: #000;
}
svg {
background-color: #FFFFFF;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
svg:not(.active):not(.ctrl) {
cursor: crosshair;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 5px;
cursor: default;
}
svg:not(.active):not(.ctrl) path.link {
cursor: pointer;
}
path.link.selected {
stroke-dasharray: 10,2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node {
stroke-width: 2px;
cursor: pointer;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.weight {
cursor: pointer;
font-weight: bold;
text-anchor: middle;
}
text.id {
text-anchor: middle;
font-weight: bold;
}
var width = 960,
height = 500,
colors = d3.scale.category10();
var svg = d3.select('body')
.append('svg')
.attr('width',width)
.attr('height',height);
var countNodeId = new Array(200);
for (var i = countNodeId.length; i >= 0; -- i) countNodeId[i] = 0;
countNodeId[0]++;
countNodeId[1]++;
countNodeId[2]++;
var nodes = [ {id : 0, x : 100, y : 100 },
{id : 1, x : 200, y : 200 },
{id : 2, x : 300, y : 300 }],
links = [ {source : nodes[0], target : nodes[1], weight : 0},
{source : nodes[1], target : nodes[2], weight : 0 }],
lastNodeId = 3;
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
var drag_line = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
var path;
var circle;
var weight;
var selected_node = null,
selected_link = null,
mousedown_link = null,
mousedown_node = null,
mouseup_node = null;
function resetMouseVars() {
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
function restart()
{
// redraw everything
svg.selectAll('g').remove();
path = svg.append('svg:g').selectAll('path'),
circle = svg.append('svg:g').selectAll('g');
weight = svg.append('svg:g').selectAll('text');
circle = circle.data(nodes, function(d) { return d.id; });
circle.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); });
var g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class','node')
.attr('r',12)
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; })
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.style('stroke', function(d) { return d3.rgb(colors(d.id)).darker().toString(); })
.on('mousedown', function(d) {
if (d3.event.ctrlKey) return;
mousedown_node = d;
if (mousedown_node === selected_node) selected_node = null;
else selected_node = mousedown_node;
selected_link = null;
// reposition drag line
drag_line
.style('marker-end', 'url(#end-arrow)')
.classed('hidden', false)
.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);
restart();
})
.on('mouseup', function(d) {
if (!mousedown_node) return;
drag_line
.classed('hidden', true)
.style('marker-end', '');
// check for drag-to-self
mouseup_node = d;
if(mouseup_node === mousedown_node) { resetMouseVars(); return; }
var source, target, direction;
source = mousedown_node;
target = mouseup_node;
var link;
link = links.filter(function(l) {
return (l.source === source && l.target === target);
})[0];
if(link) {
//link[direction] = true;
} else {
var dist = parseInt(Math.sqrt(Math.pow(source.x - target.x,2) + Math.pow(source.y - target.y,2))/5);
link = {source: source, target: target, weight: dist};
//link[direction] = true;
links.push(link);
}
// select new link
selected_link = link;
selected_node = null;
restart();
})
;
g.append('svg:text')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
//.attr('y', function (d) { return 4; })
.attr('class','id')
.text(function(d) { return d.id; });
//circle.exit().remove();
// drawing paths
path = path.data(links);
path.classed('selected', function(d) { return d === selected_link; });
path.enter().append('svg:path')
.attr('class','link')
.classed('selected',function(d) {return d === selected_link; })
.style('marker-end', function(d) { return 'url(#end-arrow)';})
.attr('d', function (d)
{
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist,
/*
sourcePadding = d.left ? 17 : 12,
targetPadding = d.right ? 17 : 12,
*/
sourcePadding = 12;
targetPadding = 17;
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
// check if needs to draw curve or not ?
var link;
link = links.filter(function(l) {
return (l.source === d.target && l.target === d.source);
})[0];
if (link)
{
// need curve
var type;
if (d.source.id < d.target.id) type = 1; else type = 2;
//var newX = weightXY(d.source.x,d.source.y,d.target.x,d.target.y,type,1).x;
//var newY = weightXY(d.source.x,d.source.y,d.target.x,d.target.y,type,1).y;
// change final point of arrow
var finalX = arrowXY(sourceX, sourceY, targetX, targetY, type).x;
var finalY = arrowXY(sourceX, sourceY, targetX, targetY, type).y;
var beginX = arrowXY(targetX, targetY, sourceX, sourceY, type).x;
var beginY = arrowXY(targetX, targetY, sourceX, sourceY, type).y;
//return 'M' + sourceX + ',' + sourceY + 'Q' + newX + ',' + newY + ' ' + targetX + ',' + targetY;
//return 'M' + sourceX + ',' + sourceY + 'Q' + newX + ',' + newY + ' ' + finalX + ',' + finalY;
return 'M' + beginX + ',' + beginY + 'L' + finalX + ',' + finalY;
}
else
{
// no need
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
}
// end check
})
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
})
;
// start weight display
weight = weight.data(links);
weight.enter().append('svg:text')
.attr('class','weight')
.attr('x', function(d)
{
var type;
if (d.source.id < d.target.id) type = 1; else type = 2;
var link;
link = links.filter(function(l) {
return (l.source === d.target && l.target === d.source);
})[0];
var curve = 0;
if (link) curve = 2;
var x = weightXY(d.source.x,d.source.y,d.target.x,d.target.y,type,curve).x;
console.log("X = " + x);
return x;
})
.attr('y', function(d)
{
var type;
if (d.source.id < d.target.id) type = 1; else type = 2;
var link;
link = links.filter(function(l) {
return (l.source === d.target && l.target === d.source);
})[0];
var curve = 0;
if (link) curve = 2;
var y = weightXY(d.source.x,d.source.y,d.target.x,d.target.y,type,curve).y;
console.log("Y = " + y);
return y;
})
.text(function(d) { return d.weight; })
;
}
function arrowXY(x1,y1,x2,y2,t)
{
var dist = Math.sqrt(Math.pow(x2-x1,2) + Math.pow(y2-y1,2));
//console.log(dist);
if (x1 === x2)
{
if (t === 1) return {x : x2 - 4, y : y2};
else return {x : x2 + 4, y : y2};
}
if (y1 === y2)
{
if (t === 1) return {x : x2, y : y2 - 4};
else return {x : x2, y : y2 + 4};
}
var m1 = (y2 - y1)/(x2-x1);
//console.log(m1);
var c1 = y1 - m1*x1;
//console.log(c1);
var m2 = -1 / m1;
//console.log(m2);
var c2 = y2 - m2*x2;
//console.log(c2);
var d = Math.sqrt(Math.pow(x2-x1,2) + Math.pow(y2-y1,2));
//console.log(d);
var v = 4;
d = d*d + v*v;
var D = d;
//console.log(D);
var z1 = c2 - y1;
var a = 1 + m2*m2;
var b = 2*m2*z1 - 2*x1;
var c = x1*x1 + z1*z1 - D;
var delta = b*b - 4*a*c;
delta = Math.sqrt(delta);
var x_1 = (-b + delta)/(2*a);
var y_1 = m2*x_1 + c2;
var x_2 = (-b - delta)/(2*a);
var y_2 = m2*x_2 + c2;
if (t === 2) return {x : x_1, y: y_1};
else return {x : x_2, y: y_2};
}
function weightXY(x1,y1,x2,y2,t,curve)
{
var dist = Math.sqrt(Math.pow(x2-x1,2) + Math.pow(y2-y1,2));
//console.log(dist);
var x2 = (x1 + x2)/2;
var y2 = (y1 + y2)/2;
if (x1 === x2)
{
if (t === 2) return {x : x2 + 16, y: y2};
else return {x : x2 - 16, y : y2};
}
if (y1 === y2)
{
if (t === 2) return {x : x2 , y: y2 + 16};
else return {x : x2, y : y2 - 16};
}
var m1 = (y2 - y1)/(x2-x1);
//console.log(m1);
var c1 = y1 - m1*x1;
//console.log(c1);
var m2 = -1 / m1;
//console.log(m2);
var c2 = y2 - m2*x2;
//console.log(c2);
var d = Math.sqrt(Math.pow(x2-x1,2) + Math.pow(y2-y1,2));
//console.log(d);
var v = 16;
if (curve === 1) v = 50;
if (curve === 2) v = 18;
/*
*/
d = d*d + v*v;
var D = d;
//console.log(D);
var z1 = c2 - y1;
var a = 1 + m2*m2;
var b = 2*m2*z1 - 2*x1;
var c = x1*x1 + z1*z1 - D;
var delta = b*b - 4*a*c;
delta = Math.sqrt(delta);
var x_1 = (-b + delta)/(2*a);
var y_1 = m2*x_1 + c2;
var x_2 = (-b - delta)/(2*a);
var y_2 = m2*x_2 + c2;
if (t === 2) return {x : x_1, y: y_1};
else return {x : x_2, y: y_2};
}
function mousedown() {
svg.classed('active', true);
if(d3.event.ctrlKey || mousedown_node || mousedown_link) return;
// insert new node at point
var point = d3.mouse(this),
node = {id: lastNodeId};
// find new last node ID
countNodeId[lastNodeId]++;
for (var i = 0; i < 200; i++)
if (countNodeId[i] === 0)
{
lastNodeId = i;
break;
}
node.x = point[0];
node.y = point[1];
nodes.push(node);
restart();
}
function mousemove() {
if(!mousedown_node) return;
// update drag line
drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
restart();
}
function mouseup() {
if(mousedown_node) {
// hide drag line
drag_line
.classed('hidden', true)
//.style('marker-end', '');
}
// because :active only works in WebKit?
svg.classed('active', false);
// clear mouse event vars
resetMouseVars();
}
function spliceLinksForNode(node) {
var toSplice = links.filter(function(l) {
return (l.source === node || l.target === node);
});
toSplice.map(function(l) {
links.splice(links.indexOf(l), 1);
});
}
var lastKeyDown = -1;
var drag = d3.behavior.drag()
.on("drag", function (d)
{
//console.log("dragging");
var dragTarget = d3.select(this).select('circle');
//console.log(this);
var new_cx, new_cy;
//console.log(d);
dragTarget
.attr("cx", function()
{
new_cx = d3.event.dx + parseInt(dragTarget.attr("cx"));
return new_cx;
})
.attr("cy", function()
{
new_cy = d3.event.dy + parseInt(dragTarget.attr("cy"));
return new_cy;
});
d.x = new_cx;
d.y = new_cy;
//console.log(d.x + " " + d.y);
restart();
});
function move()
{
}
function keydown() {
d3.event.preventDefault();
//if(lastKeyDown !== -1) return;
lastKeyDown = d3.event.keyCode;
// ctrl
if(d3.event.keyCode === 17) {
circle.call(drag);
svg.classed('ctrl', true);
}
if(!selected_node && !selected_link) return;
switch(d3.event.keyCode) {
//case 8: // backspace
case 46: // delete
if(selected_node)
{
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
countNodeId[selected_node.id] = 0;
for (var i = 0; i < 200; i++)
if (countNodeId[i] === 0)
{
lastNodeId = i;
break;
}
} else if(selected_link) {
links.splice(links.indexOf(selected_link), 1);
}
selected_link = null;
selected_node = null;
restart();
break;
case 13: //enter
if (selected_link)
{
var newWeight = prompt("Enter new weight : ");
var idx = links.indexOf(selected_link);
links[idx].weight = newWeight;
}
restart();
break;
}
}
function keyup() {
lastKeyDown = -1;
// ctrl
if(d3.event.keyCode === 17) {
circle
.on('mousedown.drag', null)
.on('touchstart.drag', null);
svg.classed('ctrl', false);
}
}
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
d3.select(window)
.on('keydown',keydown)
.on('keyup',keyup);
restart();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment