d3.js v4 dynamically add nodes to tidy tree - d3.js

I am trying to do something similar to this question d3.js how to dynamically add nodes to a tree. However, I'm finding it really difficult to get any kind of solution working in v4 of d3.js.
I am building a tree from some preloaded json, and then i want to be able to add child nodes onto the tree when the user clicks on a node. The process of adding child nodes will involve a call to a REST service, which will return some json. Most of my code is based heavily on http://bl.ocks.org/mbostock/4339083 which is an expandable/collapsible tidy tree using v3 and https://bl.ocks.org/mbostock/9d0899acb5d3b8d839d9d613a9e1fe04 which is a non expandable/collapsible tidy tree using v4.
My code is here https://jsfiddle.net/rkian/3e73Lz6d/. I can draw the tree out no problem but I'm really having trouble working out how to add nodes to the tree. I suspect this:
function click(d) {
if (!d.children && !d._children) {
var jsonChildren = $.parseJSON('...');
d.data.children = jsonChildren;
d.data._children = jsonChildren;
updateTree(d);
}
is key to it all but I'm just not able to work out how to implement it.

There was something odd with the lineage variable (flat array to be turned into hierarchical structure) which I was trying to build my tree up from, when I turned it into a tree it changed the objects within it. I solved this by making a deep copy of the lineage. All of my code is here, I hope it helps someone:
var lineage = $.parseJSON('[{"id":739,"name":"Life","parent":null,"rank":9,"child_count":1},{"id":1,"name":"Eukaryota","parent":739,"rank":1,"child_count":3},{"id":43,"name":"Animalia","parent":1,"rank":2,"child_count":9},{"id":2,"name":"Archaeplastida","parent":1,"rank":2,"child_count":1},{"id":740,"name":"Plantae","parent":1,"rank":2,"child_count":0},{"id":417,"name":"Annelida","parent":43,"rank":3,"child_count":1},{"id":336,"name":"Arthropoda","parent":43,"rank":3,"child_count":6},{"id":228,"name":"Chordata","parent":43,"rank":3,"child_count":3},{"id":222,"name":"Cnidaria","parent":43,"rank":3,"child_count":4},{"id":328,"name":"Mollusca","parent":43,"rank":3,"child_count":1},{"id":548,"name":"Myzozoa","parent":43,"rank":3,"child_count":1},{"id":604,"name":"Nematoda","parent":43,"rank":3,"child_count":1},{"id":467,"name":"Platyhelminthes","parent":43,"rank":3,"child_count":2},{"id":275,"name":"Porifera","parent":43,"rank":3,"child_count":2},{"id":418,"name":"Polychaeta","parent":417,"rank":4,"child_count":3},{"id":419,"name":"Spionida","parent":418,"rank":5,"child_count":1},{"id":452,"name":"Sabellida","parent":418,"rank":5,"child_count":1},{"id":448,"name":"Terebellida","parent":418,"rank":5,"child_count":1},{"id":420,"name":"Spionidae","parent":419,"rank":6,"child_count":2},{"id":699,"name":"Boccardia","parent":420,"rank":7,"child_count":1},{"id":421,"name":"Polydora","parent":420,"rank":7,"child_count":1},{"id":700,"name":"Boccardia proboscidea","parent":699,"rank":8,"child_count":0}]');
$(document).ready(function() {
// WTF js.
var lineage_flat;
// Color manipulation functions and settings for the tree
function shadeColor2(color, percent) {
var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}
function blendColors(c0, c1, p) {
var f=parseInt(c0.slice(1),16),t=parseInt(c1.slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF,R2=t>>16,G2=t>>8&0x00FF,B2=t&0x0000FF;
return "#"+(0x1000000+(Math.round((R2-R1)*p)+R1)*0x10000+(Math.round((G2-G1)*p)+G1)*0x100+(Math.round((B2-B1)*p)+B1)).toString(16).slice(1);
}
// Color variables
var main_color = '#FFFFFF';
var secondary_color = '#FFFFFF';
var line_color = '#FFF793';
var max_rank = lineage[lineage.length - 1]['rank'];
// Used to assign colors to ranks
function get_rank_colour(d) {
rank = d.data.data.rank;
if(rank == 9) {
return main_color;
}
else {
return blendColors(main_color, secondary_color, (d.data.rank / max_rank))
}
}
// Draws a curve between two points
function connector(d) {
return 'M' + d.x + ',' + (d.y - 18) +
"C" + (d.x + d.parent.x) / 2 + "," + (d.y - 25) +
" " + (d.x + d.parent.x) / 2 + "," + (d.parent.y + 25) +
" " + d.parent.x + "," + (d.parent.y + 17);
};
// Transition vars
var duration = 500;
// Get the width of the container element for the tree
width = $('#svgcontainer').width();
// Calculate the height required based on the number of node levels on the ancestry tree
height = lineage[lineage.length - 1]['rank']* 150;
// Set the svg element's height
$('#lifetree').attr('height', height + 'px');
// Select the svg element in the DOM
var svg = d3.select("svg")
// Insert a group container and move it 40 px to the right (to pad the tree contents in the svg container)
var g = svg.append("g").attr("transform", "translate(40,40)");
// Create and return a d3 tree object of the correct width and height, run the root hierarchy element through it
var tree = d3.tree().size([width-200, height - 160]);
var root = getTreeData(lineage);
updateTree(root);
// Get the data for the tree
function getTreeData(json) {
// Save the flat lineage, we have to do this weird parse thing to make a deep copy
lineage_flat = JSON.parse(JSON.stringify(json));
// This seems to unflatten arrays of objects with parentIds and parents. Wish I'd known about it sooner.
var dataTree = d3.stratify()
.id(function(d){ return d.id; })
.parentId(function(d){ return d.parent; })
(JSON.parse(JSON.stringify(json)));
// D3 requires a hierarchy object which then gets made into a tree
var root = d3.hierarchy(dataTree);
tree(root);
// Normalize for fixed-depth, also we do some fancy transitions so save a copy of original xys
root.each(function(d) { d.y = d.depth * 100; d.x0 = d.x; d.y0 = d.y; });
return root;
}
function drawElements(node) {
// Add circles above each node
node.append("circle")
.attr("r", 2)
.attr("transform", function(d) { return "translate(0,-18)"; })
.attr("class", "upper-circle")
.style("stroke", get_rank_colour)
.style("fill", get_rank_colour);
// Add the circles below each node
node.append("circle")
.attr("r", 4)
.attr("transform", function(d) { return "translate(0,16)"; })
.attr("class", "lower-circle")
.style("stroke", "#000000")
.style("fill", function(d) {
return d.data.data.child_count > 0 ? "#FFFFFF" : "#000000";
});
// Add text
node.append("text")
.attr("dy", 3)
.style("fill", '#FFFFFF')
.style("text-anchor", "middle")
.text(function(d) {
return d.data.data.name;
if(d.data.rank == max_rank || d.data.name == "Life") {
return d.data.name;
}
else if(d.children) {
return d.data.name + ' (' + d.children.length + ")";
}
else {
return d.data.name + ' (' + d.data.count + ")";
}
})
.each(function(d) {
d.textwidth = this.getBBox().width;
d.textheight = this.getBBox().height;
});
// Add clickable background rectangle so it looks nicer
node.insert("rect",":first-child")
.style("fill", '#000000')
.style("fill-opacity", function(d) {
if(d.children || d.data.data.rank == max_rank) { return 0.5; }
else { return 0.2; }
}
)
.attr('height', function(d) { return d.textheight + 10; })
.attr('width', function(d) { return d.textwidth + 10; })
.attr("transform", function(d) {
if(d.data.data.rank == 9) {
return "translate(-" + ((d.textwidth + 10) / 2) + ",-" + ((d.textheight + 30) / 2) + ")";
}
return "translate(-" + ((d.textwidth + 10) / 2) + ",-" + ((d.textheight + 15) / 2) + ")";
})
.attr('rx', 10)
.attr('ry', 10);
}
function updateTree(source, shallowestDepth = 0) {
/*
* Nodes
*/
// Data join with source data, keeping ids so it knows about the same nodes
var node = g.selectAll(".node")
.data(source.descendants() , function(d) { return d.data.id; });
// Data enter, this starts doing things to all the new nodes
var nodeEnter = node.enter()
.append("g")
.attr("class", function(d) { return "rank-" + d.data.data.rank + " node" + (d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) {
if(d.parent != null) {
return "translate(" + d.parent.x + "," + d.parent.y + ")";
}
return "translate(" + d.x + "," + d.y + ")";
})
.on("click", click);
// Add text + bg + circles to the nodes
drawElements(nodeEnter);
// Add pretty hover class for each taxon node
$('g').hover(function() {
$(this).children('rect').addClass('recthover');
}, function() {
$(this).children('rect').removeClass('recthover');
});
// Transition nodes to their new position.
var nodeMerge = node.merge(nodeEnter).transition()
.duration(duration)
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
nodeMerge.select('rect', ':first-child').style("fill-opacity", function(d) {
if(d.children || d.data.data.rank == max_rank) { return 0.5; }
else { return 0.2; }
});
// Get the old elements for removal
var oldNode = node.exit();
// Find the shallowest depth in the old element, that's the parent
oldNode.each(function(d) {
var shallowestParent = d;
do { shallowestParent = shallowestParent.parent; }
while(shallowestParent.depth > shallowestDepth);
d.shallowestParentX = shallowestParent.x;
d.shallowestParentY = shallowestParent.y;
});
// Transition the old nodes out
var transitionedNodes = oldNode.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.shallowestParentX + "," + d.shallowestParentY + ")";
});
oldNode.selectAll('rect').transition()
.style("fill-opacity", 0)
.duration(duration/2)
oldNode.selectAll('text').transition()
.style("fill-opacity", 0)
.duration(duration/2)
oldNode.selectAll('circle').transition()
.style("fill-opacity", 0)
.duration(duration/3)
transitionedNodes.remove();
/*
* Links
*/
var link = g.selectAll(".link")
.data(source.descendants().slice(1).reverse(), function(d) { return d.data.id; })
// Draw the links between nodes
var linkEnter = link.enter()
.insert("path",":first-child")
.attr("class", "link")
.style("stroke", function(d) {
if(d.children) {
return line_color;
}
return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
})
.attr("d", function (d) {
var o = {x: d.parent.x0, y: d.parent.y0, parent: {x: d.parent.x0, y: d.parent.y0}};
return connector(o);
});
// Transition links to their new position.
var linkMerge = link.merge(linkEnter).transition()
.duration(0)
.attr('d', connector);
// Style the links
linkMerge.style("stroke", function(d) {
if(d.children) {
return line_color;
}
return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
})
// Transition the old links out
var oldLink = link.exit();
oldLink.transition()
.duration(duration/2)
.attr("d", function(d) {
var o = {x: d.x, y: d.y, parent: {x: d.x, y: d.y}};
return connector(o);
})
.remove();
}
// Toggle children on click.
function click(d) {
// If the node does not have any pre-loaded children
if (!d.children && !d._children) {
var jsonPath = '/taxa/api/children/' + d.data.id;
// Get the JSON lineage for it
d3.json(jsonPath, function(error, json) {
// Get the children
children = json['children'];
// Make a new lineage array, can't use the one previously stored because
// of javascript variables mutability being weird
new_lineage = [];
lineage_flat.forEach(function(node, i) {
recalcIndex = lineage.indexOf(node);
// The node.rank 9 is in there because Life for some reason has rank 9
// Basically we want to exclude all nodes of a lower rank than the one clicked
if(node.rank > d.data.data.rank && node.rank != 9) {
// console.log(node); - we don't want these nodes
}
else { new_lineage.push(node); }
});
// Append the children to the new lineage
children.forEach(function(child) {
new_lineage.push(child);
});
// Javascript is weird. We need a deep copy of new_lineage
temp = JSON.parse(JSON.stringify(new_lineage));
new_lineage = JSON.parse(JSON.stringify(temp));
// Turn it into a tree and update our svg
root = getTreeData(new_lineage);
updateTree(root, d.depth);
});
}
}
});
.link {
fill: none;
stroke: orange;
stroke-width: 1.5px;
/* Transition. */
-o-transition:.5s;
-ms-transition:.5s;
-moz-transition:.5s;
-webkit-transition:.5s;
/* ...and now for the proper property */
transition:.5s;
}
.link:hover {
fill: none;
stroke: orange;
stroke-width: 3px;
}
.link.warning{
stroke: orange;
}
.lower-circle, .upper-circle {
z-index: 1;
stroke-width: 2px;
}
path.link {
stroke-width: 3px;
}
.rank-9 .upper-circle {
display: none;
}
.rank-9 text {
font-size: 3em;
font-weight: bold;
}
.rank-1 {
font-size: 1.5em;
font-weight: bold;
}
.rank-2 {
font-size: 1em;
font-weight: bold;
margin-right: 20px;
margin-left: 20px;
}
svg text {
}
.rank-3 {
font-size: 1em;
}
#triangles {
/*position: relative;
top: -20px;
margin-top: -50px;
padding-top: 80px;
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgcontainer {
position: relative;
padding-top: 80px;
/*-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgparent-disablethis {
background: transparent url('static/img/carousel/1.jpg' 0 0 no-repeat );
background: -moz-linear-gradient(top, rgba(255,255,255,0) 100%, rgba(130,91,0,1) 0%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0)), color-stop(100%,rgba(130,91,0,1))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#825b00',GradientType=0 ); /* IE6-9 */
}
svg { width: 100%; height: auto; }
rect {
/* Transition. */
-o-transition:.2s;
-ms-transition:.2s;
-moz-transition:.2s;
-webkit-transition:.2s;
/* ...and now for the proper property */
transition:.2s;
}
text {
cursor: pointer;
}
.lower-circle {
cursor: pointer;
}
.lower-circle:hover {
fill: #FFF !important;
}
.recthover {
fill: #FFEC1C !important;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="svgparent">
<div id="svgcontainer">
<svg id='lifetree'></svg>
</div>
</div>

Related

How to drag nodes with children in d3 circle packing?

I have been trying to drag in d3 packed nodes using this code as a basis https://codepen.io/MrHen/pen/GZQOPW. Unfortunately I can't find a method to use so that when a node with children is dragged around it's (visible) children move with the parent. I use this function to drag circles around:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx;
d.y += d3.event.dy;
draw();
})
I the example above for instance I want to be able to drag the nodes on layer 1 (light blue) and when I do this their children change their position so they stay (visually) in the borders of their parents.
Thank you!
First remove the pointer-events: none on the root and middle nodes. Then set yourself up a little recursion to walk a node's descendants and update their position:
function recurOnChildren(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
if (!d.children) return;
d.children.forEach(c => {
recurOnChildren(c);
});
}
And call from your drag handler:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
recurOnChildren(d);
draw();
})
Running code:
var margin = 20,
diameter = 960;
var color = d3.scale.linear()
.domain([-1, 5])
.range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl);
var pack = d3.layout.pack()
.padding(2)
.size([diameter - margin, diameter - margin])
.value(function(d) {
return d.size;
})
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
d3.json("https://gist.githubusercontent.com/mbostock/7607535/raw/695f8ed6298c06a946406c707200a1f6b21875d8/flare.json", function(error, root) {
if (error) throw error;
var focus = root,
nodes = pack.nodes(root),
view;
function recurOnChildren(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
if (!d.children) return;
d.children.forEach(c => {
recurOnChildren(c);
});
}
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
recurOnChildren(d);
draw();
})
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {
return d.parent ? d.children ? "node node--middle" : "node node--leaf" : "node node--root";
})
.style("fill", function(d) {
return d.children ? color(d.depth) : null;
});
var text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) {
return d.parent === root ? 1 : 0;
})
.style("display", function(d) {
return d.parent === root ? "inline" : "none";
})
.text(function(d) {
return d.name;
});
var node = svg.selectAll("circle,text");
svg.selectAll(".node").call(drag);
d3.select("body")
.style("background", color(-1))
draw();
function draw() {
var k = diameter / (root.r * 2 + margin);
node.attr("transform", function(d) {
return "translate(" + (d.x - root.x) * k + "," + (d.y - root.y) * k + ")";
});
circle.attr("r", function(d) {
return d.r * k;
});
}
});
.node {
cursor: pointer;
}
.node:hover {
stroke: #000;
stroke-width: 1.5px;
}
.node--leaf {
fill: white;
}
.label {
font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
text-anchor: middle;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
}
.label {
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

D3 Nodes not appearing while updating it realtime

I am playing with the Mike Bostock's mobile patent suits example
I am adding ~100 nodes using forEach loop in this JSFiddle; In reality, these nodes will be passed by an external service in real-time.
The page is loading a few static nodes initially. The reset button calls the forEach loop to create the new nodes using the function add_prc. This method pushes the node to nodes list (used to display in SVG) and then calls the refresh method but the nodes do not appear on the screen properly.
I see a dot at top-left of the screen but I can't select it or drag it to the center.
Once you add the nodes/links using the forEach loop, you aren't translating them anywhere. Check out this screenshot of the console:
As I mentioned in the comments, the tick function applies transform to the previously added path, circle and text but does it add anything to the newly added nodes, links? NO. So that's the thing that's missing. As I know your previous question, I'm adding the code from that to this:
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
Now, this would add all nodes at (100, 100) (I know you're just testing things). Anyway, I've used random x and ys. Here's a snippet and a JSFIDDLE
.node {
fill: #000;
}
.cursor {
fill: green;
stroke: brown;
pointer-events: none;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
#licensing {
fill: green;
}
.link.licensing {
stroke: green;
}
.link.resolved {
stroke-dasharray: 0,2 1;
}
circle {
fill: green;
stroke: red;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
<button id="reset" onclick="reset()">reset</button>
<button id="ref" onclick="refresh()">refresh</button>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960, height = 500;
var links = [{source:"simulator",target:"monitor" ,type:"resolved"} , {source:"web",target:"monitor" ,type:"resolved"} ];
var links1 = [{"source":"ctfa","target":"tfa"},
{"source":"cea","target":"tea"},
{"source":"ctfe","target":"tfe"},
{"source":"ctee","target":"tee"},
{"source":"ctfu","target":"tfu"},
{"source":"cteu","target":"teu"},
{"source":"rfa","target":"tfa"},
{"source":"rea","target":"tea"},
{"source":"rfe","target":"tfe"},
{"source":"ree","target":"tee"},
{"source":"rfu","target":"tfu"},
{"source":"reu","target":"teu"},
{"source":"r1fa","target":"rfa"},
{"source":"r1fa","target":"gfa"},
{"source":"r1fa","target":"ggf"},
{"source":"r1ea","target":"rea"},
{"source":"r1ea","target":"gea"},
{"source":"r1ea","target":"gge"},
{"source":"r1fe","target":"rfe"},
{"source":"r1fe","target":"gfe"},
{"source":"r1fe","target":"ggf"},
{"source":"r1ee","target":"ree"},
{"source":"r1ee","target":"gee"},
{"source":"r1ee","target":"gge"},
{"source":"r1fu","target":"rfu"},
{"source":"r1fu","target":"gfu"},
{"source":"r1fu","target":"ggf"},
{"source":"r1eu","target":"reu"},
{"source":"r1eu","target":"geu"},
{"source":"r1eu","target":"gge"},
{"source":"hh1fa","target":"ggf"},
{"source":"hh1ea","target":"gge"},
{"source":"hh1fe","target":"ggf"},
{"source":"hh1ee","target":"gge"},
{"source":"hh1fu","target":"ggf"},
{"source":"hh1eu","target":"gge"},
{"source":"dbfa","target":"gfa"},
{"source":"dbea","target":"gea"},
{"source":"dbfe","target":"gfe"},
{"source":"dbee","target":"gee"},
{"source":"dbfu","target":"gfu"},
{"source":"dbeu","target":"geu"},
{"source":"hflse","target":"tee"},
{"source":"hfnyse","target":"teu"},
{"source":"hfnse","target":"teu"},
{"source":"hfret","target":"tfu"},
{"source":"hfebs","target":"tfe"},
{"source":"hfint","target":"tfu"},
{"source":"c1e","target":"ctee"},
{"source":"c1e","target":"gge"},
{"source":"c2e","target":"ctee"},
{"source":"c3e","target":"cteu"},
{"source":"c4e","target":"cteu"},
{"source":"c5e","target":"ggf"},
{"source":"d1e","target":"ctee"},
{"source":"c1f","target":"ctfe"},
{"source":"c2f","target":"ctfe"},
{"source":"c3f","target":"ggf"},
{"source":"c4f","target":"gge"},
{"source":"c5f","target":"ctfa"},
{"source":"d1f","target":"ctfe"}];
var nodes1 = [{"id":"tfa"},
{"id":"tea"},
{"id":"tfe"},
{"id":"tee"},
{"id":"tfu"},
{"id":"teu"},
{"id":"ctfa"},
{"id":"cea"},
{"id":"ctfe"},
{"id":"ctee"},
{"id":"ctfu"},
{"id":"cteu"},
{"id":"rfa"},
{"id":"rea"},
{"id":"rfe"},
{"id":"ree"},
{"id":"rfu"},
{"id":"reu"},
{"id":"r1fa"},
{"id":"r1ea"},
{"id":"r1fe"},
{"id":"r1ee"},
{"id":"r1fu"},
{"id":"r1eu"},
{"id":"hh1fa"},
{"id":"hh1ea"},
{"id":"hh1fe"},
{"id":"hh1ee"},
{"id":"hh1fu"},
{"id":"hh1eu"},
{"id":"dbfa"},
{"id":"dbea"},
{"id":"dbfe"},
{"id":"dbee"},
{"id":"dbfu"},
{"id":"dbeu"},
{"id":"gfa"},
{"id":"gea"},
{"id":"gfe"},
{"id":"gee"},
{"id":"gfu"},
{"id":"geu"},
{"id":"gge"},
{"id":"ggf"},
{"id":"hflse"},
{"id":"hfnyse"},
{"id":"hfnse"},
{"id":"hfret"},
{"id":"hfebs"},
{"id":"hfint"},
{"id":"c1e"},
{"id":"c2e"},
{"id":"c3e"},
{"id":"c4e"},
{"id":"c5e"},
{"id":"d1e"},
{"id":"c1f"},
{"id":"c2f"},
{"id":"c3f"},
{"id":"c4f"},
{"id":"c5f"},
{"id":"d1f"}];
var nodes = [ {"id":"monitor", "grp":"system"}, {"id":"simulator", "grp":"system"}, {id:"web", grp:"client"}];
function reset() {
nodes1.forEach(function(d){ add_prc(d) });
links1.forEach(function(d){ add_con(d) });
}
function add_prc(newNode) {
//console.log(newNode);
addNodeCanvas(newNode.id,newNode.grp);
}
function add_con(newConnection) {
//console.log(newConnection);
addLinkCanvas( newConnection.source,newConnection.target);
}
//setInterval(refresh, 15000);
function addNodeCanvas(nodeName,g) {
var node1 = { x: Math.floor(Math.random()*200+100), y: Math.floor(Math.random()*200+100), id: nodeName, grp:g };
var n = nodes.push(node1);
//console.log(node1);
refresh();
}
function addLinkCanvas(idSrc, idTarget) {
if (idSrc != idTarget) {
var s = {}, t = {};
nodes.forEach(function(curNode) {
if (typeof curNode.id != "undefined") {
if (curNode.id == idSrc) { s = curNode; }
if (curNode.id == idTarget) { t = curNode; }
}
});
//console.log( { s,t});
links.push({ source: s, target: t });
};
refresh();
}
var width = 900,
height = 600,
radius = 8;
var map = {}
nodes.forEach(function(d,i){
map[d.id] = i;
})
links.forEach(function(d) {
d.source = map[d.source];
d.target = map[d.target];
})
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Per-type markers, as they don't inherit styles.
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
var circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter().append("circle")
.attr("r", 6)
.call(force.drag);
var text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter().append("text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.id; });
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", linkArc);
circle.attr("transform", transform);
text.attr("transform", transform);
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function refresh(){
node = node.data(nodes);
node.enter().insert("circle", ".cursor")
.attr("class", "node")
.attr("r", 5)
.on("mousedown", mousedownNode);
node.exit()
.remove();
link = link.data(links);
link.enter().insert("line", ".node")
.attr("class", "link");
link.exit()
.remove();
force.start();
}
</script>

d3.js graphics not showing in Jupyter notebook output HTML

I am using a d3.js visualisation that works perfectly in the Jupyter notebook with the d3 cell magic. However, when I output the notebook to html, I just get a blank space.
Below is the code. Does anyone know how to make this work with the html output? Ideally I would like the chart to remain interactive.
%%d3 4.1
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
cursor: pointer;
}
.node:hover {
stroke: #000;
stroke-width: 1.5px;
}
.node--leaf {
fill: white;
}
.label {
font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
text-anchor: middle;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
}
.label,
.node--root,
.node--leaf {
pointer-events: none;
}
</style>
<svg width="760" height="760"></svg>
<script>
require.config({
paths: {
d3: "https://d3js.org/d3.v4.min.js"
}
});
require(["d3"], function(d3)
{
console.log(d3);
var svg = d3.select("svg"),
margin = 20,
diameter = +svg.attr("width"),
g = svg.append("g").attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
var color = d3.scaleSequential(d3.interpolateViridis)
.domain([-4, 4]);
var pack = d3.pack()
.size([diameter - margin, diameter - margin])
.padding(2);
d3.json("output.json", function(error, root) {
if (error) throw error;
root = d3.hierarchy(root)
.sum(function(d) { return d.size; })
.sort(function(a, b) { return b.value - a.value; });
var focus = root,
nodes = pack(root).descendants(),
view;
var circle = g.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) { if (focus !== d) zoom(d), d3.event.stopPropagation(); });
var text = g.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
.text(function(d) { return d.data.name; });
var node = g.selectAll("circle,text");
svg
.style("background", color(-1))
.on("click", function() { zoom(root); });
zoomTo([root.x, root.y, root.r * 2 + margin]);
function zoom(d) {
var focus0 = focus; focus = d;
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween("zoom", function(d) {
var i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2 + margin]);
return function(t) { zoomTo(i(t)); };
});
transition.selectAll("text")
.filter(function(d) { return d.parent === focus || this.style.display === "inline"; })
.style("fill-opacity", function(d) { return d.parent === focus ? 1 : 0; })
.on("start", function(d) { if (d.parent === focus) this.style.display = "inline"; })
.on("end", function(d) { if (d.parent !== focus) this.style.display = "none"; });
}
function zoomTo(v) {
var k = diameter / v[2]; view = v;
node.attr("transform", function(d) { return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")"; });
circle.attr("r", function(d) { return d.r * k; });
}
});
});
</script>
The data that you are use to input here:
d3.json("output.json", function(error, root) {if (error) throw error;"
Are you sure that is on the right path?
For me, I got a similar problem and was because the input data path was incorrect.

Wrapping multi line labels

I'm building a responsive zoomable treemap based on this project. The problem is that the labels I've are longer than the original visualization and end up not showing:
function text(text) {
text.selectAll("tspan")
.attr("x", function(d) { return x(d.x) + 6; })
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; })
.style("opacity", function(d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
}
The problem I see is that the text needs to be showed completely in the first line, but I'd like to show it in multiple lines (inside the rect) instead.
There's a code by Mike Bostock which seems to solve this issue but I don't know how to apply it to the treemap.
Here's a quick a modification which wraps the parent text in that example:
<!DOCTYPE html>
<!--
Generic treemap, based on http://bost.ocks.org/mike/treemap/
-->
<html>
<head>
<meta charset="utf-8">
<title>Zoomable treemap template</title>
<style>
#chart {
background: #fff;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.title {
font-weight: bold;
font-size: 24px;
text-align: center;
margin-top: 6px;
margin-bottom: 6px;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
rect.parent {
pointer-events: none;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
window.addEventListener('message', function(e) {
var opts = e.data.opts,
data = e.data.data;
return main(opts, data);
});
var defaults = {
margin: {
top: 24,
right: 0,
bottom: 0,
left: 0
},
rootname: "TOP",
format: ",d",
title: "",
width: 960,
height: 500
};
function main(o, data) {
var root,
opts = $.extend(true, {}, defaults, o),
formatNumber = d3.format(opts.format),
rname = opts.rootname,
margin = opts.margin,
theight = 36 + 16;
$('#chart').width(opts.width).height(opts.height);
var width = opts.width - margin.left - margin.right,
height = opts.height - margin.top - margin.bottom - theight,
transitioning;
var color = d3.scale.category20c();
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function(d, depth) {
return depth ? null : d._children;
})
.sort(function(a, b) {
return a.value - b.value;
})
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
if (opts.title) {
$("#chart").prepend("<p class='title'>" + opts.title + "</p>");
}
if (data instanceof Array) {
root = {
key: rname,
values: data
};
} else {
root = data;
}
initialize(root);
accumulate(root);
layout(root);
console.log(root);
display(root);
if (window.parent !== window) {
var myheight = document.documentElement.scrollHeight || document.body.scrollHeight;
window.parent.postMessage({
height: myheight
}, '*');
}
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.values) ? d.value = d.values.reduce(function(p, v) {
return p + accumulate(v);
}, 0) : d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({
_children: d._children
});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) {
return d._children;
})
.classed("children", true)
.on("click", transition);
var children = g.selectAll(".child")
.data(function(d) {
return d._children || [d];
})
.enter().append("g");
children.append("rect")
.attr("class", "child")
.call(rect)
.append("title")
.text(function(d) {
return d.key + " (" + formatNumber(d.value) + ")";
});
children.append("text")
.attr("class", "ctext")
.text(function(d) {
return d.key;
})
.call(text2);
g.append("rect")
.attr("class", "parent")
.call(rect);
var t = g.append("text")
.attr("class", "ptext")
.attr("dy", ".75em")
t.append("tspan")
.text(function(d) {
return d.key;
});
t.append("tspan")
.attr("dy", "1.0em")
.text(function(d) {
return formatNumber(d.value);
});
t.call(text);
g.selectAll("rect")
.style("fill", function(d) {
return color(d.key);
});
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) {
return a.depth - b.depth;
});
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll(".ptext").call(text).style("fill-opacity", 0);
t1.selectAll(".ctext").call(text2).style("fill-opacity", 0);
t2.selectAll(".ptext").call(text).style("fill-opacity", 1);
t2.selectAll(".ctext").call(text2).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.selectAll("tspan")
.attr("x", function(d) {
return x(d.x) + 6;
})
text.attr("x", function(d) {
return x(d.x) + 6;
})
.attr("y", function(d) {
return y(d.y) + 6;
})
.each(function(d) {
var tspan = this.childNodes[0];
var w = x(d.x + d.dx) - x(d.x);
wrap(tspan, w, x(d.x) + 6);
})
}
function text2(text) {
text.attr("x", function(d) {
return x(d.x + d.dx) - this.getComputedTextLength() - 6;
})
.attr("y", function(d) {
return y(d.y + d.dy) - 6;
})
.style("opacity", function(d) {
return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
});
}
function rect(rect) {
rect.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", function(d) {
return x(d.x + d.dx) - x(d.x);
})
.attr("height", function(d) {
return y(d.y + d.dy) - y(d.y);
});
}
function name(d) {
return d.parent ? name(d.parent) + " / " + d.key + " (" + formatNumber(d.value) + ")" : d.key + " (" + formatNumber(d.value) + ")";
}
}
if (window.location.hash === "") {
d3.json("https://jsonblob.com/api/7c30e101-da91-11e6-90ab-11c211a4b3d5", function(err, res) {
if (!err) {
console.log(res);
var data = d3.nest().key(function(d) {
return d.region;
}).key(function(d) {
return d.subregion;
}).entries(res);
main({
title: "World Population"
}, {
key: "World",
values: data
});
}
});
}
function wrap(tspan, width, x) {
var text = d3.select(tspan),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0.4,
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", "0.75em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", "1em").text(word);
}
}
}
</script>
</body>
</html>

How can I click to add or drag in D3?

I get the impression this question is so simple nobody has bothered to make a demo of it, but I don't know enough D3 (yet) to see what I'm doing wrong. The behavior I'm looking for is if the user clicks where there's not a circle it will create one there, and if they drag an existing circle a new one will not be created, but the one they drag will move.
My current attempt is as follows
points = []
drag = d3.behavior.drag()
.origin((d) -> d)
.on("dragstart", dragstarted)
.on("dragend", dragended)
dragstarted = (d) ->
d3.event.sourceEvent.stopPropagation
d3.select(this).classed("dragging", true)
dragended = (d) ->
d3.select(this).classed("dragging", false)
mousedown = ->
return if d3.event.defaultPrevented
point = d3.mouse(this)
points[points.length] = {x: point[0], y: point[1]}
svg.selectAll("circle").data(points).enter().append("circle")
.attr("cx", (n) -> n.x)
.attr("cy", (n) -> n.y)
.attr("r", "5")
.attr("class", "dot")
.call(drag)
svg = d3.select("body").append("svg")
.attr("width", 700)
.attr("height", 400)
.on("mousedown", mousedown)
First off, you definitely have the right idea for how to add points on mousedown. The two things I'd change are:
Use click instead of mousedown, so if you click existing points you don't add a new point on top of the existing one.
Add one point at a time, instead of re-adding all the points on each click.
Here's a working click function:
function click(){
// Ignore the click event if it was suppressed
if (d3.event.defaultPrevented) return;
// Extract the click location\
var point = d3.mouse(this)
, p = {x: point[0], y: point[1] };
// Append a new point
svg.append("circle")
.attr("transform", "translate(" + p.x + "," + p.y + ")")
.attr("r", "5")
.attr("class", "dot")
.style("cursor", "pointer")
.call(drag);
}
Then, when dragging, it is simplest to move a circle using translate (which is also why I
use translate when creating points above). The only real step is to extract the drag's x and y locations. Here's a working example of drag behavior.
var drag = d3.behavior.drag()
.on("drag", dragmove);
function dragmove(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
I put all this together in this jsfiddle.
Finally, here's a relevant SO question I read when constructing the drag example: How to drag an svg group using d3.js drag behavior?.
here an example on how to create a node upon mouse click:
http://bl.ocks.org/rkirsling/5001347
and here is an example on how to drag and drop a node.
Study both examples and you will get your answer. Also put you current example in jsfiddle
so it's possible to see what's wrong.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Directed Graph Editor</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
</body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="app.js"></script>
</html>
app.css
svg {
background-color: #FFF;
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: 4px;
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: 1.5px;
cursor: pointer;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.id {
text-anchor: middle;
font-weight: bold;
}
app.js
// set up SVG for D3
var width = 960,
height = 500,
colors = d3.scale.category10();
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
{id: 0, reflexive: false},
{id: 1, reflexive: true },
{id: 2, reflexive: false}
],
lastNodeId = 2,
links = [
{source: nodes[0], target: nodes[1], left: false, right: true },
{source: nodes[1], target: nodes[2], left: false, right: true }
];
// init D3 force layout
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(150)
.charge(-500)
.on('tick', tick)
// define arrow markers for graph links
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');
svg.append('svg:defs').append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 4)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#000');
// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
circle = svg.append('svg:g').selectAll('g');
// mouse event vars
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;
}
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.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,
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
});
circle.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// update existing links
path.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; });
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.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();
});
// remove old links
path.exit().remove();
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, function(d) { return d.id; });
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.classed('reflexive', function(d) { return d.reflexive; });
// add new nodes
var g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.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(); })
.classed('reflexive', function(d) { return d.reflexive; })
.on('mouseover', function(d) {
if(!mousedown_node || d === mousedown_node) return;
// enlarge target node
d3.select(this).attr('transform', 'scale(1.1)');
})
.on('mouseout', function(d) {
if(!mousedown_node || d === mousedown_node) return;
// unenlarge target node
d3.select(this).attr('transform', '');
})
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select node
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;
// needed by FF
drag_line
.classed('hidden', true)
.style('marker-end', '');
// check for drag-to-self
mouseup_node = d;
if(mouseup_node === mousedown_node) { resetMouseVars(); return; }
// unenlarge target node
d3.select(this).attr('transform', '');
// add link to graph (update if exists)
// NB: links are strictly source < target; arrows separately specified by booleans
var source, target, direction;
if(mousedown_node.id < mouseup_node.id) {
source = mousedown_node;
target = mouseup_node;
direction = 'right';
} else {
source = mouseup_node;
target = mousedown_node;
direction = 'left';
}
var link;
link = links.filter(function(l) {
return (l.source === source && l.target === target);
})[0];
if(link) {
link[direction] = true;
} else {
link = {source: source, target: target, left: false, right: false};
link[direction] = true;
links.push(link);
}
// select new link
selected_link = link;
selected_node = null;
restart();
});
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'id')
.text(function(d) { return d.id; });
// remove old nodes
circle.exit().remove();
// set the graph in motion
force.start();
}
function mousedown() {
// prevent I-bar on drag
//d3.event.preventDefault();
// because :active only works in WebKit?
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, reflexive: false};
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);
});
}
// only respond once per keydown
var lastKeyDown = -1;
function keydown() {
d3.event.preventDefault();
if(lastKeyDown !== -1) return;
lastKeyDown = d3.event.keyCode;
// ctrl
if(d3.event.keyCode === 17) {
circle.call(force.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);
} else if(selected_link) {
links.splice(links.indexOf(selected_link), 1);
}
selected_link = null;
selected_node = null;
restart();
break;
case 66: // B
if(selected_link) {
// set link direction to both left and right
selected_link.left = true;
selected_link.right = true;
}
restart();
break;
case 76: // L
if(selected_link) {
// set link direction to left only
selected_link.left = true;
selected_link.right = false;
}
restart();
break;
case 82: // R
if(selected_node) {
// toggle node reflexivity
selected_node.reflexive = !selected_node.reflexive;
} else if(selected_link) {
// set link direction to right only
selected_link.left = false;
selected_link.right = true;
}
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);
}
}
// app starts here
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
d3.select(window)
.on('keydown', keydown)
.on('keyup', keyup);
restart();

Resources