I have my data formatted like flare.json that's used in this example :
I am just wondering what function the d3 zoomable chart uses to get the data in this format
In flare.json it's like this
{
name: "stuff",
children: [
....
]
}
and it's converted to this in the example. Which line does this?
{
children: Array[17]
depth: 1
dx: 0.6028744305756647
dy: 0.25
name: "A name would appear here"
parent: Object
value: 39850000.06
x: 0
y: 0.25
}
Chart
var total_revenue = json.total_revenue;
json = json.chart_data;
var width = 840,
height = width,
radius = width / 2,
x = d3.scale.linear().range([0, 2 * Math.PI]),
y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]),
padding = 5,
duration = 1000;
var div = d3.select("#chart_render");
div.select("img").remove();
var vis = div.append("svg")
.attr("width", width + padding * 2)
.attr("height", height + padding * 2)
.append("g")
.attr("transform", "translate(" + [radius + padding, radius + padding] + ")");
var partition = d3.layout.partition()
.value(function(d) { return d.size });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
console.log(json);
var nodes = partition.nodes({children: json});
var path = vis.selectAll("path").data(nodes);
path.enter().append("path")
.attr("id", function(d, i) { return "path-" + i; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", colour)
.on("click", click);
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style("fill-opacity", function(d) {
var relative_percent = 0;
var relative_total = 0;
//console.log(d);
if (d.depth != 0) {
for(var i = 0; i < d.parent.children.length; i++) {
relative_total += d.parent.children[i].value;
}
//console.log(relative_total);
relative_percent = d.value/total_revenue*100;
if (relative_percent > 1) {
return '1';
} else {
return '0';
}
}
})
.style("fill", function(d) {
return "#fff";
})
.attr("text-anchor", function(d) {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1,
angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
})
.on("click", click);
textEnter.append("tspan")
.attr("x", 0)
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
textEnter.append("tspan")
.attr("x", 0)
.attr("dy", "1em")
.text(function(d) { return d.depth ? d.name.split(" ")[1] || "" : ""; });
function click(d) {
path.transition()
.duration(duration)
.attrTween("d", arcTween(d));
// Somewhat of a hack as we rely on arcTween updating the scales.
text.style("visibility", function(e) {
return isParentOf(d, e) && e.value > 1500000 ? null : d3.select(this).style("visibility");
})
.transition()
.duration(duration)
.attrTween("text-anchor", function(d) {
return function() {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
};
})
.attrTween("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1;
return function() {
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
};
})
.style("fill-opacity", function(e) { return isParentOf(d, e) ? 1 : 1e-6; })
.each("end", function(e) {
d3.select(this).style("visibility", function (d) {
// var relative_total = 0;
// var relative_percent = 0;
// for(var i = 0; i < d.parent.children.length; i++) {
// relative_total += d.parent.children[i].value;
// }
// console.log(relative_total);
// relative_percent = d.value/relative_total*100;
// console.log(relative_percent);
return isParentOf(d, e) && e.value > 1500000 ? null : "hidden";
})
});
}
function isParentOf(p, c) {
if (p === c) return true;
if (p.children) {
return p.children.some(function(d) {
return isParentOf(d, c);
});
}
return false;
}
function colour(d) {
if (d.depth == 0) {
return "rgb(250, 250, 250)";
} else if (d.depth == 1) {
return 'rgb(86, 135, 209)';
} else if (d.depth == 2) {
return 'rgb(222, 120, 59)';
} else if (d.depth == 3) {
return 'rgb(106, 185, 117)';
}
// if (d.children) {
// // There is a maximum of two children!
// var colours = d.children.map(colour),
// a = d3.hsl(colours[0]),
// b = d3.hsl(colours[1]);
// // L*a*b* might be better here...
// return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
// }
// return d.colour || "#fff";
}
// Interpolate the scales!
function arcTween(d) {
var my = maxY(d),
xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, my]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d) {
return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
};
}
function maxY(d) {
return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy;
}
// http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
function brightness(rgb) {
return rgb.r * .299 + rgb.g * .587 + rgb.b * .114;
}
This line:
var nodes = partition.nodes({children: json});
Explanation of code that sets up sunburst diagram
In D3 parlance, sunburst diagram is based on D3 "partition layout". Actually, D3 "partition layout" is in a way more general term, since it can be used for displaying not only sunburst diagram, but also others based on the idea of "partitioning" parents (hence the name "partition"). This is also a useful example for noticing difference between "layout" and "diagram" (in D3 mindset), but this is another story.
Following 2 lines are first steps in initializing partition layout:
var partition = d3.layout.partition()
.value(function(d) { return d.size });
This line does all calculations:
var nodes = partition.nodes({children: json});
Then variable nodes can be used for defining actual visual appearance of svg elements (arcs and labels):
var path = vis.selectAll("path").data(nodes);
and
var text = vis.selectAll("text").data(nodes);
These two lines represent something which is called "data binding" often. They enable programmers to use data to drive visual elements, like in the following line:
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
Here, d.name originates from data, and d.depth is added by partition layout. They are both actually part of nodes.
I tried to explain in simple terms, but probably there are some confusing points to you - don't worry, it will be crystal clear to you soon, if you read the right docs and tutorials. ;)
Related
I am making a Sunburst chart and am almost complete, but I want the text to flip half-way around the circle to make it easier to read. Using help from another question here on stackoverflow I have been able to get the text to flip, but it drops down a level. I have tried modifying the computeTextRotation function to account for this, but to no prevail. I am curious if anyone is able to help me solve this problem.
Here is a picture of the chart:
Here is the computeTextRotation functions code:
function computeTextRotation(d) {
var rotation = (d.x + d.dx / 2) * 180 / Math.PI - 90;
return {
global: rotation,
correction: rotation > 90 ? 180 : 0
};
}
...
.attr("transform", function(d)
{
var r = computeTextRotation(d);
return "rotate(" + r.global + ")"
+ "translate(" + radius / 3. * d.depth + ")"
+ "rotate(" + -r.correction + ")";
}
)
Here is the entire JavaScript code:
var margin = {top: 500, right: 500, bottom: 500, left: 500},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 150;
function filter_min_arc_size_text(d, i) {return (d.dx*d.depth*radius/1)>14};
var hue = d3.scale.category10();
var luminance = d3.scale.sqrt()
.domain([0, 1e6])
.clamp(true)
.range([90, 20]);
var svg = d3.select("body").append("svg")
.attr("width", margin.left + margin.right)
.attr("height", margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, radius]);
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx - .01 / (d.depth + .5); })
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });
//Tooltip description
var tooltip = d3.select("body")
.append("div")
.attr("id", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("opacity", 0);
function format_number(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function format_description(d) {
var description = d.description;
return '<b>' + d.name + '</b></br>'+ d.description + '<br> (' + format_number(d.value) + ')';
}
function computeTextRotation(d) {
var angle=(d.x +d.dx/2)*180/Math.PI - 90
return angle;
}
function mouseOverArc(d) {
d3.select(this).attr("stroke","black")
tooltip.html(format_description(d));
return tooltip.transition()
.duration(50)
.style("opacity", 0.9);
}
function mouseOutArc(){
d3.select(this).attr("stroke","")
return tooltip.style("opacity", 0);
}
function mouseMoveArc (d) {
return tooltip
.style("top", (d3.event.pageY-10)+"px")
.style("left", (d3.event.pageX+10)+"px");
}
var root_ = null;
d3.json("data/davis-aroma-wheel.json", function(error, root) {
if (error) return console.warn(error);
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.size; })
.nodes(root)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition
.children(function(d, depth) { return depth < 3 ? d._children : null; })
.value(function(d) { return d.sum; });
var center = svg.append("circle")
.attr("r", radius / 3)
.on("click", zoomOut);
center.append("title")
.text("zoom out");
var partitioned_data = partition.nodes(root).slice(1)
var path = svg.selectAll("path")
.data(partitioned_data)
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn)
.on("mouseover", mouseOverArc)
.on("mousemove", mouseMoveArc)
.on("mouseout", mouseOutArc);
var texts = svg.selectAll("text")
.data(partitioned_data)
.enter().append("text")
.filter(filter_min_arc_size_text)
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return radius / 3 * d.depth; })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d,i) {return d.name})
function zoomIn(p) {
if (p.depth > 1) p = p.parent;
if (!p.children) return;
zoom(p, p);
}
function zoomOut(p) {
if (!p.parent) return;
zoom(p.parent, p);
}
// Zoom to the specified new root.
function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
return p.key > d.key
? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
: {depth: 0, x: 0, dx: 2 * Math.PI};
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
var new_data=partition.nodes(root).slice(1)
path = path.data(new_data, function(d) { return d.key; });
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
path.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
path.enter().append("path")
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.on("click", zoomIn)
.on("mouseover", mouseOverArc)
.on("mousemove", mouseMoveArc)
.on("mouseout", mouseOutArc)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
texts = texts.data(new_data, function(d) { return d.key; })
texts.exit()
.remove()
texts.enter()
.append("text")
texts.style("opacity", 0)
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return radius / 3 * d.depth; })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.filter(filter_min_arc_size_text)
.text(function(d,i) {return d.name})
.transition().delay(750).style("opacity", 1)
}
});
function key(d) {
var k = [], p = d;
while (p.depth) k.push(p.name), p = p.parent;
return k.reverse().join(".");
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.name));
c.l = luminance(d.sum);
return c;
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");
Your corrective factor rotates the text 180 degrees, this is only half of what you need:
By rotating 180 degrees, you get text that is right way up, but now that moves in the opposite direction because the direction of the text is also rotated.
For the second half of the circle, you need to specify a text-anchor of "end" so that the text is anchored where it should be. Currently it is anchored where it starts, which is fine for the first half of the circle.
When styling the text you'll need to perform a check to see if the text anchor needs to be set to "end" as opposed to "start":
text.style("text-anchor",function(d) { return isRotated(d) ? "end" : "start" })
With the check looking something like:
function isRotated(d) {
var rotation = (d.x + d.dx / 2) * 180 / Math.PI - 90;
return rotation > 90 ? true : false
}
The margins also need to be adjusted:
.attr("dx", function(d) {return isRotated(d) ? "-6" : "6"}) //margin
I'm using D3 and javascript to create a BiLevel Partition following this example. The labels in the left side of the diagram are upside down, and I was trying to rotate them, but I've not been successful yet.
I found numerous cases of people with the same problem, but using Sunburst. Also tried to implement those solutions, but I'm still unable to solve this problem.
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
/*var margin = {top: 350, right: 480, bottom: 350, left: 480},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 10;*/
var width = 1200,
height = 1200,
radius = Math.min(width, height) / 2;
function filter_min_arc_size_text(d, i) {return (d.dx*d.depth*radius/3)>14};
var hue = d3.scale.category10();
var luminance = d3.scale.sqrt()
.domain([0, 1e6])
.clamp(true)
.range([90, 20]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
var partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, radius]);
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx - .01 / (d.depth + .5); })
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });
//Tooltip description
var tooltip = d3.select("body")
.append("div")
.attr("id", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("opacity", 0);
function format_number(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function format_description(d) {
var description = d.description;
return /* '<b>' + d.name + '</b></br>'+*/ d.description + '<br> (' + format_number(d.value) + ')';
}
function computeTextRotation(d) {
var ang = ((d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
return (ang > 90) ? 180 + ang : ang;
}
function mouseOverArc(d) {
d3.select(this).attr("stroke","black")
tooltip.html(format_description(d));
return tooltip.transition()
.duration(50)
.style("opacity", 0.9);
}
function mouseOutArc(){
d3.select(this).attr("stroke","")
return tooltip.style("opacity", 0);
}
function mouseMoveArc (d) {
return tooltip
.style("top", (d3.event.pageY-10)+"px")
.style("left", (d3.event.pageX+10)+"px");
}
var root_ = null;
d3.json("flare.json", function(error, root) {
if (error) return console.warn(error);
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.size; })
.nodes(root)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition
.children(function(d, depth) { return depth < 2 ? d._children : null; })
.value(function(d) { return d.sum; });
var center = svg.append("circle")
.attr("r", radius / 3)
.on("click", zoomOut);
center.append("title")
.text("zoom out");
var partitioned_data=partition.nodes(root).slice(1)
var path = svg.selectAll("path")
.data(partitioned_data)
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn)
.on("mouseover", mouseOverArc)
.on("mousemove", mouseMoveArc)
.on("mouseout", mouseOutArc);
var texts = svg.selectAll("text")
.data(partitioned_data)
.enter().append("text")
.filter(filter_min_arc_size_text)
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return radius / 3 * d.depth; })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d,i) {return d.name})
function zoomIn(p) {
if (p.depth > 1) p = p.parent;
if (!p.children) return;
zoom(p, p);
}
function zoomOut(p) {
if (!p.parent) return;
zoom(p.parent, p);
}
// Zoom to the specified new root.
function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
return p.key > d.key
? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
: {depth: 0, x: 0, dx: 2 * Math.PI};
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
var new_data=partition.nodes(root).slice(1)
path = path.data(new_data, function(d) { return d.key; });
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
path.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
path.enter().append("path")
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.on("click", zoomIn)
.on("mouseover", mouseOverArc)
.on("mousemove", mouseMoveArc)
.on("mouseout", mouseOutArc)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
texts = texts.data(new_data, function(d) { return d.key; })
texts.exit()
.remove()
texts.enter()
.append("text")
texts.style("opacity", 0)
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return radius / 3 * d.depth; })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.filter(filter_min_arc_size_text)
.text(function(d,i) {return d.name})
.transition().delay(750).style("opacity", 1)
}
});
function key(d) {
var k = [], p = d;
while (p.depth) k.push(p.name), p = p.parent;
return k.reverse().join(".");
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.name));
c.l = luminance(d.sum);
return c;
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");
</script>
The problem I have is that it is only showing the right half of the Partition.
I am trying to reduce the size of the parent level inner and outer radius' when I click on one of its children nodes. You can view my current diagram here: https://jsfiddle.net/2heLd2b1/. As you can see, when a child node is clicked and the distorts to display the selected node and its path, the parent layers take up too much space. I am looking for any suggestions as to how I could reduce or shrink the parent nodes width.
ar width = 960,
height = 750,
radius = (Math.min(width, height) / 2) - 10;
var color = d3.scale.category20c();
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
function percent(d) {
var percentage = (d.value / 956129) * 100;
return percentage.toFixed(2);
}
// var tip = d3.tip()
// .attr('class', 'd3-tip')
// .offset([-10, 0])
// .html(function(d) {
// return "<strong>" + d.name + "</strong> <span style='color:red'>" + percent(d) + "%</span>";
// })
var partition = d3.layout.partition()
// .value(function(d) { return d.size; });
.value(function(d) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) })
.cornerRadius(function(d) { return 5;});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.append("g")
.classed("inner", true);
// svg.call(tip);
d3.json("flare.json", function(error, root) {
if (error) throw error;
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
path = g.append("path")
.attr("d", arc)
.attr('stroke', 'white')
.attr("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", magnify)
// .on('mouseover', tip.show)
// .on('mouseout', tip.hide)
.each(stash);
var text = g.append("text")
.attr("x", function(d) { return d.x; })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
return d.name;
})
.attr('font-size', function(d) {
return '10px';
})
.attr("text-anchor", "middle")
.attr("transform", function(d) {
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getStartAngle(d) + ")";
} else {
return null;
}
})
.on("click", magnify);
var innerG = d3.selectAll("g.inner");
// Distort the specified node to 80% of its parent.
function magnify(node) {
// get and store parent sequence
var parentSequence = getAncestors(node)
text.transition().attr("opacity", 0);
spin(node);
// check if node has a parent. If so, iterate throught parentSequence and update the size of each node in the sequence
if (node.parent) {
for (var p = 0; p < parentSequence.length; p++) {
if (parent = parentSequence[p].parent) {
var parent,
x = parent.x,
k = 0.95;
parent.children.forEach(function(sibling) {
x += reposition(sibling, x, sibling === parentSequence[p]
? parent.dx * k / parentSequence[p].value
: parent.dx * (1 - k) / (parent.value - parentSequence[p].value));
});
} else {
reposition(parentSequence[p], 0, parentSequence[p].dx / parentSequence[p].value);
}
}
// if node does not have parent (center node) reset all values to original
} else {
if (parent = node.parent) {
var parent,
x = parent.x,
k = 0.95;
parent.children.forEach(function(sibling) {
x += reposition(sibling, x, sibling === node
? parent.dx * k / node.value
: parent.dx * (1 - k) / (parent.value - node.value));
});
} else {
reposition(node, 0, node.dx / node.value);
}
}
path.transition()
.duration(750)
.attrTween("d", arcTween)
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in node
if (e.x >= node.x && e.x < (node.x + node.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("x", function(d) {
return d.x;
})
.attr("transform", function(d) {
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getNewAngle(d) + ")";
} else {
return null;
}
});
}
});
}
function spin(d) {
var spin1 = new Promise (function(resolve, reject) {
var newAngle = - x(d.x + d.dx / 2);
// console.log('newAngle', newAngle)
innerG
.transition()
.duration(1500)
.attr("transform", "rotate(" + ((180 / Math.PI * newAngle)) + ")");
resolve("Success!");
});
spin1.then(function() {
var newerAngle = - x(d.x + d.dx / 2);
// console.log('newerAngle', newerAngle)
innerG
.transition()
.duration(1500)
.attr("transform", "rotate(" + ((180 / Math.PI * newerAngle)) + ")");
})
path
.classed("selected", function (x) { return d.name == x.name; });
}
// Recursively reposition the node at position x with scale k.
function reposition(node, x, k) {
// console.log(k)
node.x = x;
if (node.children && (n = node.children.length)) {
var i = -1, n;
while (++i < n) x += reposition(node.children[i], x, k);
}
return node.dx = node.value * k;
}
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
};
});
function getStartAngle(d) {
// Offset the angle by 90 deg since the '0' degree axis for arc is Y axis, while
// for text it is the X axis.
var thetaDeg = (180 / Math.PI * (arc.startAngle()(d) + arc.endAngle()(d)) / 2 - 90);
// If we are rotating the text by more than 90 deg, then "flip" it.
// This is why "text-anchor", "middle" is important, otherwise, this "flip" would
// a little harder.
return (thetaDeg > 90) ? thetaDeg - 180 : thetaDeg;
}
function getNewAngle(d) {
var thetaDeg = (180 / Math.PI * (arc.startAngle()(d) + arc.endAngle()(d)) / 2 - 90);
return (thetaDeg < 90) ? thetaDeg - 180 : thetaDeg;
}
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
I managed to figure it out by combining the tween methods from my original jsFiddle link, https://jsfiddle.net/2heLd2b1/, with the traditional tween used by a zoomable sunburst.You can see the implementation here: https://jsfiddle.net/6e4y0s11/
I altered my innerRadius and outerRadius from:
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) })
.cornerRadius(function(d) { return 5;});
to:
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(d.depth * 20, y(d.y)) })
.outerRadius(function(d) { return Math.max(100, y(d.y + d.dy)) })
.cornerRadius(function(d) { return 5;});
I also added:
function arcTweenZoom(d) {
var yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) {
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
so that I could interpolate the scales. The final result allows the inner parent level nodes to shrink without actually disappearing. The maintain a minimum radius based on their d.y values.
I'm using this model : http://www.jasondavies.com/coffee-wheel/:
I made a sunburst that works pretty well, except when I zoom in or out, sometimes it changes the current node.
When I look at my tooltip, I can see that the "zone" doesn't refer to the correct element and it sends me to this wrong element. I don't know where that could come from.
name should be "LDG", but there is a small zone in which there is a reference to another element.
loadSunburstTree: function() {
var width = 960,
height = width,
radius = Math.min(width, height) / 2,
padding = 5,
duration = 1000;
var x = d3.scale.linear().range([0, 2 * Math.PI]);
var y = d3.scale.sqrt().range([0, radius]);
var color = d3.scale.category20c();
var path = null;
var arc = null;
var text = null;
$.ajax({
url: $("#path").val() + ".json",
method: "GET",
success: function(result) {
var data = {
"name": "home",
"level" : "root",
"children": []
};
[...] // filling the variable data here (data are correct),
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height + 50)
.datum(data)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
path = svg.selectAll("path")
.data(partition.nodes)
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", click);
text = svg.selectAll("text").data(partition.nodes);
text.enter().append("text")
.style("full-opacity", 1)
.style("fill", function(d) {
return brightness(d3.rgb(colour(d))) < 125 ? "#eee" : "#000";
})
.attr("text-anchor", function(d) {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d) {
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle;
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
})
.text(function(d) {
if( d.dx < .01){
d3.select(this).style("display","none");
}
return d.depth ? d.name : "";
})
.on("click", click);
}
});
function click(d) {
path.transition()
.duration(duration)
.attrTween("d", arcTween(d));
text.transition()
.duration(duration)
.attrTween("transform", function(d) {
return function() {
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90;
var rotate = angle;
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
};
})
.attrTween("text-anchor", function(d) {
return function() {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
};
})
.style("fill-opacity", function(e) { return isParentOf(d, e) ? 1 : 1e-6; })
.each("end", function(e) {
d3.select(this).style("display","");
var st = e;
if (st.dx / d.dx < 0.01) {
d3.select(this).style("display","none");
} else {
d3.select(this).style("display","");
}
});
}
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
};
}
function isParentOf(p, c) {
if (p === c) return true;
if (p.children) {
return p.children.some(function(d) {
return isParentOf(d, c);
});
}
return false;
}
function colour(d) {
if (d.children) {
// There is a maximum of two children!
var colours = d.children.map(colour),
a = d3.hsl(colours[0]),
b = d3.hsl(colours[1]);
// L*a*b* might be better here...
return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
}
return d.colour || "#fff";
}
function brightness(rgb) {
return rgb.r * .299 + rgb.g * .587 + rgb.b * .114;
}
}
};
PS : Looks like the demo also have this problem, it's just harder to notice because there is no tooltip.
PPS : The clickable element is the text ! (in most of the case) The texts only have an opacity = 0 and are still on the view. That what makes the problem. They must not be correctly hidden. Still working on it.
That solved the problem. Texts tooltips are no longer visible nor clickable :
Just edit the text code of the function click(d) like follow :
function click(d) {
path.transition()
.duration(duration)
.attrTween("d", arcTween(d));
text.transition()
.duration(duration)
.attrTween("transform", function(d) {
return function() {
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90;
var rotate = angle;
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
};
})
.attrTween("text-anchor", function(d) {
return function() {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
};
})
//modified from here
.style("fill-opacity", function(e) {
if (isParentOf(d, e)) {
return 1;
} else {
return 0;
}
})
.each("end", function(e) {
if (e.dx / d.dx < 0.01 || $(this).css("fill-opacity") == 0) {
d3.select(this).style("display","none");
} else {
d3.select(this).style("display","");
}
});
//to here
}
I am trying to render a treemap in d3.js and using the example code - http://mbostock.github.com/d3/ex/treemap.html:
I am encountering an issue in that my source JSON is a flat heirachy, and therefore the call to treemap.nodes is wrong.
Can anyone advise how to return a flat hierarchy?
My sample JSON:
[
{"ticker":"$GOOG","count":"82","sentiment":"9"},
{"ticker":"$AAPL","count":"408","sentiment":"8"}, ...
And my full code so far:
d3.json("/finance/data", function(json) {
var w = 960,
h = 500,
color = d3.interpolateRgb("rgb(0,255,0)", "rgb(255,0,0)");
//xcolor = d3.scale.linear().domain([-26,13]).range("rgb(0,255,0)", "rgb(255,0,0)"),
x = d3.scale.linear().domain([-26,13]).range([0, 1]),
stepsize = [2.46, 1.66],
minval = [-16.28, -16.67];
var treemap = d3.layout.treemap()
.size([w, h])
.sticky(true)
.value(function(d) { return d.count; });
var div = d3.select("#treemap-transition").append("div")
.style("position", "relative")
.style("width", w + "px")
.style("height", h + "px");
div.data([json]).selectAll("div")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "cell")
.style("background", function(d) { return treemap_color(d.sentiment, 2.5, 10); })
.call(cell)
.attr("text-anchor", "middle")
.text(function(d) { return d.children ? null : d.ticker; });
function cell() {
this
.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return d.dx - 1 + "px"; })
.style("height", function(d) { return d.dy - 1 + "px"; })
.style("text-anchor", "middle");
}
function treemap_color(value, stepsize, steps) {
if (value == 0) {
return "rgb(0,0,0)";
} else if (value < 0 ) {
var x = Math.round( (255/steps) * Math.abs( value/stepsize) );
return 'rgb(0,' + x + ',0)'; //DECREASE in unemployment => green
} else {
var y = Math.round( (255/steps) * value/stepsize );
return 'rgb(' + y + ',0,0)'; //INCREASE in unemployment => red
}
}
});
Appreciate any comments.
Not sure how you mean flat. If you mean you don't have a "Nodes" supercategory in D3, then you can just use:
.data(treemap)
instead of:
.data(treemap.nodes)
but without the "children" attribute in your JSON, you won't get any hierarchical packing.