Zoomable sunburst chart in R (SunburstR package) - javascript

I'm trying to customize a sunburst chart from different examples that I've found accross the internet. I'm working in R (and Shiny as well) and there is one package that I thought could a good working base, that is SunburstR.
A working example can be viewed here.
What I want to have :
breadcrumbs
zoomable (forward when cliking a child, and backward when clicking the center)
fading as you hover the childs it fades other elements except current curent hierarchy
the percentage in the middle of the chart (or elsewhere relevant) as well as the breadcrumbs. Could be one or the other as long as it is somewhere on the page.
emphasizing with a bigger circle all around the chart. You'd see the whole circle if hovering the center, and you'd see the current footprint of current hovered element (from startangle to endangle). This is just to help seeing how much what you're hovering is reprensenting compare to the whole thing. This circle would also be the bounding circle, eg when you are exiting it, it cancels the fading since you're not hovering the chart anymore.
I have a little bit of code of everything but struggle to put everything together. Here is where things are currently standing :
I've got the fading ok.
I've got the percentages (both center and breadcrumbs)
I've got the outerRadius defined to allow for the bounding circle around the chart, currently visible at all times. I can't find a way to make it appear when I'm hovering (only the arc of the current hovered element, whole circle when hovering the center) and put it back to #fff
when not hovering anymore this circle.
I don't have (at all) the abitlity to zoom
Current sunburst.js :
HTMLWidgets.widget({
name: 'sunburst',
type: 'output',
factory: function(el, width, height) {
var instance = {};
instance.chart = {};
var dispatch = d3.dispatch("mouseover","mouseleave","click");
d3.rebind(instance.chart, dispatch, 'on');
var draw = function(el, instance) {
// would be much nicer to implement transitions/animation
// remove previous in case of Shiny/dynamic
d3.select(el).select(".sunburst-chart svg").remove();
var x = instance.x;
var json = instance.json;
var chart = instance.chart;
// Dimensions of sunburst
var width = el.getBoundingClientRect().width - (x.options.legend.w ? x.options.legend.w : 75);
var height = el.getBoundingClientRect().height - 70;
var radius = Math.min(width, height) / 2;
var outerRadius = radius/3.5; // reserved pixels all around the vis
d3.select(el).select(".sunburst-chart").append("svg")
.style("width", width + "px") // shouldnt have to do this
.style("height", height + "px"); // shouldnt have to do this
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
// these will be the defaults
var b = {
w: 0, h: 30, s: 3, t: 10
};
// if breadcrumb is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.breadcrumb).map(function(ky){
b[ky] = x.options.breadcrumb[ky];
});
/*
// Mapping of step names to colors.
var colors = {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
};
*/
var colors = d3.scale.category20();
if(x.options.colors !== null){
// if an array then we assume the colors
// represent an array of hexadecimal colors to be used
if(Array.isArray(x.options.colors)) {
try{
colors.range(x.options.colors)
} catch(e) {
}
}
// if an object with range then we assume
// that this is an array of colors to be used as range
if(x.options.colors.range){
try{
colors.range(x.options.colors.range)
} catch(e) {
}
}
// if an object with domain then we assume
// that this is an array of colors to be used as domain
// for more precise control of the colors assigned
if(x.options.colors.domain){
try{
colors.domain(x.options.colors.domain);
} catch(e) {
}
}
// if a function then set to the function
if(typeof(x.options.colors) === "function") {
colors = x.options.colors;
}
}
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select(el).select(".sunburst-chart").select("svg")
.append("g")
.attr("id", el.id + "-container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
//.size([2 * Math.PI, radius * radius])
.size([2 * Math.PI, (radius - outerRadius) * (radius - outerRadius)])
.value(function(d) { return d[x.options.valueField || "size"]; });
// check for sort function
if(x.options.sortFunction){
partition.sort(x.options.sortFunction);
}
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return (d.dy == d.y) ? Math.sqrt(d.dy)/3 : Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
createVisualization(json);
// set up a container for tasks to perform after completion
// one example would be add callbacks for event handling
// styling
if (!(typeof x.tasks === "undefined") ){
if ( (typeof x.tasks.length === "undefined") ||
(typeof x.tasks === "function" ) ) {
// handle a function not enclosed in array
// should be able to remove once using jsonlite
x.tasks = [x.tasks];
}
x.tasks.map(function(t){
// for each tasks call the task with el supplied as `this`
t.call({el:el,x:x,instance:instance});
});
}
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("circle")
.attr("r", radius - outerRadius)
.style("opacity", 0);
var highlight = vis.append("circle")
.attr("r", radius)
.style("fill", "#eee");
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors.call(this, d.name); })
.style("opacity", 1)
.on("mouseover", mouseover)
//.on("mouseleave", mouseleave)
.on("click", click);
// Add the mouseleave handler to the bounding circle.
d3.select(el).select("#"+ el.id + "-container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
drawLegend(nodes);
d3.select(el).select(".sunburst-togglelegend").on("click", toggleLegend);
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
//highlight.attr("d", arc(d));
//highlight.style("fill", "#eee");
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
var countString = [
'<span style = "font-size:.7em">',
d3.format("1.2s")(d.value) + ' of ' + d3.format("1.2s")(totalSize),
'</span>'
].join('')
var explanationString = "";
if(x.options.percent && x.options.count){
explanationString = percentageString + '<br/>' + countString;
} else if(x.options.percent){
explanationString = percentageString;
} else if(x.options.count){
explanationString = countString;
}
//if explanation defined in R then use this instead
if(x.options.explanation !== null){
explanationString = x.options.explanation.bind(totalSize)(d);
}
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "")
.style("top",((height - 35)/2) + "px")
.style("width",width + "px")
.html(explanationString);
var sequenceArray = getAncestors(d);
chart._selection = sequenceArray.map(
function(d){return d.name}
);
dispatch.mouseover(chart._selection);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.select(el).selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
//highlight.attr("d", null);
//highlight.style("fill", "#fff");
dispatch.mouseleave(chart._selection);
chart._selection = [];
// Hide the breadcrumb trail
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.select(el).selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.select(el).selectAll("path")
.transition()
.duration(250)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "hidden");
}
function click(d,i) {
var sequenceArray = getAncestors(d);
dispatch.click(sequenceArray.map(
function(d){return d.name}
));
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select(el).select(".sunburst-sequence").append("svg")
.attr("width", width)
//.attr("height", 50)
.attr("id", el.id + "-trail");
// Add the label at the end, for the percentage.
trail.append("text")
.attr("id", el.id + "-endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
if (b.w <= 0) {
// calculate breadcrumb width based on string length
points.push(d.string_length + ",0");
points.push(d.string_length + b.t + "," + (b.h / 2));
points.push(d.string_length + "," + b.h);
} else {
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
}
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select(el).select("#" + el.id + "-trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("g");
if (b.w <= 0) {
// Create a node array that contains all the breadcrumb widths
// Calculate positions of breadcrumbs based on string lengths
var curr_breadcrumb_x = 0;
nodeArray[0].breadcrumb_x = 0;
nodeArray[0].breadcrumb_h = 0;
entering.append("polygon")
.style("z-index",function(d,i) { return(999-i); })
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", b.t + 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "left")
.text(function(d) { return d.name; });
// Remove exiting nodes.
g.exit().remove();
// loop through each g element
// calculate string length
// draw the breadcrumb polygon
// and determine if breadcrumb should be wrapped to next row
g.each(function(d,k){
var crumbg = d3.select(this);
var my_string_length = crumbg.select("text").node().getBoundingClientRect().width;
nodeArray[k].string_length = my_string_length + 12;
crumbg.select("polygon").attr("points", function(d){
return breadcrumbPoints(d, k);
});
var my_g_length = crumbg.node().getBoundingClientRect().width;
curr_breadcrumb_x += k===0 ? 0 : nodeArray[k-1].string_length + b.s;
nodeArray[k].breadcrumb_h = k===0 ? 0 : nodeArray[k-1].breadcrumb_h;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
nodeArray[k].breadcrumb_h += b.h; // got to next line
curr_breadcrumb_x = b.t + b.s; // restart counter
}
nodeArray[k].breadcrumb_x = curr_breadcrumb_x;
});
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + d.breadcrumb_x + ", "+d.breadcrumb_h+")";
});
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", function(d){
var bend = d3.select(this);
var curr_breadcrumb_x = nodeArray[nodeArray.length-1].breadcrumb_x + nodeArray[nodeArray.length-1].string_length + b.t + b.s;
var my_g_length = bend.node().getBoundingClientRect().width;
var curr_breadcrumb_h = nodeArray[nodeArray.length-1].breadcrumb_h + b.h/2;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
curr_breadcrumb_h += b.h + b.h/2;
curr_breadcrumb_x = b.t + b.s; // restart counter
}
bend.datum({
"breadcrumb_x": curr_breadcrumb_x,
"breadcrumb_h": curr_breadcrumb_h
});
return curr_breadcrumb_x;
})
.attr("y", function(d){return d.breadcrumb_h})
.attr("dy", "0.35em")
.attr("text-anchor", "start")
.text(percentageString);
} else {
entering.append("polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
}
// Make the breadcrumb trail visible, if it's hidden.
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "");
}
function drawLegend(nodes) {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
// if legend is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.legend).map(function(ky){
li[ky] = x.options.legend[ky];
});
// remove if already drawn
d3.select(el).select(".sunburst-legend svg").remove();
// get labels from node names
var labels = d3.nest()
.key(function(d) {return d.name})
.entries(
nodes.sort(
function(a,b) {return d3.ascending(a.depth,b.depth)}
)
)
.map(function(d) {
return d.values[0];
})
.filter(function(d) {
return d.name !== "root";
});
var legend = d3.select(el).select(".sunburst-legend").append("svg")
.attr("width", li.w)
.attr("height", labels.length * (li.h + li.s));
var g = legend.selectAll("g")
.data( function(){
if(x.options.legendOrder !== null){
return x.options.legendOrder;
} else {
// get sorted by top level
return labels;
}
})
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors.call(this, d.name); });
g.append("text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
}
function toggleLegend() {
var legend = d3.select(el).select(".sunburst-legend")
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
};
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
return {
renderValue: function(x) {
instance.x = x;
// x.data should be a data.frame in R so an Javascript Object of Objects
// but buildHierarchy expects an Array of Arrays
// so use d3.zip and apply to do this
var json = [];
if(x.csvdata !== null){
json = buildHierarchy(
d3.zip.apply(
null,
Object.keys(x.csvdata).map(function(ky){return x.csvdata[ky]})
)
);
} else {
json = x.jsondata
}
instance.json = json;
draw(el, instance);
},
resize: function(width, height) {
draw(el, instance);
},
instance: instance
};
}
});
I have found this question where Skip Jack gives this pen, but I can't get it to work along with the rest of the sunburstR package... And still this pen would miss the bounding circle and the percentages.
I must emphasize that I'm pretty new to JS so please excuse me if this actually quite easy to solve.
Any help would be welcome, thanks ahead of time for your support !

Related

How to remove the default "left" position of node. d3.js

I have a problem where the node always move left. Here is my code:
<div id="tree-container"></div>
<script src="<?php echo base_url('d3js-nettree/js/d3.js');?>" type="text/javascript"></script>
<script>
<?php if(isset($params['user_code'])){ ?>
treeJSON = d3.json("<?php echo base_url('d3js-parse/'.$params['user_code'].'.json');?>", function(error, treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// Misc. variables
var zoomFactor = 1;
var i = 0;
var duration = 750;
var root;
var rectWidth = 160, rectHeight = 125;
// size of the diagram
var viewerWidth = $("#tree-container").width();
var viewerHeight = 500;
var tree = d3.layout.tree()
.size([viewerWidth, viewerHeight]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x, d.y];
});
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
d3.selectAll('button').on('click', function(){
if(this.id === 'zoom_in') {
zoomFactor = zoomFactor + 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'zoom_out') {
zoomFactor = zoomFactor - 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'up_level') {
updateNewTree("ID04838614");
}
});
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.x0;
y = -source.y0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// define click event
function click(d) {
console.log("clicked");
// if (d3.event.defaultPrevented) return; // click suppressed
//d = toggleChildren(d);
if(d.url !== "") {
window.open(d.url, "_self");
} else {
updateNewTree(d.distributor_code);
}
update(d);
centerNode(d);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newWidth = d3.max(levelWidth) * 300; // 300 pixels per line
tree = tree.size([newWidth, viewerHeight]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
//d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
d.y = (d.depth * 200); //200px per level.
});
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.on('click', click);
nodeEnter.append("image")
.attr("href", "<?php echo base_url('assets/images/people.png');?>")
.attr("x", -rectWidth/4)
.attr("y", -rectHeight-75)
.attr("width", 75)
.attr("height", 75);
nodeEnter.append("rect")
.attr('class', 'nodeRect')
.attr("x", -rectWidth/2)
.attr("y", -rectHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("width", rectWidth)
.attr("height", rectHeight)
.style("fill", function(d) {
//return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr('class', 'txt1')
.attr("x", 0)
.attr("y", -rectHeight+15)
.attr('class', 'textBold')
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.user_code;
});
nodeEnter.append("text")
.attr('class', 'txt2')
.attr("x", 0)
.attr("y", -rectHeight+25)
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.fullname;
});
//IT GOES ON FOR SEVERAL MORE
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", 0)
.attr("height", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
nodeExit.select("image")
.style("display", "none");
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function updateNewTree($base_id) {
// Get the data again
d3.json("nettree-alt.json", function(error, treeData) {
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = -100;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
<?php } ?>
function EnableTreeMode(){
$('.tree').treegrid({
expanderExpandedClass: 'glyphicon glyphicon-minus',
expanderCollapsedClass: 'glyphicon glyphicon-plus'
});
$('.tree').treegrid('collapseAll');
}
EnableTreeMode();
function collapse(){
$('.tree').treegrid('collapseAll');
}
function expand(){
$('.tree').treegrid('expandAll');
}
</script>
The problem is like this, i have a "tree" that consist of users which pulls data from a json file:
USER 1
/\
USER 2 USER 3
What i was expecting when i remove a user, say user 2, the "tree" should be like this:
USER 1
/\
NONE USER 3
But instead the result i got were like this:
USER 1
/\
USER 3 NONE
After i deleted USER 2 the USER 3, which is on the right node, moved on the left node. I have tried debugging my code line by line but still no result.
TIA.

R Shiny + D3js - updating plot with new input results in overlapping graphs

I'm working on a Shiny dashboard where I would like to display a barplot corresponding to a selected dataset a user can choose. While I can create the barplot and switch between the inputs, I have a problem with the switch of the graphs:
Switching between my datasets causes overlapping of the bars in the plot (the columns are added to the previous plot).
I thought about trying it with ".transition()" to somehow make a swift between the datasets but unfortunately, I do not get it to work. And I'm not sure if it makes any sense at all. I also tried to use ".remove" but it would always remove a bar with each switch. ".exit().remove()" did not work for me.
Has anybody an idea how I could prevent the overlapping?
My data:
Histogram.js (I did comment out my approaches, mentioned above)
// !preview r2d3 data=readr::read_tsv("Beispiel.tsv"), d3_version = "5", container = "div"
//r2d3: https://rstudio.github.io/r2d3, d3_version=3
// d3.tip
// Returns a tip
/* !preview r2d3 data=readr::read_tsv("Beispiel.tsv"), d3_version = "3", container = "div"*/
/* !preview r2d3 data=readr::read_tsv("Histogram_Data_Barplot.tsv"), d3_version = "3", container = "div" */
d3.tip = function() {
var direction = d3TipDirection,
offset = d3TipOffset,
html = d3TipHTML,
rootElement = document.body,
node = initNode(),
svg = null,
point = null,
target = null;
function tip(vis) {
svg = getSVGNode(vis);
if (!svg) return;
point = svg.createSVGPoint();
rootElement.appendChild(node);
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments);
if (args[args.length - 1] instanceof SVGElement) target = args.pop();
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop ||
rootElement.scrollTop,
scrollLeft = document.documentElement.scrollLeft ||
rootElement.scrollLeft;
nodel.html(content)
.style('opacity', 1).style('pointer-events', 'all');
while (i--) nodel.classed(directions[i], false);
coords = directionCallbacks.get(dir).apply(this);
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px');
return tip;
};
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl();
nodel.style('opacity', 0).style('pointer-events', 'none');
return tip;
};
// Public: Proxy attr calls to the d3 tip container.
// Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
// eslint-disable-next-line no-unused-vars
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n);
}
var args = Array.prototype.slice.call(arguments);
d3.selection.prototype.attr.apply(getNodeEl(), args);
return tip;
};
// Public: Proxy style calls to the d3 tip container.
// Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
// eslint-disable-next-line no-unused-vars
tip.style = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n);
}
var args = Array.prototype.slice.call(arguments);
selection.prototype.style.apply(getNodeEl(), args);
return tip;
};
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction;
direction = v == null ? v : functor(v)
return tip;
};
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset;
offset = v == null ? v : functor(v)
return tip;
};
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html;
html = v == null ? v : functor(v)
return tip;
};
// Public: sets or gets the root element anchor of the tooltip
//
// v - root element of the tooltip
//
// Returns root node of tip
tip.rootElement = function(v) {
if (!arguments.length) return rootElement;
rootElement = v == null ? v : functor(v)
return tip;
};
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if (node) {
getNodeEl().remove()
node = null;
}
return tip;
};
function d3TipDirection() { return 'n' }
function d3TipOffset() { return [0, 0] }
function d3TipHTML() { return ' ' }
var directionCallbacks = d3.map({
n: directionNorth,
s: directionSouth,
e: directionEast,
w: directionWest,
nw: directionNorthWest,
ne: directionNorthEast,
sw: directionSouthWest,
se: directionSouthEast
}),
directions = directionCallbacks.keys();
function directionNorth() {
var bbox = getScreenBBox(this);
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
};
}
function directionSouth() {
var bbox = getScreenBBox(this);
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
};
}
function directionEast() {
var bbox = getScreenBBox(this);
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
};
}
function directionWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
};
}
function directionNorthWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function directionNorthEast() {
var bbox = getScreenBBox(this)
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function directionSouthWest() {
var bbox = getScreenBBox(this)
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function directionSouthEast() {
var bbox = getScreenBBox(this)
return {
top: bbox.se.y,
left: bbox.se.x
}
}
function initNode() {
var div = d3.select(document.createElement('div'))
div
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box')
return div.node()
}
function getSVGNode(element) {
var svgNode = element.node()
if (!svgNode) return null
if (svgNode.tagName.toLowerCase() === 'svg') return svgNode
return svgNode.ownerSVGElement
}
function getNodeEl() {
if (node == null) {
node = initNode()
// re-add node to DOM
rootElement.appendChild(node)
}
return d3.select(node)
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast),
// nw(northwest), sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox(targetShape) {
var targetel = target || targetShape
while (targetel.getScreenCTM == null && targetel.parentNode != null) {
targetel = targetel.parentNode
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
// Private - replace D3JS 3.X d3.functor() function
function functor(v) {
return typeof v === 'function' ? v : function() {
return v
}
}
return tip
}
//d3----------------------------------
var margin = {top: 40, right: 20, bottom: 30, left: 40},
/// width and height will be generated by R automatically. Removed parts: Numbers after width = xxx and height = xxx.
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var formatPercent = d3.format("");
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(.1);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y)
// .tickFormat(formatPercent);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>SubCluster:</strong> <span style='color:red'>" + d.SubCluster + "</span>";
})
//R already created a SVG element. So we do not need to create it again. Removing the following part:
//d3.select("body")
var svg = div.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 + ")");
svg.call(tip);
// To make the data input dynamically, we would do this: Create .tsv file (only in this example)
// Then change the header to read this specific .tsv file
// r2d3.onRender = To read the data into r2d3.
r2d3.onRender(function(data, s, w, h, options) {
x.domain(data.map(function(d) { return d.DimensionName; }));
y.domain([0, d3.max(data, function(d) { return d.Amount;})]);
//d3.count(data, d => d.amount);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Occurence");
svg.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.Amount); })
.attr("height", function(d) { return height - y(d.Amount); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
// .remove()
;
/* svg.select(".bar")
.transition()
.duration(500)
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", function(d) { return y(d.Amount); })
.attr("y")
.remove(); */
// svg.exit().remove();
/* svg.transition()
.data(data)
.duration(500)
.attr("class", "bar")
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.Amount); })
.attr("height", function(d) { return height - y(d.Amount); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
svg.exit().remove() */
});
function type(d) {
d.Amount = +d.Amount;
return d;
}
.R File (I'll only include how I call the plot in the UI and in the server, because the code is quite lengthy:
if (interactive()) {
ui <- [Code about navigation bar etc.]
body = argonDashBody(
argonTabItems(
argonTabItem(
tabName = "subcluster_analysis",
fluidRow(
column(width=4,
#FM
argonTable(
cardWrap = FALSE,
#title = "Barplots",
headTitles = "D3JS Barplot - Cluster Per Feature",
shinycssloaders::withSpinner(
# verbatimTextOutput("summary"),
d3Output(outputId = "out_barplotD3_clusterPerFeature_NEU")),
)
),
[...rest of the body...]
#### Server initiation ---------------------------------
server <- function(input, output, session){
[Server output]
#Barplot in D3.Js
output$out_barplotD3_clusterPerFeature_NEU <- renderD3({
r2d3(data = cleanedSubClusteringData_barplot(),
script= "Histogram.js",
d3_version = "4",
container = "div")
})
}
shinyApp(ui,server)
}
Data:
https://drive.google.com/drive/folders/1BXQPgEBYax0mwB8eTA5Uf-PuYs27C_kD?usp=sharing
I'm grateful for any advice! :)

Javascript D3 not showing visualization

I am trying to replicate the visualization I see on this github profile
https://github.com/mojoaxel/d3-sunburst/tree/master/examples
I copied the following code and changed the path to the respective files.
suburst.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sequences sunburst</title>
<link rel="stylesheet" type="text/css" href="sunburst.css"/>
<link rel="stylesheet" type="text/css" href="examples.css"/>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="sunburst.js" type="text/javascript"></script>
</head>
<body>
<div id="main">
<div id="sunburst-breadcrumbs"></div>
<div id="sunburst-chart">
<div id="sunburst-description"></div>
</div>
</div>
<div id="sidebar">
<input type="checkbox" id="togglelegend"> Legend<br/>
<div id="sunburst-legend" style="visibility: hidden;"></div>
</div>
<script type="text/javascript">
(function() {
var sunburst = new Sunburst({
colors: {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}
});
sunburst.loadCsv("/Users/Documents/data/visit-sequences.csv");
d3.select("#togglelegend").on("click", function() {
var legend = d3.select('#sunburst-legend');
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
});
})();
</script>
</body>
</html>
sunburst.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['d3'], factory);
} else {
root.Sunburst = factory(root.d3);
}
}(this, function (d3) {
var defaultOptions = {
// DOM Selectors
selectors: {
breadcrumbs: '#sunburst-breadcrumbs',
chart: '#sunburst-chart',
description: '#sunburst-description',
legend: '#sunburst-legend'
},
// Dimensions of sunburst.
width: 750,
height: 600,
// Mapping of step names to colors.
colors: {},
// If a color-name is missing this color-scale is used
colorScale: d3.scale.category20(),
colorScaleLength: 20,
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
breadcrumbs: {
w: 75,
h: 30,
s: 3,
t: 10
},
// parser settings
separator: '-'
};
/**
* This hashing function returns a number between 0 and 4294967295 (inclusive) from the given string.
* #see https://github.com/darkskyapp/string-hash
* #param {String} str
*/
function hash(str) {
var hash = 5381;
var i = str.length;
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
return hash >>> 0;
}
var Sunburst = function(options, data) {
this.opt = Object.assign({}, defaultOptions, options);
// Total size of all segments; we set this later, after loading the data.
this.totalSize = 0;
if (data) {
this.setData(data);
}
}
Sunburst.prototype.getColorByName = function(name) {
return this.opt.colors[name] || this.opt.colorScale(hash(name) % this.opt.colorScaleLength);
}
Sunburst.prototype.setData = function(data) {
var json = this.buildHierarchy(data);
this.createVisualization(json);
}
Sunburst.prototype.loadCsv = function(csvFile) {
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text(csvFile, function(text) {
var array = d3.csv.parseRows(text);
var json = this.buildHierarchy(array);
this.createVisualization(json);
}.bind(this));
}
// Main function to draw and set up the visualization, once we have the data.
Sunburst.prototype.createVisualization = function(json) {
var that = this;
var radius = Math.min(this.opt.width, this.opt.height) / 2
this.vis = d3.select(this.opt.selectors.chart).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", this.opt.height)
.append("svg:g")
.attr("id", "sunburst-container")
.attr("transform", "translate(" + this.opt.width / 2 + "," + this.opt.height / 2 + ")");
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); });
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
// Basic setup of page elements.
this.initializeBreadcrumbTrail();
this.drawLegend();
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var all = this.vis.data([json])
.selectAll("path")
.data(nodes)
.enter();
all.append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return that.getColorByName(d.name); })
.style("opacity", 1)
.on("mouseover", that.mouseover.bind(this));
// some tests with text
/*
var arcText = 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 * 0.4); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy * 0.4); })
var arcsText = arcs.append("svg:path")
.attr("d", arcText)
.style("fill", "none")
.attr("id", function(d, i){
return "s" + i;
});
var texts = all.append("svg:text")
.attr("dx", "0")
.attr("dy", "0")
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href", function(d, i){
return "#s" + i;
})
.attr("startOffset",function(d,i){return "25%";})
.text(function (d) {
return d.depth === 1 ? d.name : '';
});
*/
// Add the mouseleave handler to the bounding circle.
d3.select(this.opt.selectors.chart).on("mouseleave", that.mouseleave.bind(this));
// Get total size of the tree = value of root node from partition.
var node = all.node();
this.totalSize = node ? node.__data__.value : 0;
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
Sunburst.prototype.mouseover = function(d) {
var percentage = (100 * d.value / this.totalSize).toPrecision(3);
var sequenceArray = this.getAncestors(d);
this.updateDescription(sequenceArray, d.value, percentage)
this.updateBreadcrumbs(sequenceArray, d.value, percentage);
// Fade all the segments.
this.vis.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
this.vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
Sunburst.prototype.mouseleave = function(d) {
var that = this;
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
//TODO cancel this transition on mouseover
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", that.mouseover.bind(that));
});
d3.select(this.opt.selectors.description)
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
Sunburst.prototype.getAncestors = function(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
Sunburst.prototype.initializeBreadcrumbTrail = function() {
// Add the svg area.
var trail = d3.select(this.opt.selectors.breadcrumbs).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
Sunburst.prototype.breadcrumbPoints = function(d, i) {
var points = [];
var b = this.opt.breadcrumbs;
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// format the description string in the middle of the chart
Sunburst.prototype.formatDescription = function(sequence, value, percentage) {
return percentage < 0.1 ? "< 0.1%" : percentage + '%';
}
Sunburst.prototype.updateDescription = function(sequence, value, percentage) {
d3.select(this.opt.selectors.description)
.html(this.formatDescription(sequence, value, percentage))
.style("visibility", "");
}
// format the text at the end of the breadcrumbs
Sunburst.prototype.formatBreadcrumbText = function(sequence, value, percentage) {
return value + " (" + (percentage < 0.1 ? "< 0.1%" : percentage + "%") + ")";
}
// Update the breadcrumb trail to show the current sequence and percentage.
Sunburst.prototype.updateBreadcrumbs = function(sequence, value, percentage) {
var that = this;
var b = this.opt.breadcrumbs;
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(sequence, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", this.breadcrumbPoints.bind(that))
.style("fill", function(d) { return that.getColorByName(d.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (sequence.length + 1) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.html(this.formatBreadcrumbText(sequence, value, percentage));
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
Sunburst.prototype.drawLegend = function() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select(this.opt.selectors.legend).append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(this.opt.colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(this.opt.colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
Sunburst.prototype.buildHierarchy = function(array) {
var root = {"name": "root", "children": []};
for (var i = 0; i < array.length; i++) {
var sequence = array[i][0];
var size = +array[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split(this.opt.separator);
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"] || [];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
}
return Sunburst;
}));
All the html, js and css files are stored in same folder.
But when I run it, I get the browser window like this with no visualization but just a legend box on the right.
What am I missing? The dataset is downloaded from the same github page.
Provided the path to visit-sequences.csv is correct, its local loading will be blocked by the browser (see CORS).
You can either:
run it via a (even local) webserver
start Chrome with the --allow-file-access-from-files flag (if you're using Chrome)
The problem is here
sunburst.loadCsv("/Users/i854319/Documents/Mike_UX_web/data/visit-sequences.csv");
Your browser cannot just load a file from your PC.

d3.js updating text only works when taking away text not when adding dynamically

The following is my update function for a heatmap that monitors different databases on different servers. SchemaTables is a combined variable of the schema it is referencing and the table within the schema it's referencing. The function is within a dictionary named heatmap which contains other functions as well. For some reason when taking away rows, the y-value axis fixes themselves accordingly, however when adding rows a couple of errors pop up as explained in the following code and comments:
update: function(usingFilter=false, schemaTableFilter=0) {
// get functions get most recent data
var data = getData(),
servers = getServers(),
schemaTables = getSchemaTables(),
//constants
gridSize = {height: 35, width: 140},
colors = ["#E50000", "#E45500", "#E3A900", "#C7E200", "#72E100", "#1DE00D", "#00E036"],
width = 460 + (gridSize.width * columns),
height = 400 + (gridSize.height * rows),
columns = servers.length,
rows = schemaTables.length;
//Fit HTML
$("html").height(height);
$("html").width(width);
$("svg").height(height);
$("svg").width(width);
var svg = d3.select("g");
//User has the option to use a filter, if a filter is
//used then weed out whatever shouldn't be shown.
if (usingFilter) {
schemaTables = schemaTableFilter;
data = getFilteredData($("#filters").val());
}
//x-axis labels - the amount of servers doesn't really change
//so as far as I know this works, but perhaps not
var serverLabels = svg.selectAll(".serverLabel")
.data(servers)
.text(function (d) { return d; })
.exit().remove()
.enter().append("text")
.attr("x", function(d, i) { return i * gridSize.width})
.attr("y", 0)
.attr("transform", "translate(" + gridSize.width / 2 + ", -6)")
.attr("class", "serverLabel mono")
//y-axis labels - where I'm having trouble
//It works when taking away rows but not when adding
var schemaTableLabels = svg.selectAll(".schemaTableLabel")
console.log(schemaTableLabels);
schemaTableLabels.data(schemaTables).text(function (d) { return d; })
.exit().remove()
.enter().append("text")
.attr("x", 50)
.attr("y", function (d, i) { return i * gridSize.height; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize.height / 1.5 + ")")
.attr("class", "schemaTableLabel mono")
//If removing rows no error occurs, if adding rows then
//I get an Uncaught RangeError: Invalid array length
var gridAmount = servers.length * schemaTables.length,
coordinates = Array(gridAmount).join(".").split(".");
//This for loop puts the data at the correct x, y coordinates
//within a list
for (var i = 0; i < data.length; i++) {
var serverIndex = servers.indexOf(data[i].server);
var schemaTableIndex = schemaTables.indexOf(data[i].schemaTable);
var index = (schemaTables.length * serverIndex) + (schemaTableIndex % schemaTables.length)
var currentData = data[i];
coordinates[index] = currentData;
}
//This for loop makes sure each index has an x, y
//coordinate to follow
for (var j = 0; j < coordinates.length; j++) {
currentGrid = coordinates[j];
if (currentGrid == "") {
currentGrid = {};
}
var xvalue = Math.floor(j / schemaTables.length);
var yvalue = Math.floor(j % schemaTables.length)
currentGrid["x"] = xvalue;
currentGrid["y"] = yvalue;
coordinates[j] = currentGrid;
}
//updates each rectangle for the heatmap
var grid = svg.selectAll("rect")
.data(coordinates, function(d) { return d; })
//remove old grids and add new ones, this function works
//when removing and adding grids
grid.exit().remove();
grid.enter().append("rect")
.attr("x", function(d, i) { return d.x * gridSize.width })
.attr("y", function(d, i) { return d.y * gridSize.height})
.attr("rx", 8)
.attr("ry", 8)
.attr("transform", "translate(50, 10)")
.attr("class", "server bordered")
.attr("width", gridSize.width)
.attr("height", gridSize.height)
.transition().duration(0)
.style("fill", function(d, i) {
var medianRefresh;
var color;
if (!d.hasOwnProperty("data")) {
medianRefresh = undefined;
}
else {
medianRefresh = d.data.medianrefresh;
}
if (medianRefresh == 0) {
color = colors[6];
}
else if (medianRefresh == null || medianRefresh == undefined) {
color = "white"
}
else if (medianRefresh > 0 && medianRefresh <= 300) {
color = colors[5];
}
else if (medianRefresh > 300 && medianRefresh <= 600) {
color = colors[4];
}
else if (medianRefresh > 600 && medianRefresh <= 900) {
color = colors[3];
}
else if (medianRefresh > 900 && medianRefresh <= 400) {
color = colors[2];
}
else if (medianRefresh > 400 && medianRefresh <= 500) {
color = colors[1];
}
else if (medianRefresh > 500) {
color = colors[0];
}
return color;
})
//Update titles within rectangles
svg.select("rect").selectAll("title").remove();
var title = svg.selectAll("rect")
.append("title")
.text(function(d) {
var medianRefresh,
server,
schemaTable,
schema,
table;
if (!d.hasOwnProperty("data")) {
medianRefresh = undefined;
server = servers[d.x];
schema = schemaTables[d.y].split(".")[0];
table = schemaTables[d.y].split(".")[1];
}
else {
medianRefresh = d.data.medianrefresh;
server = d.server;
schemaTable = d.schemaTable;
schema = schemaTable.split(".")[0];
table = schemaTable.split(".")[1];
}
var result = "Median Refresh: " + medianRefresh + "\nServer: " + server + "\nSchema: " + schema + "\nTable: " + table;
return result
})
I'm new to d3.js so any tips would be helpful. Here is the code where I call the update function:
$("#filters").on("select2:select", function() {
var filter = $("#filters option:selected").text();
if (filter != "") {
var filterInfo = getFilterInfo(filter)
filter = true;
heatmap.update(filter, filterInfo)
}
else {
heatmap.update();
}
})
QUESTION UPDATE:
I fixed my problem here's the fixed code:
//x-axis labels
var serverLabels = svg.selectAll(".serverLabel")
.data(servers)
serverLabels.enter().append("text")
.attr("class", "serverLabel mono")
.attr("x", function(d, i) { return i * gridSize.width; }).attr("y", 0)
.attr("transform", "translate(" + gridSize.width / 2 + ", -6)")
.attr("class", "serverLabel mono")
.merge(serverLabels)
.text(function (d) { return d; })
serverLabels.exit().remove();
//y-axis labels
var schemaTableLabels = svg.selectAll(".schemaTableLabel")
.data(schemaTables);
schemaTableLabels.enter().append("text")
.attr("class", "schemaLabel mono")
.attr("x", 50).attr("y", function(d, i) { return i * gridSize.height; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize.height / 1.5 + ")")
.attr("class", "schemaTableLabel mono")
.merge(schemaTableLabels)
.text(function(d) { return d; })
schemaTableLabels.exit().remove();

Don't rotate nodes in radial tree layout in d3.js

Fiddle Example
I can't figure out how to tweak the transform:rotate attribute for the nodes so that the pictures and text in the foreign objects don't rotate/ go upside down. I have tried tweaking with this block of code:
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
In the Chrome Console, I change a node from
<g transform="translate(226.5247584249853,-164.57987064189248)rotate(-36)">
<foreignObject.....></foreignObject></g>
to
transform="translate(226.5247584249853,-164.57987064189248)rotate(0)
and it works. But changing rotate to 0 in the code above would make every children node go to the left side., like this
Full code:
treeData = myJSON;
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var width = $(document).width();
var height = $(document).height();
var diameter = 800;
var tree = d3.layout.tree().size([360, diameter / 2 - 120])
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 10) / a.depth;
});
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal.radial()
.projection(function (d) {
return [d.y, d.x / 180 * Math.PI];
});
// Define the root
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function (d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function (d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// sort the tree according to the node names
function sortTree() {
tree.sort(function (a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function () {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([1, 1]).on("zoom", zoom);
function initiateDrag(d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function (a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
}).filter(function (d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function (d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
}
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "overlay")
.call(zoomListener);
// Define the drag listeners for drag/drop behaviour of nodes.
dragListener = d3.behavior.drag()
.on("dragstart", function (d) {
if (d == root) {
return;
}
dragStarted = true;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
// it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
})
.on("drag", function (d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
// get coords of mouseEvent relative to svg container to allow for panning
relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(this, 'left');
} else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(this, 'right');
} else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(this, 'up');
} else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(this, 'down');
} else {
try {
clearTimeout(panTimer);
} catch (e) {
}
}
d.x0 = d3.event.x;
d.y0 = d3.event.y;
var node = d3.select(this);
node.attr("transform", "translate(" + d.x0 + "," + (d.y0) + ")");
updateTempConnector();
})
.on("dragend", function (d) {
if (d == root) {
return;
}
domNode = this;
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
//centerNode(draggingNode);
draggingNode = null;
}
}
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function (d) {
console.log(d);
selectedNode = d;
updateTempConnector();
};
var outCircle = function (d) {
selectedNode = null;
updateTempConnector();
};
// Function to update the temporary connector indicating dragging affiliation
var updateTempConnector = function () {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
x: $('svg g').first().offset().left + selectedNode.position.left,
y: selectedNode.position.top
},
target: {
x: draggingNode.x0,
y: draggingNode.y0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal.radial())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal.radial());
link.exit().remove();
};
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.x0;
y = -source.y0;
x = x * scale + width / 2;
y = y * scale + height / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
//centerNode(d);
//dofocus([{ name : 'o_id' , value : d.o_id }]);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function (level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function (d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
//var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
// tree = tree.size([newHeight, width]);
// Compute the new tree layout.
var nodes = tree.nodes(root); //.reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
// nodes.forEach(function(d) {
// d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// // alternatively to keep a fixed scale one can set a fixed depth per level
// // Normalize for fixed-depth by commenting out below line
// // d.y = (d.depth * 500); //500px per level.
// });
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr("class", "node")
// .attr("transform", function(d) {
// return "translate(" + source.y0 + "," + source.x0 + ")";
// })
.on('click', click)
nodeEnter.append("foreignObject")
.attr("class", "smallcircle")
.attr("width", function (d) {
var f = document.createElement("span");
f.id = "hiddenText";
f.style.display = 'hidden';
f.style.padding = '0px';
f.innerHTML = d.name;
document.body.appendChild(f);
textWidth = f.offsetWidth;
var f1 = document.getElementById('hiddenText');
f1.parentNode.removeChild(f1);
return textWidth + 50;
})
.attr("overflow", "visible")
.attr("height", 50)
.attr("y", - 50 / 2)
.attr("x", - 50)
.append("xhtml:div").attr("class", "mainDiv")
.html(function (d) {
var htmlString = "";
htmlString += "<div class='userImage' style='border-color:red '><img src='https://www.gravatar.com/avatar/6d2db975d856b8799a6198bab4777aed?s=32&d=identicon&r=PG' width='50' height='50'></div>";
htmlString += "<div class='content' style='color:red;'>" + d.name + "</div>";
htmlString += "<div style='clear:both;'></div>";
return htmlString;
})
nodeEnter.append("text")
.text(function (d) {
return d.name;
})
.style("font", "8px serif")
.style("opacity", 0.9)
.style("fill-opacity", 0);
// phantom node to give us mouseover in a radius around it
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2) // change this to zero to hide the target area
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function (node) {
node.position = $(this).position();
node.offset = $(this).offset();
overCircle(node);
})
.on("mouseout", function (node) {
outCircle(node);
});
// Update the text to reflect whether node has children or not.
// node.select('text')
// .attr("x", function(d) {
// return d.children || d._children ? -10 : 10;
// })
// .attr("text-anchor", function(d) {
// return d.children || d._children ? "end" : "start";
// })
// .text(function(d) {
// return d.name;
// });
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Fade the text in
// nodeUpdate.select("text")
// .style("fill-opacity", 1);
nodeUpdate.select("text")
.style("fill-opacity", 1)
// .attr("transform", function(d) { return d.x < 180 ? "translate(0)" : "rotate(180)translate(-" + (d.name.length + 50) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function (d) {
return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)";
});
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g").attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
// Collapse all children of roots children before rendering.
root.children.forEach(function (child) {
collapse(child);
});
// Layout the tree initially and center on the root node.
update(root);
d3.select(self.frameElement).style("height", width);
The rotation is set in line 1221 of your fiddle:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
The rotation here is required for the positioning, as you're rotating around the origin of the non-translated coordinate system. So simply removing the rotate(...) won't work. However, you can rotate the elements back after positioning:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")rotate(" + (-d.x + 90) + ")";
Complete fiddle here.

Categories