Org Chart - Cluster Layout V3 to V4 - d3.js

I'm trying to update this Org Chart visualisation to use V4 of d3 but I'm hitting problems. This JSFiddle uses V3 while this JSFiddle uses V4.
I've run into a few things that have changed, for instance the CSV parsing (lines 53-54):
// var data = d3.csv.parse(csvData);
var data = d3.csvParse(csvData);
And the calculation of diagonal (discussed here, lines 79-97):
/*
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) {
return [d.y, d.x / 180 * Math.PI];
});
*/
var diagonal = function n(n, i) {
var u = t.call(this, n, i),
o = e.call(this, n, i),
a = (u.y + o.y) / 2,
l = [u, {
x: u.x,
y: a
}, {
x: o.x,
y: a
}, o];
return l = l.map(r), "M" + l[0] + "C" + l[1] + " " + l[2] + " " + l[3]
}
And the geting of nodes has changed (lines 211-212):
//var nodes = cluster.nodes(root);
var nodes = d3.hierarchy(root);
Now I seem to be hitting an issue with those nodes with this error message for line 216:
Uncaught TypeError: cluster.links is not a function
Any help greatly appreciated, I'm no slouch when it comes to JS but this is my first foray into d3 and I'm getting really rather lost :-(.

Live demo:
var csvData = `Associate,Manager
Matt Herman,John Smith
Jane Doe,John Smith
Adam Brown,John Smith
Susan Harris,John Smith
Mike Jones,John Smith
John Smith,Colin Krauss
Colin Krauss,Ashley Carlin
Ashley Carlin,Lia McDermott
Evan Park,Lia McDermott
Lauren Werner,Evan Park
Shane Waterson,Evan Park
Emma Smith,Evan Park
Mike Gregory,Evan Park
Jose Biggleman,Evan Park
Michelle Spektor,Evan Park
Juan Branch,Evan Park
John Orbase,Evan Park
Matt McCloud,Evan Park
Kelsey Carsen,Evan Park
Kelli Krazwinski,Colin Krauss
Stephanie Goldstien,Colin Krauss
Ryan Woolwine,Colin Krauss
Kyle Bohm,Colin Krauss
Sydney Yellen,Colin Krauss
Shankar Murjhree,Colin Krauss
Wayne Ellington,Colin Krauss
Dwight Folds,Colin Krauss
Ellen McGlynn,Colin Krauss
Nicolas Smith,Colin Krauss
Molly Ercole,Colin Krauss
Scott Hane,Colin Krauss
Regina McMahon,Colin Krauss
Skip Holden,Colin Krauss
Kadeem McPherson,Colin Krauss
Ray Ortiz,Colin Krauss
Janet Barnes,Colin Krauss
Holly Gold,Colin Krauss
Lance Martinez,Ashley Carlin
Mike Lubow,Ashley Carlin
Jordan Belsin,Ashley Carlin
Tom Strithers,Ashley Carlin
Jamie Raleigh,Ellen McGlynn
Joseph Bowman,Ellen McGlynn
Kylie Branch,Ellen McGlynn
Lars Randall,Ellen McGlynn
Carlos Barndt,Lia McDermott
Leo Hastings,Lia McDermott
Jaime Kellemen,Lia McDermott
Harvey Klien,Lia McDermott
Lia McDermott,Lia McDermott`;
var data = d3.csvParse(csvData);
var height = document.getElementById("tree-container").offsetHeight;
var width = document.getElementById("tree-container").offsetWidth;
var avatarRadius = 20;
var translateOffset = 25;
var radius = d3.min([height, width]) / 2;
var cluster = d3.cluster()
.size([360, radius / 1.33])
// .separation(function(a,b){return (a.parent == b.parent ? 1:2)/a.depth;});
var svg = d3.select("#tree-container").append("svg")
.attr("width", radius * 2)
.attr("height", radius * 2)
.attr("id", "tree-container-svg")
.append("g")
.attr("transform", "translate(" + radius + "," + height / 2 + ")");
//Clip path needed for cicrular SVG avatars
var defs = svg.append('defs');
var clipPath = defs.append('clipPath')
.attr('id', 'clip-circle')
.append('circle')
.attr('r', avatarRadius - 2.5);
function project(x, y) {
var angle = (x - 90) / 180 * Math.PI,
radius = y;
return [radius * Math.cos(angle), radius * Math.sin(angle)];
}
var diagonal = function (d) {
return "M" + project(d.x, d.y) + "C" + project(d.x, (d.y + d.parent.y) / 2) +
" " + project(d.parent.x, (d.y + d.parent.y) / 2) +
" " + project(d.parent.x, d.parent.y);
}
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function () {
return this.each(function () {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
//http://www.d3noob.org/2014/01/tree-diagrams-in-d3js_11.html
function treeify(list, callback) {
var dataMap = list.reduce(function (map, node) {
map[node.Associate] = node;
return map;
}, {});
var treeData = [];
list.forEach(function (node) {
//Assuming the highest node is the last in the csv file
if (node.Manager === node.Associate) {
node.Manager = "Board of Directors"
callback(node);
}
// add to parent
var parent = dataMap[node.Manager];
if (parent) {
// create child array if it doesn't exist
(parent.children || (parent.children = []))
// add node to child array
.push(node);
} else {
// parent is null or missing
treeData.push(node);
}
});
};
function findItem(root, name, callback) {
var stack = [];
stack.push(root);
while (stack.length !== 0) {
var element = stack.pop();
if (element.Associate === name) {
callback(element);
return;
}
//The up, uncompressed case
else if (element.children !== undefined && element.children.length > 0) {
for (var i = 0; i < element.children.length; i++) {
stack.push(element.children[i]);
}
}
//The down (compressed) case
else if (element._children !== undefined && element._children.length > 0) {
for (var j = 0; j < element._children.length; j++) {
stack.push(element._children[j]);
}
}
}
}
function defaultPlot(root, elem) {
findItem(root, elem, function (d) {
//Showing 1 up and below
findItem(root, d.Manager, function (x) {
(x.children) ? x.children.forEach(collapse): x.children = x._children;
drawIt(x, root);
})
})
}
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = undefined;
}
}
//For the buggy transition interruption with many nodes
function showAllCurrentPathsAndNodes() {
d3.selectAll(".link").style("opacity", 1);
d3.selectAll(".node").style("opacity", 1);
}
// Toggle children on click.
function clickedNode(d, root) {
//Accounting for the transition bug on the delay
showAllCurrentPathsAndNodes();
if (d.children) {
d._children = d.children;
d.children = undefined;
drawIt(root)
} else {
d.children = d._children;
d._children = undefined;
drawIt(root)
}
}
//http://bl.ocks.org/syntagmatic/4092944
function drawIt(root) {
var nodes = d3.hierarchy(root);
cluster(nodes);
var links = nodes.descendants().slice(1);
var link = svg.selectAll("path.link").data(links);
var node = svg.selectAll("g.node").data(nodes.descendants(),function(d){
return d.data.Associate;
});
link.transition().duration(1000).attr("d", diagonal);
d3.selectAll(".node-cicle").classed("highlight", false);
showAllCurrentPathsAndNodes();
link.enter().append("path")
.attr("class", "link")
.attr("d", diagonal)
.attr("", function (d) {
d3.select(this).moveToBack();
})
.style("opacity", 0)
.transition()
.duration(300)
.delay(function (d, i) {
return 28 * i;
}).style("opacity", 1);
node.transition().duration(800).attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
var g = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
.style("opacity", 0)
.style("cursor", function (d) {
d=d.data;
return ((d._children || d.children) && d.Manager !== "Board of Directors") ? "pointer" : "not-allowed";
})
.on("mouseover", function () {
d3.select(this).moveToFront();
})
//Cant trust the enter append here, reassign the event listener for all nodes each draw
d3.selectAll(".node")
.on("click", function (d) {
d=d.data;
return ((d._children || d.children) && d.Manager !== "Board of Directors") ? clickedNode(d, root) : "";
});
g.transition().duration(300)
.delay(function (d, i) {
return 28 * i;
})
.style("opacity", 1);
g.append("circle")
.attr("r", avatarRadius)
.attr("class", "circle-marker")
.style("stroke", function (d) {
d = d.data;
return ((d._children || d.children) && d.Manager !== "Board of Directors") ? "steelblue" : "gray";
})
.style("fill", function (d) {
d = d.data;
return ((d._children || d.children) && d.Manager !== "Board of Directors") ? "steelblue" : "#fff";
});
g.append("svg:image")
.attr("class", "node-avatar")
.attr("xlink:href", "http://safariuganda.com/wp-content/uploads/2014/12/480px-Facebook-default-no-profile-pic.jpg")
.attr("height", avatarRadius * 2)
.attr("width", avatarRadius * 2)
.attr("x", "-" + avatarRadius)
.attr("y", "-" + avatarRadius)
.attr('clip-path', 'url(#clip-circle)');
//Might want to tween this?
d3.selectAll(".node-avatar")
.attr("transform", function (d) {
return "rotate(" + (-1 * (d.x - 90)) + ")";
});
g.append("text")
.attr("dy", ".31em")
.attr("class", "label-text")
.text(function (d) {
return d.data.Associate;
})
//search all labels to ensure they are right side up (cant rely on the enter append here)
d3.selectAll(".label-text")
.attr("text-anchor", function (d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function (d) {
return d.x < 180 ? "translate(" + translateOffset + ")" : "rotate(180)translate(-" + translateOffset + ")";
})
link.exit().transition().duration(0).style("opacity", 0).remove();
node.exit().transition().duration(0).style("opactiy", 0).remove();
}
treeify(data, function (treeReturn) {
var root = treeReturn;
defaultPlot(root, root.children[0].Associate)
});
html,
body {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
background-color: #fff;
height: 100%;
width: 100%;
background-color: #f1f1f1;
position: relative;
display: block;
}
#tree-container {
position: relative;
display: block;
margin-left: 100px;
height: 100%;
width: 100%;
}
.node circle {
stroke-width: 1.5px;
}
.node {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.label-text {
-webkit-user-select: none;
/* Chrome/Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE10+ */
/* Rules below not implemented in browsers yet */
-o-user-select: none;
user-select: none;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
<div id="tree-container"></div>
<link rel="stylesheet" href="style.css">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="index.js"></script>
</body>
</html>
How to migrate
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) {
return [d.y, d.x / 180 * Math.PI];
});
to
function project(x, y) {
var angle = (x - 90) / 180 * Math.PI, radius = y;
return [radius * Math.cos(angle), radius * Math.sin(angle)];
}
var diagonal = function (d) {
return "M" + project(d.x, d.y) + "C" + project(d.x, (d.y + d.parent.y) / 2) +
" " + project(d.parent.x, (d.y + d.parent.y) / 2) +
" " + project(d.parent.x, d.parent.y);
}
And there're many changes in the drawIt method. Just refer to https://bl.ocks.org/mbostock/4739610f6d96aaad2fb1e78a72b385ab

Related

D3 tree with boxes

With my limited D3 knowledge, I've combined some tree layout examples to make a collapsible tree with branch labels. Here is a functional extract:
var root = {
children:[
{
title:"Node title",
children:[
{
type:"end",
items:[],
optionTitle:"Branch 1"
},
{
type:"end",
items:[],
optionTitle:"Branch 2"
}
]
}
]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type!='end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x<root.x) {
d.x -= (root.x-d.x)*3;
} else if (d.x>root.x) {
d.x += (d.x-root.x)*3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 6)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type!='end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function (d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d,i){
return 'pic_plus';
})
.attr('height',60)
.attr('width',60)
.attr('x',0)
.attr('y',0)
.append('image')
.attr('xlink:href',function(d,i){
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height',12)
.attr('width',12)
.attr('x',0)
.attr('y',0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children&&!d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type=='end';
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width:100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
<script src="code.js"></script>
</body>
</html>
In my code, the nodes are circles with the node labels next to them:
When I collapse a node, a plussign shows up.
Now I'm trying to put the node labels in boxes as it's shown in this example code.
I know I have to change the circles by foreignObjectsas in the example code but when I do it, paths aren't adjusted to the boxes.
How could I change the circles by foreignObjects and maintain the same functionality expand/collapse/plus?
The changes that you need to make to your current layout are:
move the blue boxes to be vertically aligned with the circles and with the right edge adjacent to the left side of each circle;
alter the paths to terminate at the left side of each blue box
The first can be done using a transform to alter the position of the blue boxes, and the second by altering the coordinates for the target point of each line.
Looking at the sample you linked to, there are rect elements behind the foreignObject elements that provide the colour, and the foreignObject is offset from the position of the rect elements. I have taken the liberty of adding the rect elements to your code and grouping the rect and foreignObject elements together so they can be moved in a single transform:
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp');
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
If you look at the tree this produces, the rect/foreignObject group needs to be translated the length of the rect element + the circle radius along the x axis, and by half the height of the rect element along the y axis. So, first let's add a variable to represent the circle radius and replace the hard-coded number with that variable:
var circleRadius = 6;
// a bit further on
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
Now write the transform:
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate('
+ -(rectNode.width + circleRadius) + ',' // note the transform is negative
+ -(rectNode.height/2) + ')' );
Check the resulting tree:
var rectNode = {
width: 120,
height: 45,
textMargin: 5
};
var root = {
slideId: 100,
children: [{
title: "Node title",
children: [{
type: "end",
items: [],
optionTitle: "Branch 1"
},
{
type: "end",
items: [],
optionTitle: "Branch 2"
}
]
}]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type != 'end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
var circleRadius = 6;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x < root.x) {
d.x -= (root.x - d.x) * 3;
} else if (d.x > root.x) {
d.x += (d.x - root.x) * 3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate('
+ -(rectNode.width + circleRadius) + ',' // note the transform is negative
+ -(rectNode.height/2) + ')' );
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type != 'end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function(d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d, i) {
return 'pic_plus';
})
.attr('height', 60)
.attr('width', 60)
.attr('x', 0)
.attr('y', 0)
.append('image')
.attr('xlink:href', function(d, i) {
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height', 12)
.attr('width', 12)
.attr('x', 0)
.attr('y', 0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children && !d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type == 'end';
}
function isCollapsed(node) {
return d._children || (!d._children && !d.children);
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width: 100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
.node-rect {
fill: #00f;
}
.node-text {
color: #fff;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
</body>
</html>
The next task is to alter the links to terminate at the edge of the rect boxes. If you check the code that covers the link positions and transitions, the enter and exit selections both use the position of the source node. The code we are interested in is this:
link.transition()
.duration(duration)
.attr("d", diagonal);
where the diagonal function is
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
d3.svg.diagonal() takes an object of the form
{ source: { x: 10, y: 10 }, target: { x: 20, y: 50 } }
and if you look at each item in the tree.links array, you'll see it is in the form
{ source: { /* source node coordinates */ }, target: { /* target node coords */ }
so to alter the position of the link target, we need to create a new object with the target coordinates altered. Once again, the x axis alteration should be -(rectNode.width + circleRadius); the y axis is OK. Note that the diagonal function switches over the x and y values, though, so we need to alter the target's y value, not the x value. Thus, we have:
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d) {
return diagonal({
source: d.source, // this is the same
target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) }
});
});
Add that to our code:
var rectNode = {
width: 120,
height: 45,
textMargin: 5
};
var root = {
slideId: 100,
children: [{
title: "Node title",
children: [{
type: "end",
items: [],
optionTitle: "Branch 1"
},
{
type: "end",
items: [],
optionTitle: "Branch 2"
}
]
}]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type != 'end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
var circleRadius = 6;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x < root.x) {
d.x -= (root.x - d.x) * 3;
} else if (d.x > root.x) {
d.x += (d.x - root.x) * 3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate(' +
-(rectNode.width + circleRadius) + ',' // note the transform is negative
+
-(rectNode.height / 2) + ')');
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type != 'end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d) {
return diagonal({
source: d.source,
target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) }
});
});
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function(d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d, i) {
return 'pic_plus';
})
.attr('height', 60)
.attr('width', 60)
.attr('x', 0)
.attr('y', 0)
.append('image')
.attr('xlink:href', function(d, i) {
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height', 12)
.attr('width', 12)
.attr('x', 0)
.attr('y', 0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children && !d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type == 'end';
}
function isCollapsed(node) {
return d._children || (!d._children && !d.children);
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width: 100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
.node-rect {
fill: #00f;
}
.node-text {
color: #fff;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
</body>
</html>
You can check that the links are terminating in the correct place by removing the fill on the rect elements.
There are now a number of other fixes to be made, but that should have proved your proof of concept; if you are unclear about any parts of the new code, please just ask.

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>

Continent zoom topojson

I'm trying to make the map able to zoom in on continenents on mouseclick. The code is based on Mike Bostock's tutorial on making maps and uses a CSV dataset to bind some values to the countries. https://bost.ocks.org/mike/map/.
I've tried to use various examples like this one: https://bl.ocks.org/mbostock/2206590 but nothing seems to work. The map just dissapears when I try to add a .onclick attribute. Does anyone have an idea how I can make the zoom work?
HTML
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 960px;
height: 500px;
position: relative;
}
#canvas {
}
#canvas-svg {
}
.land {
fill: #222;
}
.boundary {
fill: none;
stroke: #fff;
stroke-width: 1px;
}
#tooltip-container {
position: absolute;
background-color: #fff;
color: #000;
padding: 10px;
border: 1px solid;
display: none;
}
.tooltip_key {
font-weight: bold;
}
.tooltip_value {
margin-left: 20px;
float: right;
}
</style>
<div id="tooltip-container"></div>
<div id="canvas-svg"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script>
d3.csv("import.csv", function(err, data) {
var config = {"data0":"Country","data1":"Total","label0":"label 0","label1":"label 1","color0":"#99ccff","color1":"#0050A1","width":800,"height":400}
var width = 960,
height = 960;
var COLOR_COUNTS = 9;
function Interpolate(start, end, steps, count) {
var s = start,
e = end,
final = s + (((e - s) / steps) * count);
return Math.floor(final);
}
function Color(_r, _g, _b) {
var r, g, b;
var setColors = function(_r, _g, _b) {
r = _r;
g = _g;
b = _b;
};
setColors(_r, _g, _b);
this.getColors = function() {
var colors = {
r: r,
g: g,
b: b
};
return colors;
};
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function valueFormat(d) {
if (d > 1000000000) {
return "$" + Math.round(d / 1000000000 * 10) / 10 + "M";
} else if (d > 1000000) {
return "$" + Math.round(d / 1000000 * 10) / 10 + "M";
} else if (d > 1000) {
return "$" + Math.round(d / 1000 * 10) / 10 + "B";
} else {
return "$" + d + "M";
}
}
var COLOR_FIRST = config.color0, COLOR_LAST = config.color1;
var rgb = hexToRgb(COLOR_FIRST);
var COLOR_START = new Color(rgb.r, rgb.g, rgb.b);
rgb = hexToRgb(COLOR_LAST);
var COLOR_END = new Color(rgb.r, rgb.g, rgb.b);
var startColors = COLOR_START.getColors(),
endColors = COLOR_END.getColors();
var colors = [];
for (var i = 0; i < COLOR_COUNTS; i++) {
var r = Interpolate(startColors.r, endColors.r, COLOR_COUNTS, i);
var g = Interpolate(startColors.g, endColors.g, COLOR_COUNTS, i);
var b = Interpolate(startColors.b, endColors.b, COLOR_COUNTS, i);
colors.push(new Color(r, g, b));
}
var MAP_KEY = config.data0;
var MAP_VALUE = config.data1;
var projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("#canvas-svg").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
var valueHash = {};
function log10(val) {
return Math.log(val);
}
data.forEach(function(d) {
valueHash[d[MAP_KEY]] = +d[MAP_VALUE];
});
var quantize = d3.scale.quantize()
.domain([0, 1.0])
.range(d3.range(COLOR_COUNTS).map(function(i) { return i }));
quantize.domain([d3.min(data, function(d){
return (+d[MAP_VALUE]) }),
d3.max(data, function(d){
return (+d[MAP_VALUE]) })]);
d3.json("./world-topo-min.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
svg.append("path")
.datum(graticule)
.attr("class", "choropleth")
.attr("d", path);
var g = svg.append("g");
g.append("path")
.datum({type: "LineString", coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]]})
.attr("class", "equator")
.attr("d", path);
var country = g.selectAll(".country").data(countries);
country.enter().insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d,i) { return d.id; })
.attr("title", function(d) { return d.properties.name; })
.style("fill", function(d) {
if (valueHash[d.properties.name]) {
var c = quantize((valueHash[d.properties.name]));
var color = colors[c].getColors();
return "rgb(" + color.r + "," + color.g +
"," + color.b + ")";
} else {
return "#ccc";
}
})
.on("mousemove", function(d) {
var html = "";
html += "<div class=\"tooltip_kv\">";
html += "<span class=\"tooltip_key\">";
html += d.properties.name;
html += "</span>";
html += "<span class=\"tooltip_value\">";
html += (valueHash[d.properties.name] ? valueFormat(valueHash[d.properties.name]) : "");
html += "";
html += "</span>";
html += "</div>";
$("#tooltip-container").html(html);
$(this).attr("fill-opacity", "0.8");
$("#tooltip-container").show();
var coordinates = d3.mouse(this);
var map_width = $('.choropleth')[0].getBoundingClientRect().width;
if (d3.event.pageX < map_width / 2) {
d3.select("#tooltip-container")
.style("top", (d3.event.layerY + 15) + "px")
.style("left", (d3.event.layerX + 15) + "px");
} else {
var tooltip_width = $("#tooltip-container").width();
d3.select("#tooltip-container")
.style("top", (d3.event.layerY + 15) + "px")
.style("left", (d3.event.layerX - tooltip_width - 30) + "px");
}
})
.on("mouseout", function() {
$(this).attr("fill-opacity", "1.0");
$("#tooltip-container").hide();
});
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1.5px")
.attr("transform", "");
}
d3.select(self.frameElement).style("height", height + "px");
});
</script>
CSV
,Country,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,Total
,Afghanistan,,,34,,,35,3,41,150,344,377,660,521,235,288,74,2763
,African Union**,,,,,,53,9,,10,,,,,2,,2,76
,Albania,,,,2,,7,,5,,,,,15,7,7,7,50
,Algeria,412,546,249,195,242,159,340,509,1529,1075,829,1115,889,373,487,636,9585
,Angola,147,156,148,48,8,39,7,31,29,20,,,,31,2,61,726
,Argentina,213,6,16,12,168,3,9,24,23,16,14,46,36,44,14,23,668
,Armenia,,,,,118,,,1,,46,52,5,,16,,,238
,Australia,338,1236,663,801,472,459,727,675,445,775,1507,1567,876,255,932,1574,13301
,Austria,25,15,69,42,55,23,4,269,190,286,7,8,8,6,2,,1008
,Azerbaijan,3,,89,,4,53,159,211,29,142,146,557,333,398,602,285,3012
,Bahamas,33,,,,,3,,,,1,,,,,29,22,88
,Bahrain,299,30,54,1,10,66,63,26,20,,103,1,26,78,10,,786
,Bangladesh,203,266,41,8,32,10,221,88,13,,35,193,252,727,257,653,2997
,Barbados,,,,,,,,10,10,10,,,,,,,29
,Belarus,,,,,,6,140,,,3,,75,75,75,60,164,598
,Belgium,35,29,56,21,15,3,27,174,203,90,32,21,32,46,103,49,934
,Belize,1,,,,,,,,,,,,,,,,1
,Benin,,,7,,,,,3,,1,0,0,20,2,,,33
,Bhutan,,,,,0,,,,,,,,,,,1,1
,Bolivia,19,,,5,1,1,9,2,3,5,1,25,12,1,46,7,136
,Botswana,53,15,1,9,8,,,,,13,10,,,8,,,119
,Brazil,122,626,213,100,81,224,179,189,179,189,318,312,311,223,284,289,3840
,Brunei,,4,1,0,0,1,2,,,33,33,229,22,15,122,36,498
,Bulgaria,,,,2,12,132,22,48,116,119,8,29,5,,,3,495
,Burkina Faso,,,,,,12,1,4,,2,0,15,4,,5,6,48
,Burundi,,1,3,,,,,,,1,2,,5,,,1,14
,Cambodia,,,,,,,14,56,,4,34,,77,67,,,252
,Cameroon,,7,6,,,5,2,,1,,10,,,39,107,2,177
,Canada,570,538,428,152,352,124,104,456,408,106,236,351,211,194,306,395,4931
,Cape Verde,1,,,,,,,,,2,,,10,,,,12
,Central African Republic,,,,,,,9,,0,,,7,,,,,16
,Chad,,16,,,,,17,20,75,35,38,1,4,59,57,,322
,Chile,202,60,74,187,70,449,1095,662,397,335,475,316,62,51,125,114,4673
,China,2552,2833,2891,2373,3316,3558,2900,1692,1892,1407,1045,1128,1703,1452,1184,1214,33140
,Colombia,60,278,171,147,18,16,56,234,110,248,255,145,207,162,190,215,2510
,Comoros,,,,,,,,,6,,,,1,,,,7
,Democratic Republic of Congo,0,,,,,4,1,0,,1,27,,23,12,13,,81
,Costa Rica,,,,,,,,,,,,,1,,1,,2
,Cote d'Ivoire,0,,32,61,9,,,,,,,,,,6,6,113
,Croatia,,57,2,24,8,,,15,99,4,10,17,17,,63,11,326
,Cyprus,4,115,1,6,16,20,26,17,,,44,41,6,,,,295
,Czech Republic,14,65,47,97,7,593,45,10,23,6,13,71,22,15,,0,1029
,Denmark,50,153,51,54,228,120,83,146,95,129,17,,109,42,177,0,1455
,Djibouti,1,,3,,4,7,,,,,,,6,5,12,7,44
,Dominican Republic,13,5,0,8,35,2,,,,10,51,12,,,,1,137
,DR Congo,88,15,12,,,19,17,,18,41,151,,10,1,,,374
,Ecuador,,10,1,,15,48,17,2,102,77,90,68,88,11,53,1,583
,Egypt,837,847,719,630,654,778,678,651,333,159,686,630,281,675,368,1475,10401
,El Salvador,,,19,10,,,,,4,,,,2,,20,,55
,Equatorial Guinea,,7,,,7,12,,28,30,68,3,82,65,2,92,,394
,Eritrea,20,57,18,17,80,98,,4,,,,,,,,,293
,Estonia,27,,1,13,8,23,7,19,29,43,13,1,2,18,2,22,228
,Ethiopia,140,,20,174,239,,,,,,54,76,193,153,30,,1079
,Finland,474,9,12,227,77,107,136,108,135,42,50,58,76,280,156,228,2172
,France,107,66,43,57,93,2,67,74,5,76,103,33,91,107,17,34,976
,Gabon,,,,,6,,20,18,,,22,5,1,2,,3,77
,Gambia,,,,,5,,,,,16,,,,,,,21
,Georgia,6,85,,1,37,92,97,155,91,36,,2,,5,1,,608
,Germany,113,133,71,64,239,204,416,82,292,339,282,84,157,113,114,102,2804
,Ghana,1,12,1,6,35,0,0,16,,11,2,76,39,63,14,13,290
,Greece,708,787,400,2274,1368,405,731,1712,516,1229,652,80,37,52,199,762,11910
,Guatemala,,,,,,,,,0,,,,2,,,33,35
,Guinea,18,6,,2,,1,,0,,,4,1,4,,5,,40
,Guyana,,7,3,,,,,,0,,,,,,,,10
,Palestine,,,,,,,,,,,1,2,,,0,,3
,Lebanon,0,0,3,0,1,0,9,,,20,20,,,,,,54
,Honduras,,,,,,,,,0,,,0,0,20,13,6,39
,Hungary,14,14,,,72,13,253,196,5,4,18,9,,2,7,,604
,Iceland,,,,,,,,,,,,50,,,,,50
,India,995,1321,1911,2878,2180,1161,1480,2299,1867,1945,3017,3706,4545,5291,3487,3078,41160
,Indonesia,151,27,68,356,79,36,61,577,239,436,225,250,218,802,1136,683,5341
,Iran,418,524,435,282,121,57,423,331,62,62,103,103,47,31,13,13,3025
,Iraq,,,,,71,186,305,263,377,398,453,603,474,353,650,1215,5348
,Ireland,0,45,22,,19,5,13,19,22,0,5,2,1,,40,41,232
,Israel,368,131,349,189,852,1133,1122,862,676,153,67,85,120,153,510,617,7387
,Italy,241,221,243,559,446,162,433,525,220,109,115,298,219,91,145,596,4623
,Jamaica,,,,,,10,10,13,2,,1,,,1,,2,38
,Japan,491,441,477,480,400,464,511,498,680,525,426,292,270,306,480,310,7050
,Jordan,133,160,121,281,157,49,48,176,161,234,87,214,175,81,252,198,2526
,Kazakhstan,121,139,20,,47,42,44,82,25,39,58,52,84,141,93,419,1406
,Kenya,15,,,,,,,10,,,112,14,60,1,17,30,258
,Kosovo,,,,,,,,,,,,,,,1,,1
,Kuwait,245,67,23,45,2,12,5,279,5,9,85,113,31,73,692,366,2052
,Kyrgyzstan,,,,9,5,3,2,,,,,,,,,,19
,Laos,7,36,,0,,4,,,7,26,,14,19,19,,7,138
,Latvia,3,13,3,25,12,4,9,49,44,11,14,27,9,5,5,1,234
,Lebanon,4,,,,,1,,5,1,52,64,2,31,24,5,50,239
,Lesotho,,6,,,1,,1,,,,,,,,,,8
,Liberia,8,,,,,,,,,,,,,,,,8
,Libya,1,14,15,16,26,24,8,73,120,10,44,349
,Lithuania,6,16,12,1,47,9,52,4,27,27,8,1,1,73,3,21,308
,Uganda,,,0,,,,,,,,,,,,,,0
,Sri Lanka,1,,1,,,0,,,,,,,,,,,2
,Luxembourg,,,,1,,,,,,7,7,,1,,,,16
,Macedonia,14,112,,,,,0,,,,,,,1,,,128
,Madagascar,,,,,,,,,,0,,,,0,,,0
,Malawi,,,,,,,,,3,,,,1,2,2,,7
,Malaysia,26,26,154,147,65,57,398,526,508,1512,421,5,47,71,73,190,4227
,Maldives,,,,,,,15,,,,5,,,5,,,24
,Mali,7,,1,,,13,,8,3,10,,,9,6,,,57
,Malta,0,0,,,,18,,,,,,8,8,,12,,45
,Mauritania,31,,26,7,,,,,,,9,5,11,10,5,,103
,Mauritius,,,,,6,,,,,,,,,,45,,51
,Mexico,227,152,97,31,320,33,75,,22,59,112,229,261,69,97,500,2284
,Mongolia,,,,,,,,,14,,27,3,51,,,13,107
,Morocco,125,11,156,12,14,101,47,27,46,39,306,1398,826,82,572,42,3803
,Mozambique,0,,,1,,,,,,,,0,0,9,12,,22
,Myanmar,16,149,157,83,194,160,175,138,93,52,63,664,478,237,300,320,3277
,Namibia,,21,11,,16,,72,6,66,15,,1,57,9,2,,276
,NATO**,,18,,,,,116,,,420,,,22,,,,576
,Nepal,,11,9,9,34,6,,,1,,,3,,,5,14,91
,Netherlands,125,178,244,125,127,102,320,240,157,288,193,149,376,332,10,86,3052
,New Zealand,,45,17,107,49,10,5,81,2,48,58,23,25,26,79,56,631
,Nicaragua,,,,,,,,14,,14,,,,,,,28
,Niger,,,,14,,,,,8,1,0,,,13,3,9,48
,Nigeria,36,7,6,62,6,,15,52,19,65,186,65,4,35,224,188,971
,Macedonia,,0,,,,,,,,,,,,,,,0
,Afghanistan,19,207,,,,,,,,,,,,,,,226
,North Korea,18,28,9,8,8,5,15,5,5,4,1,,,,,,103
,Norway,229,99,90,14,6,14,542,567,624,588,159,614,149,62,23,143,3921
,Libya,,,,,,,,,,,,0,,,,,0
,Oman,118,33,40,36,41,173,301,16,75,93,30,21,120,490,736,148,2469
,Pakistan,175,408,541,628,455,421,338,670,1047,1214,2176,1063,1028,1144,752,735,12797
,Palestine,,,,,,,,2,,14,6,,,,,,21
,Panama,,,1,,,,,,,7,,,15,30,,,52
,Papua New Guinea,,,,,,,,,,,,,,,3,,3
,Paraguay,,6,,,4,1,,,,,3,8,7,0,,,29
,Peru,24,5,16,22,47,368,193,172,,43,46,63,10,49,153,169,1380
,Philippines,,10,5,10,34,12,23,16,10,1,3,63,16,75,12,158,449
,PIJ (Israel/Palestine)*,,,,,,,,,,,,,0,,,,0
,Poland,148,36,255,349,225,112,463,990,594,169,158,92,207,155,252,131,4336
,Portugal,2,19,,29,70,157,220,60,144,369,892,168,40,13,4,11,2196
,PRC (Israel/Palestine)*,,,,,,,,,,,,,,,0,,0
,Qatar,14,11,11,11,,,,27,,286,30,198,319,73,55,655,1690
,Romania,21,19,16,17,285,437,61,98,78,56,105,80,21,20,0,22,1335
,Russian Federation,,,,,,,4,100,,8,22,11,98,153,206,88,690
,Rwanda,14,,,,,,2,15,8,11,,,5,11,14,7,86
,Saudi Arabia,85,61,567,167,1170,167,205,214,363,796,1070,1237,1080,1672,2782,3161,14796
,Senegal,,,,,,15,9,18,6,4,4,20,,6,7,11,99
,Serbia,1,27,,,,,,,,,16,,0,,,,44
,Seychelles,,,,,,15,,,,,,8,,7,16,,45
,Sierra Leone,,,,,,,9,,,,,2,0,1,,,12
,Singapore,798,254,234,83,376,538,69,355,1113,1481,1020,935,828,780,683,98,9645
,Slovakia,0,,27,,,4,,0,,9,9,,2,6,,,57
,Slovenia,2,41,2,17,17,3,3,2,,6,40,28,,,,,162
,Somalia,,,,,,,,,,,,,,3,,0,3
,South Africa,6,18,,,,262,708,881,486,128,180,212,132,2,50,,3065
,South Korea,1396,773,528,752,1059,804,1650,1755,1683,796,1250,1553,1066,182,715,245,16207
,South Sudan,,,,,,,,37,44,1,,61,3,5,18,22,190
,Spain,334,168,289,253,315,363,315,337,346,235,290,181,238,176,127,153,4119
,Sri Lanka,297,161,45,35,49,58,97,89,71,,5,21,,,5,,934
,Sudan,,106,49,204,293,132,65,33,128,89,182,173,119,196,61,27,1858
,Suriname,2,,,,,,,,,,,,,7,,3,11
,Swaziland,1,1,,,,,,,,,,,,,,,2
,Sweden,204,165,75,64,47,78,122,41,44,54,51,191,206,52,43,43,1481
,Switzerland,23,68,63,117,203,168,83,108,18,39,47,31,9,4,0,2,983
,Syrian Arab Republic,64,28,45,69,25,35,100,20,276,193,298,368,371,361,15,,2267
,Syria rebels*,,,,,,,,,,,,,1,1,0,,2
,Taiwan (ROC),585,345,298,117,319,691,503,12,11,60,97,198,425,553,1084,681,5978
,Tajikistan,,,,,,,13,7,,32,,,,3,,,55
,Tanzania,3,,,51,,10,12,1,,25,,78,115,92,26,20,433
,Thailand,85,117,152,131,114,70,47,8,13,64,49,270,289,392,93,185,2078
,Timor-Leste,,,,,,,,,,,18,,,,,,18
,Togo,,,,,,,,,,,1,,4,,16,,21
,Tonga,,,,,,,,,,,,,,,,5,5
,Trinidad and Tobago,11,0,2,,,,,6,,,24,12,12,,,46,113
,Tunisia,,,83,1,,168,2,,3,,7,7,,38,43,6,357
,Turkey,1190,510,905,330,257,1113,531,692,710,780,484,770,1503,650,1556,448,12427
,Turkmenistan,,,,18,21,,,,7,36,14,236,115,69,102,122,739
,UAE,247,186,222,685,1210,2170,2021,919,752,561,605,1210,1088,2235,731,1289,16128
,Uganda,6,,34,,36,10,5,,5,23,37,471,219,20,,3,868
,UIC (Somalia)*,,,,,,,0,,,,,,,,,,0
,Ukraine,,,,,,,,,,,,,,,1,10,11
,Ukraine Rebels*,,,,,,,,,,,,,,,24,,24
,United Kingdom,871,1277,719,761,212,27,308,764,508,383,511,368,586,492,214,382,8381
,United Nations**,31,,,,2,1,2,1,0,,,,,5,23,4,69
,United States,330,487,499,592,560,520,641,819,951,968,1111,995,1180,802,566,565,11587
,United Wa State (Myanmar)*,,1,,,,,,,,,,,,,,,1
,Unknown country,,2,,0,,,,,12,,8,6,,30,31,51,139
,Unknown rebel group*,,0,0,,,,,,,,,,,,,,0
,Uruguay,4,,11,,,20,8,3,78,29,20,,,6,9,8,196
,Uzbekistan,,8,9,,,,,,,,,,,,,62,79
,Venezuela,108,103,50,15,9,21,380,774,737,358,208,594,680,1165,173,162,5535
,Viet Nam,7,85,66,28,304,297,41,8,204,78,184,1039,766,362,1078,870,5414
,Yemen,158,85,593,62,254,317,38,66,40,5,258,45,38,23,6,12,2000
,Zambia,33,,2,,,0,30,5,2,,,1,66,,,24,161
,Zimbabwe,3,10,,16,,25,25,,,,,,,,,,78

Make D3 force directed graph responsive and adhere to bounding box

I have a force directed graph generated via D3 that isn't playing well with the responsive code or the bounding box code I've found. Since the radius of all of my circles varies, I think it's throwing some things off... Any help is appreciated!
I have to use a custom length on the lines because the nodes run into each other if I don't manually space them out because the radii aren't the same.
(Please don't link me to the d3 page with the code, I've tried it, but maybe I'm placing it in the wrong spot if you think it would work on this. I also tried to post an image, but I don't have enough reputation.)
var width = 876,
height = 600;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1010)
.linkDistance(function(d) { return d.distance; })
.size([width, height])
.gravity(0.7);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function (d) {
return d.name + " (" + d.instances + ")";
})
svg.call(tip);
d3.json("datawords.json", function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("width", function(d) { return d.totalLength; })
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d.instances;})
.style("fill", function(d) { return color(d.instances); })
.call(force.drag)
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.on('click', connectedNodes)
force.on("tick", function() {
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.each(collide(0.5))
});
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
};
};
var padding = 10, // separation between circles
radius=15;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
return function(d) {
var rb = 4*radius + padding,
nx1 = d.x - rb,
nx2 = d.x + rb,
ny1 = d.y - rb,
ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y);
if (l < rb) {
l = (l - rb) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
};
window.addEventListener('resize', resize);
function resize() {
width = window.innerWidth, height = window.innerHeight;
svg.attr("width", width).attr("height", height);
force.size([width, height]).resume();
}
});
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
.node-active{
stroke: #555;
stroke-width: 1.5px;
}
.node:hover{
stroke: #555;
stroke-width: 1.5px;
}
marker {
display:none;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 200%;
left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3/d3tip.js"></script>
<div class="graph"></div>
</body>

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