tungnk1993
6/18/2014 - 6:08 PM

[Curve] Directed / Weighted

[Curve] Directed / Weighted

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;
				
				//return 'M' + sourceX + ',' + sourceY + 'Q' + newX + ',' + newY + ' ' + targetX + ',' + targetY;
				return 'M' + sourceX + ',' + sourceY + 'Q' + newX + ',' + newY + ' ' + 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 - 8, y : y2};
		else return {x : x2 + 8, y : y2};
	}
	
	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 = 8;
	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) return {x : midX + 10, y: midY};
	
	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 = 50;
	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();
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;
}
<!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.js"></script>
  <script src="visual.js"></script>
</html>
  • 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