I have been trying to replicate https://beta.observablehq.com/#mbostock/d3-zoomable-sunburst this in pure JS to use it in one of my projects. I am using PHP and Ajax to load Dynamic Data in JavaScript. I think the code in the Observable link is not in pure JS but rather Node or something else.
I am a newbie in Scripting, hence it is becoming very difficult for me to understand the written code. I do know that a pure JS will need the data (flare.json) in a specific format, which will generate the expected output. I can control the JSON structure from the backend, but I am unable to generate an output like the link.
I have followed multiple examples online:
https://bl.ocks.org/mbostock/4348373
And the same in d3 version 4 (which is very similar to v5, used in the Observable example):
https://bl.ocks.org/maybelinot/5552606564ef37b5de7e47ed2b7dc099
I have been trying to convert the Observable Zoomable Sunburst into JS functions, but I am unable to make it work. I have the exact same flare.json file and tried to recreate exact functions as therein Observable one. But it still is not working.
I am attaching my work. How can I get it working?
Sample Work
I have also tried to seek help on the Google Groups for d3-js, but I haven't got any help from there too.
The closest possible output which I have achieved till now is mentioned below:
var margin = {top: 288, right: 416, bottom: 288, left: 416},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 5;
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 ; })
.padAngle(.01)
.padRadius(radius / 3)
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });
// d3.json("https://api.myjson.com/bins/byw4q", function(error, root) {
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
if (error) throw 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 path = svg.selectAll("path")
.data(partition.nodes(root).slice(1))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn);
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]);
path = path.data(partition.nodes(root).slice(1), 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)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
}
});
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");
<!DOCTYPE html>
<meta charset="utf-8">
<style>
circle,
path {
cursor: pointer;
}
circle {
fill: none;
pointer-events: all;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
</body>
The code written is pure javascript tough, whatever the data you are getting from Ajax the same endpoint you just need to pass here,
The example I am running here means the same should work in your project as well, Instead of calling the Ajax you can pass your Json in this line
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root)
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #fff;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 960,
height = 700,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);
var y = d3.scaleSqrt()
.range([0, radius]);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var partition = d3.partition();
var arc = d3.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x0)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x1)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y0));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y1));
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
if (error) throw error;
root = d3.hierarchy(root);
root.sum(function(d) {
return d.size;
});
svg.selectAll("path")
.data(partition(root).descendants())
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
.on("click", click)
.append("title")
.text(function(d) {
return d.data.name + "\n" + formatNumber(d.value);
});
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
svg.selectAll("text")
.attr("dy", "0.35em")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.data(root.descendants().slice(1))
.enter().append("text")
.text(d => d.data.name);
});
function click(d) {
svg.transition()
.duration(750)
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]),
yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
return function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
};
})
.selectAll("path")
.attrTween("d", function(d) {
return function() {
return arc(d);
};
});
}
d3.select(self.frameElement).style("height", height + "px");
</script>
Related
Instead of printing the number of children a specific node in flare.json has, I want to print the real value of the child node from flare.json. When hovering over a parent node (a node with children), I want the sum of all children (and sub-children, ...) values printed.
There are similar questions (not specific d3) suggesting a recursive function. But all the code snippets did not help me so far. Does d3 ship with some function to achieve this behavior?
var width = 300,
height = 300,
radius = Math.min(width, height) / 2,
color = d3.scale.category20c();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .50 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
d3.json("https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json", function(error, root) {
if (error) throw error;
var data = root;
var path = svg.datum(root).selectAll("path")
.data(partition.nodes)
.enter()
.append("path")
.attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
.attr("d", arc)
.style("stroke", "#fff")
.style("fill-rule", "evenodd")
.style("fill", howToFill)
.each(stash)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
var value = function(d) { return d.value; };
});
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
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 howToFill(d) {
if ( d.name == "-" ) {
return "#fff";
} else {
return color((d.children ? d : d.parent).name);
}
}
// Mouseover
function onMouseOver(d) {
d3.select(this)
svg.append("text")
.style("text-anchor", "middle")
.text(function() { return d.name + " - " + d.value; });
}
function onMouseOut(d) {
d3.select(this)
svg
.select("text")
.remove();
}
d3.select(self.frameElement).style("height", height + "px");
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link rel="stylesheet" type="text/css" href="main.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<script type="text/javascript" src="chart.js"></script>
</body>
Here is a version of your code which displays the actual counts:
var width = 300,
height = 300,
radius = Math.min(width, height) / 2,
color = d3.scale.category20c();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .50 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
d3.json("https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json", function(error, root) {
if (error) throw error;
// Deep copy of the root object (for use by the mouseover function)
// since the d3 partitioning will modify root and only keep
// partitioning details:
var initialData = JSON.parse(JSON.stringify(root));
var data = root;
var path = svg.datum(root).selectAll("path")
.data(partition.nodes)
.enter()
.append("path")
.attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
.attr("d", arc)
.style("stroke", "#fff")
.style("fill-rule", "evenodd")
.style("fill", howToFill)
.each(stash)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
var value = function(d) { return d.value; };
// Let's move the onMouseOver function inside the json extractor in order
// to easily work with "initialData":
function onMouseOver(d) {
d3.select(this)
svg.append("text")
.style("text-anchor", "middle")
//.text(function() { return d.name + " - " + d.value; });
.text(function() {
return d.name + " - " + sumValue(findElem(initialData, d.name, d.depth, 0));
});
}
});
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
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 howToFill(d) {
if ( d.name == "-" ) {
return "#fff";
} else {
return color((d.children ? d : d.parent).name);
}
}
function onMouseOut(d) {
d3.select(this)
svg
.select("text")
.remove();
}
d3.select(self.frameElement).style("height", height + "px");
function findElem(elmt, name, searchedDepth, currDepth) {
if (currDepth > searchedDepth)
return undefined
else if (elmt.name == name && searchedDepth == currDepth) {
return elmt;
}
else if (elmt.children) {
var result = elmt.children.map(c => findElem(c, name, searchedDepth, currDepth + 1)).filter(x => x);
return result.length == 1 ? result[0] : undefined;
}
else
return undefined;
}
function sumValue(elmt) {
try {
if (elmt.children)
return elmt.children.map(c => sumValue(c)).reduce((a, b) => a + b, 0);
else
return elmt.value;
} catch(err) {
// The try catch is there to hide the exception due to the "data"
// value which is not supported. Not sure how to cleanly fix this
// corner case:
return "error";
}
}
<meta charset="utf-8">
<head>
<link rel="stylesheet" type="text/css" href="main.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<script type="text/javascript" src="chart.js"></script>
</body>
During the mouseover, let's first find back the actual data element which is hovered. The thing is, the d3 layout partitioning removes data information from the model... So that's why we need to get back to the original data to find back the node in question:
function findElem(elmt, name, searchedDepth, currDepth) {
if (currDepth > searchedDepth)
return undefined
else if (elmt.name == name && searchedDepth == currDepth) {
return elmt;
}
else if (elmt.children) {
var result = elmt.children.map(c => findElem(c, name, searchedDepth, currDepth + 1)).filter(x => x);
return result.length == 1 ? result[0] : undefined;
}
else
return undefined;
}
Which is called from the mouseover function this way:
.text( function() {
return d.name + " - " + sumValue(findElem(initialData, d.name, d.depth, 0));
});
where initialData is a deep copy of the json (otherwise, since the extracted json root is modified by the partitioning, we would loose original data information).
Notice the usage of the depth in order to handle the fact that different nodes can have the same label (for instance there are several nodes at different depths named "data").
Once we have the node in question, we can recursively sum all children values this way:
function sumValue(elmt) {
if (elmt.children)
return elmt.children.map(c => sumValue(c)).reduce((a, b) => a + b, 0);
else
return elmt.value;
}
I'm new to programming D3, we are having a problem updating the sunburst chart without causing it to break its transitions. When the data is changed, parts of the chart stop resizing for the zooming feature and appear to be over each other. Additionally, I get errors resulting, apparently, from the interpolation of the flags each "arc" in the graph (i.e. Error: attribute d: Expected arc flag ('0' or '1'), "…125862517296 0 0.987032831999999…".). We are using a tween that is specific for this problem and thus can't understand what might be causing it.
Since I'm inexperienced in this language I've spent several days on this problem only to see it come back. If any of you could help I would appreciate it immensely.
Here's a working fiddle that I created to illustrate our problem:
- https://jsfiddle.net/v9ab0vms/1/
The problem can be seen by waiting 5 seconds and clicking the "Commercial" and then "Communications" arcs. Causing the transition errors and breaking some of the arcs. Zooming the graph while the data gets changed causes it to break in other ways, any help on that would also be greatly appreciated. I was just trying to fix it step by step.
Picture of broken chart arcs
The code has to be posted as well so I'm just inserting the two main functions for the sunburst:
function genSunburst2() {
var width = d3.select("#container_sunburst").style("width").split("px")[0],
height = d3.select("#container_sunburst").style("height").split("px")[0],
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var svg = d3.select("#sunburst")
.attr("id", "sunburst")
.attr("width", width)
.attr("height", height)
.select("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
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, y(d.y));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y + d.dy));
});
function computeTextRotation(d) {
var angle = x(d.x + d.dx / 2) - Math.PI / 2;
return angle / Math.PI * 180;
}
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 arcTweenUpdate(a) {
console.log(path);
var _self = this;
var i = d3.interpolate({
x: this.x0,
dx: this.dx0
}, a);
return function(t) {
var b = i(t);
console.log(window);
_self.x0 = b.x;
_self.dx0 = b.dx;
return arc(b);
};
}
updateSunburst3 = function() {
if (sunburstClick) {
sunburstClick = false;
return;
}
var root = createJsonDataset();
// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));
// ENTER
var g = gs.enter().append("g").on("click", click);
// UPDATE
var path = g.append("path");
gs.select('path')
.style("fill", function(d) {
return color(d.name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
})
.transition().duration(500)
.attr("d", arc);
var text = g.append("text");
gs.select('text')
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
function click(d) {
sunburstClick = true;
console.log(d);
// fade out all text elements
/*if (d.size !== undefined) {
d.size += 100;
};*/
text.transition().attr("opacity", 0);
console.log(path);
for (var i = 0; i < path[0].length; ++i) {
if (path[0][i] === undefined || path[0][i] === null) {
path[0].splice(i, 1);
--i;
}
}
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.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("transform", function() {
return "rotate(" + computeTextRotation(e) + ")"
})
.attr("x", function(d) {
return y(d.y);
});
}
});
userSelection = undefined;
purposeSelection = undefined;
// TODO: alterar para ele ter em conta a hierarquia da selecao corrente
// para so sair de uma hierarquia
if (typeof d.name != "undefined" && d.name != "") {
if (typeof d.size === "undefined") {
userSelection = d.name;
purposeSelection = undefined;
} else {
userSelection = d.parent.name;
purposeSelection = d.name;
}
} else {
// if only the user was selected, back out to no selection
if (purposeSelection === undefined && userSelection != undefined) {
userSelection = undefined;
// if both the user and the purpose were selected, back out to just user selection
} else if (purposeSelection != undefined && userSelection != undefined) {
purposeSelection = undefined;
}
}
applySelection();
}
// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();
}
}
Thank you in advance
Edit: To clarify:
This is in D3 V3.
We have two variables one with the full dataset
and another with the "working" dataset. We apply filters to the full
dataset and store the result in the working dataset.
I am trying to alter the traditional zooming feature on a sunburst chart. Traditionally when you click on a partition, that partition grows to cover 100% of the base layer while all other partitions on the same layer disappear. The children of the selected partition all grow to fill the newly created space.
My current code does just what I stated above. I would like to alter my code to allow for the selected partition to only take up 75% of the base layer. The children elements will grow to cover this new space but the remaining 25% will still contain all other non-selected partitions.
I have tried altering the 't' value that is returned from d3.interpolate() but I have had unpredictable results.
I hope my description is clear.
Does anyone have any thoughts on this?
<script>
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var color = d3.scale.category20c();
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 svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
svg.call(tip);
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, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) });
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
// .attr("stroke", 'black')
// .style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.style("fill", function(d, i) {
return color(i);
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
if (percent(d) > 1.35) {
return d.name;
}
})
.attr('font-size', function(d) {
if (d.value < 100000) {
return '10px'
} else {
return '20px';
}
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
function click(d) {
console.log(d)
// fade out all text elements
text.transition().attr("opacity", 0);
path
.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.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("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
});
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
console.log(d.name, x.domain())
console.log(d.name, y.domain())
console.log(d.name, y.range())
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) {
console.log(t)
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
I found the solution here: https://bl.ocks.org/mbostock/1306365. This example manages the zoom without getting rid of the sibling nodes.
I have a D3 partition which shows all the levels for the entire partition.
I would like to only show the first level when the chart loads and then show subsequent levels on click.
For example in this Tree the next level is shown on click of a node: D3Tree
Here is the code for my partition: Plunker link
$(document).ready(function(){
var width = 600,
height = 400,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var partition = d3.layout.partition()
.value(function(d) {
if(d.depth == 2)
console.log(d.depth, d);
return 1; // 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, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
d3.json("flare.json", function(error, root) {
if (error) throw error;
svg.selectAll("path")
.data(partition.nodes(root))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", click)
.append("title")
.text(function(d) { return d.name + "\n" + formatNumber(d.value); });
});
function click(d) {
svg.transition()
.duration(750)
.tween("scale", function() {
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(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
})
.selectAll("path")
.attrTween("d", function(d) { return function() { return arc(d); }; });
}
d3.select(self.frameElement).style("height", height + "px");
});
I would like to do something like toggle on click:
// Toggle children.
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
Where the children get set and unset, then redrawn
To do something like the tree layout would be a little tough doing it with the help of display is a a cake walk.
When the path are drawn for the first time make all the nodes whose depth > 1 disappear using display:none:
svg.selectAll("path")
.data(partition.nodes(root))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.style("display", function(d) {
if (d.depth > 1) {
return "none";//nodes whose depth is more than 1 make its vanish
} else {
return "";
}
})
Now on node click make all nodes reappear except when root node is clicked.
.style("display", function(d1) {
if (d.depth == 0 && d1.depth > 1) {
return "none"//root node clicked so show only 2 depths.
} else {
return "";
}
})
Working code here
I have a Zoomable Sunburst diagram exhibiting strange problems with arc sizing.
http://colinwhite.net/Sunburst/
I would expect the size the arcs to be proportional to number of children (shown in the tool tip). Yet, I have parent arcs with few children, that are proportionally larger than their peers with far more children. Arc size is not reflective of number of children. I've tried various other d3.scales which haven't helped. What am I doing wrong?
My code is largely boiler plate from the D3 examples.
var width = 760, height = 700,
radius = Math.min(width, height) / 2.25,
color = d3.scale.category20c();
var x = d3.scale.linear().range([0, 2 * Math.PI]),
y = d3.scale.sqrt().range([0, radius]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .52 + ")");
var partition = d3.layout.partition()
.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.x))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("opacity", 0);
d3.json("data/getJson.php", function(error, data) {
var treeData = genJSON(data, ['Location', 'Provider', 'Diagnosis', 'Procedure']);
console.log(treeData);
var path = svg.selectAll("path")
.data(partition.nodes(treeData))
.enter().append("svg:path")
.attr("d", arc)
.style("fill-rule", "evenodd")
.style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", click)
.on("mouseover", function(d) {
tooltip.html(function() {
return (d.children ? d : d.parent).name + " (" + d.value + ")";
});
return tooltip.transition()
.duration(50)
.style("opacity", 0.9);
})
.on("mousemove", function(d) {
return tooltip
.style("top", (d3.event.pageY-10)+"px")
.style("left", (d3.event.pageX+10)+"px");
})
.on("mouseout", function(){return tooltip.style("opacity", 0);});
function click(d) {
path.transition()
.duration(750)
.attrTween("d", arcTween(d));
}
});
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); };
};
}
The JSON is nested with this genJSON function -http://colinwhite.net/Sunburst/js/treeRemapper.js
Thanks for any help or advice.
Changing the partition call, to include a .sort(null) like this -
var partition = d3.layout.partition()
.sort(null) //<-- was missing this
.value(function(d) { return 1; });
Seems to have resolved the strange arc scale problems.