Javascript D3 not showing visualization - javascript

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.

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.

D3.js child map failure ? Any gurus spot my error?

First the code can be found here: working - almost - code
I've had a few pointers and learnt "quite a bit" along to way trying to get this to work.
Basically I'm building, or trying to build a hierachy tree for office staff with some basic functions.
Its all going pretty well except one last issue and no matter how I look at it or approach it, I cannot see why it isnt working.
Problem:
If you hold the mouse over a node, 4 little pop up menus appear - the green and the red add and remove nodes - this works.
At the top of the canvas is a "Save" button, which I'm trying to get to go through all the nodes giving their parent-to-child relationship - again this works until you add a node and then another node, it will not see the a child of a new node.
If anyone knows the way to refresh the "child map" that Im using in the snippit below, it would be much appreciated:
d3.selectAll('g.node')
.each(function(p) {
p.children.map(function(c) {
alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" +
p.id + ")")
});
});
I don't know if I got your problem right, maybe I'm totally wrong with my assumption but your data is seems fine to me. The example you linked will throw an error when clicking on save for those nodes without children on line 25:
p.children.map(function(c) {
alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" + p.id + ")")
});
Since in some cases p.children is undefined (there is none) the loop will break.
Since the application is working visually, there is no reason to think the data bindings aren't correct. Just to be sure, I wrote a slightly modified version of your "save" loop. Add some children and check the console.
Now, a little bit out of scope to your question but may save you some headaches the next time: d3 just maps your data to your elements. When creating, destroying and updating stuff, leave all the DOM manipulation to d3 and concentrate on your model.
var diameter = 1000;
var height = diameter - 150;
var n = {
"name": "A",
"id": 1,
"target": 0,
"children": [{
"name": "B",
"id": 2,
"target": 1,
"children": [{
"name": "Cr",
"id": 8,
"target": 2,
"children": [{
"name": "D",
"id": 7,
"target": 2
}, {
"name": "E",
"id": 9,
"target": 8
}, {
"name": "F",
"id": 10,
"target": 8
}]
}]
},
{
"name": "G",
"id": 3,
"target": 0
}, {
"name": "H",
"id": 4,
"target": 0
}, {
"name": "I",
"id": 5,
"target": 0
}, {
"name": "J",
"id": 6,
"target": 0
}
]
}
var tree = d3.layout.tree()
.size([260, diameter / 2 - 120])
.separation(function(a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth;
});
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) {
return [d.y, d.x / 180 * Math.PI];
});
var myZoom = d3.behavior.zoom()
.scaleExtent([.5, 10])
.on("zoom", zoom);
var container = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", height)
.style('border', '3px solid black')
.call(myZoom);
//I am centering my node here
var svg = container.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + height / 2 + ")");
myZoom.translate([diameter / 2, height / 2]);
var init = true;
function zoom() {
svg.attr("transform", "translate(" + (d3.event.translate[0]) + "," + (d3.event.translate[1]) + ")scale(" + d3.event.scale + ")");
}
var nodes = tree(n);
//make sure to set the parent x and y for all nodes
nodes.forEach(function(node) {
if (node.id == 1) {
node.px = node.x = 500;
node.py = node.y = 304;
} else {
node.px = node.parent.x;
node.py = node.parent.y;
}
});
// Build a array for borken tree case
var myCords = d3.range(50);
buildSingleTreeData();
var id = ++nodes.length;
function update(root) {
var node = svg.selectAll(".node");
var link = svg.selectAll(".link");
nodes = tree.nodes(root);
if (checkBrokenTree(root)) {
if (!root.children || root.children.length == 0) {
id = 2;
} else {
var returnId = resetIds(root, 1);
id = nodes.length + 1;
}
singleNodeBuild(nodes);
}
links = tree.links(nodes);
/*This is a data join on all nodes and links
if a node is added a link will also be added
they are based parsing of the root*/
node = node.data(nodes, function(d) {
return d.id;
});
link = link.data(links, function(d) {
return d.source.id + "-" + d.target.id;
});
var enterNodes = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
d.tx = (d.parent ? d.parent.x : d.px) - 90;
return "rotate(" + ((d.parent ? d.parent.x : d.px) - 90) +
")translate(" + d.py + ")";
})
enterNodes.append('g')
.attr('class', 'label')
.attr('transform', function(d) {
return 'rotate(' + -d.px + ')';
})
.append('text')
.attr("dx", "-1.6em")
.attr("dy", "2.5em")
.text(function(d) {
return d.name;
})
.call(make_editable, function(d) {
return d.name;
});
var circlesGroup = enterNodes.append('g')
.attr('class', 'circles');
var mainCircles = circlesGroup.append("circle")
.attr('class', 'main')
.attr("r", 9);
circlesGroup.append("circle")
.attr('class', 'delete')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'red')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'add')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'green')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'admin')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'blue')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'userid')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'yellow')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.on("mouseenter", function() {
var elem = this.__data__;
elem1 = d3.selectAll(".delete").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2 = d3.selectAll(".add").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem3 = d3.selectAll(".admin").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem4 = d3.selectAll(".userid").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2.transition()
.duration(duration)
.attr('cx', -20)
.attr('cy', 0)
.attr("r", 8);
elem1.transition()
.duration(duration)
.attr('cx', 20)
.attr('cy', 0)
.attr("r", 8);
elem3.transition()
.duration(duration)
.attr('cx', -10)
.attr('cy', -20)
.attr("r", 8);
elem4.transition()
.duration(duration)
.attr('cx', 10)
.attr('cy', -20)
.attr("r", 8);
});
circlesGroup.on("mouseleave", function() {
var elem = this.__data__;
elem1 = d3.selectAll(".delete").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2 = d3.selectAll(".add").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem3 = d3.selectAll(".admin").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem1.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem3.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem4.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
});
var linkEnter = link.enter()
.insert("path", '.node')
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: d.source.px,
y: d.source.py
};
return diagonal({
source: o,
target: o
});
});
// UserID node event handeler
node.select('.userid').on('click', function() {
var p = this.__data__;
alert(p.name + " Userid (" + p.username + ")" + "-->" + p.id + "<--" + p.children);
});
// Admin node event handeler
node.select('.admin').on('click', function() {
var p = this.__data__;
alert(p.name + " password is (" + p.pword + ")");
});
// Delete node event handeler
node.select('.delete').on('click', function() {
var p = this.__data__;
if (p.id != 1) {
removeNode(p);
var childArr = p.parent.children;
childArr = childArr.splice(childArr.indexOf(p), 1);
update(n);
}
function removeNode(p) {
if (!p.children) {
if (p.id) {
p.id = null;
}
return p;
} else {
for (var i = 0; i < p.children.length; i++) {
p.children[i].id == null;
removeNode(p.children[i]);
}
p.children = null;
return p;
}
}
node.exit().remove();
link.exit().remove();
// alertify.alert(p.name + " has left the building..");
});
// The add node even handeler
node.select('.add').on('click', function() {
var p = this.__data__;
var aId = id++;
var d = {
name: 'name' + aId
};
d.id = aId;
if (p.children) {
p.children.push(d);
//top node add
} else {
p.children = [d];
//child of child
}
d.px = p.x;
d.py = p.x;
d3.event.preventDefault();
update(n)
node.exit().remove();
link.exit().remove();
});
/* this is the update section of the graph and nodes will be updated to their current positions*/
var duration = 700;
node.transition()
.duration(duration)
.attr("transform", function(d) {
d.utx = (d.x - 90);
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
link.transition()
.duration(duration).attr("d", diagonal);
node.select('g')
.transition()
.duration(duration)
.attr('transform', function(d) {
return 'rotate(' + -d.utx + ')';
});
node.select('.circles').attr('transform', function(d) {
return 'rotate(' + -d.utx + ')';
});
node.exit().remove();
link.exit().remove();
}
update(n);
/** make a manual tree for when it is just
a linked list. For some reason the algorithm will break down
all nodes in tree only have one child.
*/
function buildSingleTreeData() {
myCords = d3.range(50);
var offset = 130;
myCords = myCords.map(function(n, i) {
return {
x: 90,
y: offset * i
};
});
}
/**
This function will build single node tree where every node
has 1 child. From testing this layout does not support
a layout for nodes tree less than size 3 so they must
be manually drawn. Also if evey node has one child then
the tree will also break down and as a result this fucntion
is there to manually build a singe tree up to 50 nodes
*/
function resetIds(aNode, aId) {
if (aNode.children) {
for (var i = 0; i < aNode.children.length; i++) {
aNode.children[i].id = ++aId;
resetIds(aNode.children[i], aId);
}
return aId;
}
}
/*
builds a liner tree D3 does not support this
and so it must be hard coded
*/
function singleNodeBuild(nodes) {
nodes.forEach(function(elem) {
var i = elem.id - 1;
elem.x = myCords[i].x;
elem.y = myCords[i].y;
});
}
/* D3 does not support operations
on where root nodes does not have atlest
2 children. this case need to be check for
and hard coded
*/
function checkBrokenTree(rootNode) {
var size = nodes.length;
var val = 0;
function recur(nod, i) {
if (nod.children) {
return recur(nod.children[0], i + 1);
} else {
return i + 1;
}
}
return recur(rootNode, val) == nodes.length;
}
/*
Credit https://gist.github.com/GerHobbelt/2653660
This funciton make a text node editable
*/
function make_editable(d, field) {
this
.on("mouseover", function() {
d3.select(this).style("fill", "red");
})
.on("mouseout", function() {
d3.select(this).style("fill", null);
})
.on("click", function(d) {
var p = this.parentNode;
//console.log(this, arguments);
// inject a HTML form to edit the content here...
// bug in the getBBox logic here, but don't know what I've done wrong here;
// anyhow, the coordinates are completely off & wrong. :-((
var xy = this.getBBox();
var p_xy = p.getBBox();
xy.x -= p_xy.x;
xy.y -= p_xy.y;
var el = d3.select(this);
var p_el = d3.select(p);
var frm = p_el.append("foreignObject");
var inp = frm
.attr("x", xy.x - 40)
.attr("y", xy.y + 40)
.attr("dx", "2em")
.attr("dy", "-3em")
.attr("width", 100)
.attr("height", 25)
.append("xhtml:form")
.append("input")
.attr("value", function() {
// nasty spot to place this call, but here we are sure that the <input> tag is available
// and is handily pointed at by 'this':
this.focus();
//console.log( d);
return d.name;
})
.attr({
maxlength: 16
})
.style({
width: "100px"
})
// make the form go away when you jump out (form looses focus) or hit ENTER:
.on("blur", function() {
//console.log("blur", this, arguments);
var txt = inp.node().value;
d.name = txt;
if (txt) {
el
.text(function(d) {
return d.name;
});
}
// Note to self: frm.remove() will remove the entire <g> group! Remember the D3 selection logic!
p_el.select("foreignObject").remove();
})
.on("keypress", function() {
// console.log("keypress", this, arguments);
// IE fix
if (!d3.event)
d3.event = window.event;
var e = d3.event;
if (e.keyCode == 13) {
if (typeof(e.cancelBubble) !== 'undefined') // IE
e.cancelBubble = true;
if (e.stopPropagation)
e.stopPropagation();
e.preventDefault();
var txt = inp.node().value;
if (txt) {
d.name = txt;
el
.text(function(d) {
return d.name;
});
}
// odd. Should work in Safari, but the debugger crashes on this instead.
// Anyway, it SHOULD be here and it doesn't hurt otherwise.
p_el.select("foreignObject").remove();
}
});
});
}
.node .main {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
h1 {
text-align: center;
}
.node .delete {
stroke-width: 1.5px;
}
.node {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
svg {
margin-left: auto;
margin-right: auto;
display: block;
}
text {
font: 10px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<html lang="en">
<head>
<meta charset="utf-8">
<title> Staff</title>
</head>
<h1> STAFF </h1>
<input type="button" id="button" value="Save" />
<body style="text-align:center">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
var onClick = function() {
d3.selectAll('g.node')
.each(function(p) {
if (p.children) {
console.log(`node ${p.name} has ${p.children.length} children: `, p.children.map(child => child.name));
[...p.children].forEach((child) => {
console.log(`${child.name} is child of ${child.parent.name}`);
});
} else {
console.log(`node ${p.name} has no children`);
}
console.log('----------------')
});
};
$('#button').click(onClick);
</script>
</body>
</head>

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();

D3 Tree update color of nodes dynamically

So I'm trying to create a tree in D3 (Adapted from here) to show a series of nodes that are a specific color according to their value. The problem is that I am getting new data at set intervals that may change these values. I want the tree to update the colors accordingly when it recieves new information. I've tried a number of different methods that result in the entire tree redrawing itself, flying onto the screen, and auto expanding every nodes children. The desired effect that I am looking for is for the color of updated nodes to change, while the tree respects the status of collapsed/uncollapsed nodes that the user doesn't see the whole tree reset itself. Is this possible?
Here's what i have so far:
// Get JSON data
var treeData = {
"name": "rootAlert",
"alert": "true",
"children": [{
"name": "Child1",
"alert": "true",
"children": [{
"name": "Child1-1",
"alert": "false"
}, {
"name": "Child1-2",
"alert": "false"
}, {
"name": "Child1-3",
"alert": "true"
}]
}, {
"name": "Child2",
"alert": "false",
"children": [{
"name": "Child2-1",
"alert": "false"
}, {
"name": "Child2-2",
"alert": "false"
}, {
"name": "Child2-3",
"alert": "false"
}]
}, {
"name": "Child3",
"alert": "false"
}]
}
// 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 viewerWidth = $(document).width();
var viewerHeight = $(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// 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);
}
}
}
function visit2(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;
});
// 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([0.1, 3]).on("zoom", zoom);
// 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.y0;
y = -source.x0;
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]);
}
function leftAlignNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = (x * scale) + 100;
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]);
}
// 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;
}
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
if (d._children != null) {
var isCollapsed = true
} else {
var isCollapsed = false;
}
d = toggleChildren(d);
update(d);
if (isCollapsed) {
leftAlignNode(d);
} else {
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 newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 5)); //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("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.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";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { // alert(d.alert);
//console.log(d.name + ' is ' + d.alert)
if (d.alert == 'true') //if alert == true
return "red";
else return d._children ? "green" : "green";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// 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");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
tree.nodes(root).forEach(function(n) {
toggle(n);
});
update(root);
leftAlignNode(root);
setInterval(function() {
//update the color of each node
}, 2000);
.node {
cursor: pointer;
}
.overlay {
background-color: #EEE;
}
.node circle {
fill: #fff;
stroke: gray;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.templink {
fill: none;
stroke: red;
stroke-width: 3px;
}
.ghostCircle.show {
display: block;
}
.ghostCircle,
.activeDrag .ghostCircle {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="tree-container"></div>
I've found a great example here
It does basically exactly what I want. However, instead of updating the tree when a selection is made, I want it to update automatically when new data is recieved. I'd also like to see it not in AngularJS. I've attempted to implement the same type of update function from that example, but mine still pos in from the top left, while the example is so smooth!
It may be best to re-render the visualization when your data pipeline is updated.
You'll have to save the state of your visualization, preserving all information that will be needed to re-render the visualization, so that your users are none the wiser.

Zoomable sunburst chart in R (SunburstR package)

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 !

Categories