- 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
-
-
Save mayblue9/94c1ce40946f96935bcc to your computer and use it in GitHub Desktop.
[Parallel] Directed / Weighted
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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