I am making a d3 v4 vertical tree diagram based on http://bl.ocks.org/d3noob/8326869, and I'm trying to add text labels to the paths (eg. Yes, No, etc.). This is the code in the links section that I have so far:
function update (source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 95
});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function (d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.attr("data-info", function (d) { return d.data.info })
.on('click', click);
// Add Circle for the nodes
/*nodeEnter.append('circle')
.attr('class', 'node')
.attr("id", function(d){return "node" + d.id;})//id of the node.
.attr('r', 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});*/
nodeEnter.append('rect')
.attr('class', 'node')
.attr('width', 170)
.attr('height', 55)
.attr('x', -85)
.attr('y', -22)
.attr('rx',7) //rounding
.attr('ry',7)
.attr("id", function(d){return "node" + d.id;});//id of the node.
nodeEnter.append('text')
//.attr("dy", ".25em")
//.attr('x', -23)
.attr('class', 'node-text')
.attr("text-anchor", "middle")
.text(function (d) {
return d.data.name;
})
.call(wrap, 165);
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Update the node attributes and style
nodeUpdate.select('rect.node')
.style("fill", function (d) {
if (d._children) {
return "#007bff"; // dark blue
}
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', 'g')
.attr("id", function(d){ return ("link" + d.id)})//unique id
.attr("class", "link")
.attr('d', function (d) {
var o = {x: source.x0, y: source.y0}
return diagonal(o, o);
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
var linkLabel = link.enter().insert("text","g")
.attr("class", "link2")
.attr("id", function(d){ return ("link-label" + d.id)})//unique id
.attr("dx",function(d){ return (d.parent.x + d.x)/2 })
.attr("dy",function(d){ return (d.parent.y + d.y)/2 })
.text(function(d) {
if (d.data.label === "Yes") {
this.setAttribute("x",-30);
} else {
this.setAttribute("x",10);
}
return d.data.label;
});
linkUpdate.merge(linkLabel);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) {
svg.select("#link-label" + d.id).transition().duration(duration).attr("dx",function(d){ return (d.parent.x + d.x)/2 })
.attr("dy",function(d){ return (d.parent.y + d.y)/2});
return diagonal(d, d.parent)
});
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = {x: source.x, y: source.y};
svg.selectAll("#link-label" + d.id).remove();
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
The "linkEnter.insert("text","g")" part is where I am having trouble. It looks like it keeps appending to the element instead of being a sibling so that it doesn't display at all, and I'm not sure how to code it so that it isn't a child of , while still being animated appropriately when a node is hidden/shown.
I'm also not sure how to set up the coordinates once I do manage to fix the above issue so that the text label is in the middle of the path (or at least close to it).
I've tried adding similar code in the node section instead but then it seems harder to find the right positioning to align to the path.
========
Edit: I've updated the code as it seems like it's working now. I created linkLabel which pulls data from the JSON variable. It's positioned to be next to the paths but not overlapping them. The animations aren't smooth but at least it seems to work.
My requirement is to draw a category-grouped bar chart in which each category has a different number of groups, using pure d3. I have no idea how to take domain and range to meet my requirement.
I tried in the way given in the answer to d3 nested grouped bar chart, but it did not work in my case.
Here my graph structure is like:
The issue with the plunker of the answer that you mention is that it will just work for values that have the same children. In order to handle the dynamic children values I took this approach
Lets create the color mapping for our groups:
var color = {
Mechanical: '#4A7B9D',
Electrical: '#54577C',
Hydraulic: '#ED6A5A'
};
We also need a structure with nested values that will be the inner groups:
// Simulated data structure
var data = [{
key: 'Mechanical',
values: [{
key: 'Gear',
value: 11
}, {
key: 'Bearing',
value: 8
}, {
key: 'Motor',
value: 3
}]
}];
I created a barPadding value which will dictate the separation between bars:
var barPadding = 120;
We are going to need a dummy scale to get the rangeBand of the bars, lets do that:
// dummy array
var rangeBands = [];
// cummulative value to position our bars
var cummulative = 0;
data.forEach(function(val, i) {
val.cummulative = cummulative;
cummulative += val.values.length;
val.values.forEach(function(values) {
rangeBands.push(i);
})
});
// set scale to cover whole svg
var x_category = d3.scale.linear()
.range([0, width]);
// create dummy scale to get rangeBands (width/childrenValues)
var x_defect = d3.scale.ordinal().domain(rangeBands)
.rangeRoundBands([0, width], .1);
var x_category_domain = x_defect.rangeBand() * rangeBands.length;
x_category.domain([0, x_category_domain]);
Then lets add all our category groups g elements:
var category_g = svg.selectAll(".category")
.data(data)
.enter().append("g")
.attr("class", function(d) {
return 'category category-' + d.key;
})
.attr("transform", function(d) { // offset by inner group size
var x_group = x_category((d.cummulative * x_defect.rangeBand()));
return "translate(" + x_group + ",0)";
})
.attr("fill", function(d) { // make child elements of group "inherit" this fill
return color[d.key];
});
Adding our inner groups g elements:
var defect_g = category_g.selectAll(".defect")
.data(function(d) {
return d.values;
})
.enter().append("g")
.attr("class", function(d) {
return 'defect defect-' + d.key;
})
.attr("transform", function(d, i) { // offset by index
return "translate(" + x_category((i * x_defect.rangeBand())) + ",0)";
});
Having our g elements lets add the labels:
var category_label = category_g.selectAll(".category-label")
.data(function(d) {
return [d];
})
.enter().append("text")
.attr("class", function(d) {
console.log(d)
return 'category-label category-label-' + d.key;
})
.attr("transform", function(d) {
var x_label = x_category((d.values.length * x_defect.rangeBand() + barPadding) / 2);
var y_label = height + 30;
return "translate(" + x_label + "," + y_label + ")";
})
.text(function(d) {
return d.key;
})
.attr('text-anchor', 'middle');
var defect_label = defect_g.selectAll(".defect-label")
.data(function(d) {
return [d];
})
.enter().append("text")
.attr("class", function(d) {
console.log(d)
return 'defect-label defect-label-' + d.key;
})
.attr("transform", function(d) {
var x_label = x_category((x_defect.rangeBand() + barPadding) / 2);
var y_label = height + 10;
return "translate(" + x_label + "," + y_label + ")";
})
.text(function(d) {
return d.key;
})
.attr('text-anchor', 'middle');
And finally our rects:
var rects = defect_g.selectAll('.rect')
.data(function(d) {
return [d];
})
.enter().append("rect")
.attr("class", "rect")
.attr("width", x_category(x_defect.rangeBand() - barPadding))
.attr("x", function(d) {
return x_category(barPadding);
})
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
Here's the above code in plnkr: https://plnkr.co/edit/L0eQwtEMQ413CpoS5nvo?p=preview
I think I'm missing something very obvious here. Basically what I am trying to do is create a treemap that on button click will go to the server and retrieve the next level into the treemap...This is necessary because the treemap structure is too large and takes too long to calculate so jumping one level at a time is the only option we have.
[Note to IE users, in this example the treemap node names don't appear to be working. Try using Chrome]
http://plnkr.co/edit/simVGU
This code is taken almost exactly from
http://bost.ocks.org/mike/treemap/
I'm using vizData1.json for the "first" level and on mouse click I'm using vizData2.json as the "second" level. You can see that the two end up overlapping. I've tried to do svg.exit() as well as svg.clear() without any luck.
I should also note that I have already tried the sticky(false) suggestion from this post
Does the d3 treemap layout get cached when a root node is passed to it?
UPDATE:
To continue my hunt I have found an example that successfully adds new nodes to an existing treemap. However I am having trouble adapting this logic as the treemap I am attempting to fit this logic into has been heavily customized by Michael Bostock - #mbostock to allow for the nice breadcrumb trail bar at the top.
Code snippet that proves appending to existing treemap nodes is possible:
http://jsfiddle.net/WB5jh/3/
Also, Stackoverflow is forcing me to post code because I'm linking to plnkr so I have dumped my script.js here for those who would rather not interact with plunker
$(function() {
var margin = { top: 20, right: 0, bottom: 0, left: 0 },
width = 960,
height = 500,
formatNumber = d3.format(",d"),
transitioning;
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)
.sticky(false);
var svg = d3.select("#treemap")
.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");
d3.json("vizData1.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
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.children)
? d.value = d.children.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) {
console.log(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);
g.selectAll(".child")
.data(function (d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function (d) { return formatNumber(d.value); });
g.append("foreignObject")
.call(rect)
.attr("class", "foreignobj")
.append("xhtml:div")
.attr("dy", ".75em")
.html(function (d) { return d.name; })
.attr("class", "textdiv");
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
d3.json("vizData2.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
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);
g2.selectAll("foreignObject div").style("display", "none"); /*added*/
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
t1.selectAll(".textdiv").style("display", "none"); /* added */
t1.selectAll(".foreignobj").call(foreign);
t2.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// 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.attr("x", function (d) { return x(d.x) + 6; })
.attr("y", function (d) { return y(d.y) + 6; });
}
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 foreign(foreign) { /* added */
foreign.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.name
: d.name;
}
});
I am trying to replicate the health & wealth nations chart.
http://bost.ocks.org/mike/nations/:
When i click start the animation of the chart works which works perfectly and if i click stop the animation stops. However if i click the start next time, it is starting from the beginning instead from where i stopped? how do i animate from the place where i left?
Following is the code:
<h1>The Wealth & Health of Nations</h1>
<p id="chart"></p>
<input type="submit" value="Start" onclick=start();>
<input type="submit" value="Stop" onclick=stop();>
<script src="http://d3js.org/d3.v2.js?2.8.1"></script>
<script>
// Various accessors that specify the four dimensions of data to visualize.
function x(d) { return d.income; }
function y(d) { return d.lifeExpectancy; }
function radius(d) { return d.population; }
function color(d) { return d.region; }
function key(d) { return d.name; }
// Chart dimensions.
var margin = {top: 29.5, right: 29.5, bottom: 29.5, left: 59.5},
width = 960 - margin.right,
height = 500 - margin.top - margin.bottom;
// Various scales. These domains make assumptions of data, naturally.
var xScale = d3.scale.log().domain([300, 1e5]).range([0, width]),
yScale = d3.scale.linear().domain([10, 85]).range([height, 0]),
radiusScale = d3.scale.sqrt().domain([0, 5e8]).range([0, 40]),
colorScale = d3.scale.category10();
// The x & y axes.
var xAxis = d3.svg.axis().orient("bottom").scale(xScale).ticks(12, d3.format(",d")),
yAxis = d3.svg.axis().scale(yScale).orient("left");
// Create the SVG container and set the origin.
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Add the x-axis.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the y-axis.
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Add an x-axis label.
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width)
.attr("y", height - 6)
.text("income per capita, inflation-adjusted (dollars)");
// Add a y-axis label.
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 6)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("life expectancy (years)");
// Add the year label; the value is set on transition.
var label = svg.append("text")
.attr("class", "year label")
.attr("text-anchor", "end")
.attr("y", height - 24)
.attr("x", width)
.text(2000);
function start()
{
//alert("Start Clicked");
// Load the data.
d3.json("nations_new.json", function(nations) {
// A bisector since many nation's data is sparsely-defined.
var bisect = d3.bisector(function(d) { return d[0]; });
// Add a dot per nation. Initialize the data at 2000, and set the colors.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2000))
.enter().append("circle")
.attr("class", "dot")
.style("fill", function(d) { return colorScale(color(d)); })
.call(position)
.sort(order);
// Add a title.
dot.append("title")
.text(function(d) { return d.name; });
// Add an overlay for the year label.
var box = label.node().getBBox();
var overlay = svg.append("rect")
.attr("class", "overlay")
.attr("x", box.x)
.attr("y", box.y)
.attr("width", box.width)
.attr("height", box.height);
//.on("mouseover", enableInteraction);
// Start a transition that interpolates the data based on year.
svg.transition()
.duration(30000)
.ease("linear")
.tween("year", tweenYear)
.each("end", enableInteraction);
// Positions the dots based on data.
function position(dot) {
dot .attr("cx", function(d) { return xScale(x(d)); })
.attr("cy", function(d) { return yScale(y(d)); })
.attr("r", function(d) { return radiusScale(radius(d)); });
}
// Defines a sort order so that the smallest dots are drawn on top.
function order(a, b) {
return radius(b) - radius(a);
}
// After the transition finishes, you can mouseover to change the year.
function enableInteraction() {
var yearScale = d3.scale.linear()
.domain([2000, 2009])
.range([box.x + 10, box.x + box.width - 10])
.clamp(true);
// Cancel the current transition, if any.
svg.transition().duration(0);
overlay
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("mousemove", mousemove)
.on("touchmove", mousemove);
function mouseover() {
label.classed("active", true);
}
function mouseout() {
label.classed("active", false);
}
function mousemove() {
displayYear(yearScale.invert(d3.mouse(this)[0]));
}
}
// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
var year = d3.interpolateNumber(2000, 2009);
return function(t) { displayYear(year(t)); };
}
// Updates the display to show the specified year.
function displayYear(year) {
dot.data(interpolateData(year), key).call(position).sort(order);
label.text(Math.round(year));
}
// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
return nations.map(function(d) {
return {
name: d.name,
region: d.region,
income: interpolateValues(d.income, year),
population: interpolateValues(d.population, year),
lifeExpectancy: interpolateValues(d.lifeExpectancy, year)
};
});
}
// Finds (and possibly interpolates) the value for the specified year.
function interpolateValues(values, year) {
var i = bisect.left(values, year, 0, values.length - 1),
a = values[i];
if (i > 0) {
var b = values[i - 1],
t = (year - a[0]) / (b[0] - a[0]);
return a[1] * (1 - t) + b[1] * t;
}
return a[1];
}
});
}
function stop()
{
//alert("stop Clicked");
svg.transition().duration(0);
}
the json file is :
[
{
"name":"Angola",
"region":"Sub-Saharan Africa",
"income":[[2000,2446.65],[2001,2479.69],[2002,2773.29],[2003,2785.39],[2004,3007.11],[2005,3533],[2006,4069.56],[2007,4755.46],[2008,5228.74],[2009,5055.59]],
"population":[[2000,10442812],[2001,10623424],[2002,10866106],[2003,11186202],[2004,11521432],[2005,11827315],[2006,12127071],[2007,12420476],[2008,12707546]],
"lifeExpectancy":[[2000,43.56],[2001,43.86],[2002,44.22],[2003,44.61],[2004,45.05],[2005,45.52],[2006,46.02],[2007,46.54],[2008,47.06],[2009,47.58]]
},
{
"name":"china",
"region":"East Asia & Pacific",
"income":[[2000,12446.65],[2001,12479.69],[2002,12773.29],[2003,12785.39],[2004,12007.11],[2005,12533],[2006,12069.56],[2007,12755.46],[2008,12228.74],[2009,12055.59]],
"population":[[2000,31542812],[2001,31623424],[2002,31866106],[2003,32186202],[2004,31521432],[2005,31827315],[2006,32127071],[2007,32420476],[2008,32707546]],
"lifeExpectancy":[[2000,53.56],[2001,63.86],[2002,64.22],[2003,64.61],[2004,76.05],[2005,66.52],[2006,86.02],[2007,87.54],[2008,89.06],[2009,68.58]]
},
{
"name":"India",
"region":"South Asia",
"income":[[2000,22446.65],[2001,22479.69],[2002,22773.29],[2003,22785.39],[2004,22007.11],[2005,22533],[2006,22069.56],[2007,22755.46],[2008,22228.74],[2009,22055.59]],
"population":[[2000,41542812],[2001,41623424],[2002,41866106],[2003,42186202],[2004,41521432],[2005,41827315],[2006,42127071],[2007,42420476],[2008,42707546],[2009,42707546]],
"lifeExpectancy":[[2000,43.56],[2001,43.86],[2002,44.22],[2003,64.61],[2004,56.05],[2005,56.52],[2006,66.02],[2007,68.54],[2008,67.06],[2009,73.58]]
}
]
In the start function, you would need to keep track of what year you're currently showing, e.g. with a global variable:
var thisYear = 2000;
// lots of code...
function displayYear(year) {
thisYear = year;
dot.data(interpolateData(year), key).call(position).sort(order);
label.text(Math.round(year));
}
Then you would need to modify the year it starts with depending on the value of that variable:
function tweenYear() {
var year = d3.interpolateNumber(thisYear, 2009);
return function(t) { displayYear(year(t)); };
}
I was working on something very similar and the suggestions by Lars got me very far. However, I kept getting my dots duplicated each time I hit start. I have since discovered the following:
If you would like to make it so your dots are not duplicated each time you select the start button, you would need to add the following code as a part of your start() function, and ideally just before the dots are initially added:
svg.selectAll(".dot").remove()
This removes the previous .dot elements; new .dot elements are subsequently created assuming you've setup the thisYear global variable mentioned by Lars.