I am using the d3.js
treemap
in a an application with backbone.js
. The treemap renders correctly with the first JSOn data, but subsequent calls with different JSON data do not cause the treemap to re-render.
My HTML looks like this:
<html>
<head>
<title>Jenkins analytics</title>
<!-- stylesheets -->
<link rel="stylesheet" href="css/spa.css" type="text/css"/>
<link rel="stylesheet" href="css/treemap.css" type="text/css"/>
</head>
<body>
<nav>
<form>
<fieldset>
<label for="chart">Chart:</label>
<select id="chart" name="chart">
<option value="treemap" selected>Treemap</option>
<option value="motion">MotionChart</option>
</select>
</fieldset>
</form>
<form>
<fieldset>
<label for="period">Period:</label>
<select id="period" name="period">
<option value="lastday" selected>Day</option>
<option value="lastweek">Week</option>
<option value="lastmonth">Month</option>
</select>
<label for="team">Team:</label>
<select id="team" name="team">
<option value="all" selected>all</option>
<option value="spg">spg</option>
<option value="beacon">beacon</option>
<option value="disco">disco</option>
</select>
<label for="status">Status:</label>
<select id="status" name="status">
<option value="" selected>all</option>
<option value="SUCCESS">success</option>
<option value="FAILURE">failure</option>
<option value="ABORTED">aborted</option>
</select>
</fieldset>
</form>
<form>
<fieldset>
<label for="duration">Duration</label>
<input id="duration" type="radio" name="mode" value="size" checked />
<label for="count">Count</label>
<input id="count" type="radio" name="mode" value="count" />
<label for="average">Average</label>
<input id="average" type="radio" name="mode" value="avg" />
</fieldset>
</form>
</nav>
<div id="container" />
<!-- Third party javascript -->
<script type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="script/underscore/underscore.js" charset="utf-8"></script>
<script type="text/javascript" src="script/backbone/backbone.js" charset="utf-8"></script>
<script type="text/javascript" src="script/d3/d3.v3.js" charset="utf-8"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<!-- Application javascript -->
<script type="text/javascript" src="script/module.js"></script>
<!-- Startup -->
<script>
var navview = new NavViewer();
</script>
</body>
</html>
script/module.js
looks like this:
var NavViewer = Backbone.View.extend({
el: 'nav',
events: {
"change #chart": "change_chart",
"change #period": "change_period",
"change #team": "change_team",
"change #status": "change_status"
},
initialize: function() {
console.log("NavViewer.initialize");
this.d3view = new D3Viewer();
this.active_view = this.d3view;
},
change_chart: function(e) {
console.log("NavViewer.change_chart");
},
change_period: function(e) {
var _period = $('#period').val();
console.log("NavViewer.change_period to " + _period);
this.active_view.load();
},
change_team: function(e) {
var _team = $('#team').val();
console.log("NavViewer.change_team to "+ _team);
this.active_view.load();
},
change_status: function(e) {
var _status = $('#status').val();
console.log("NavViewer.change_status to " + _status);
this.active_view.load();
}
});
var JenkinsViewer = Backbone.View.extend({
el: '#container',
server: "http://192.168.1.100:5000",
url_fragment: function() {
var _period = $('#period').val();
var _team = $('#team').val();
var _status = $('#status').val();
return "when=" + _period +
(_team == "all" ? "" : ("&" + "team=" + _team)) +
(_status == "" ? "" : ("&" + "status=" + _status));
}
});
var D3Viewer = JenkinsViewer.extend({
initialize: function() {
this.margin = {top: 8, right: 0, bottom: 0, left: 0};
this.width = 1200 - this.margin.left - this.margin.right;
this.height = 800 - this.margin.top - this.margin.bottom - 60;
this.container = d3.select(this.el);
this.color = d3.scale.category20c();
this.base_url = this.server + "/team_build";
this.treemap = d3.layout.treemap()
.size([this.width, this.height])
.sticky(true)
.value(function(d) { return d.size; });
this.position = function() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
};
/* style the container */
this.container
.style("position", "relative")
.style("width", this.width + "px")
.style("height", this.height + "px")
.style("left", this.margin.left + "px")
.style("top", this.margin.top + "px")
.style("border", "1px solid black");
/* tootlip is appended to container */
this.tooltip = this.container.append("div")
.attr('class', 'tooltip')
.style("visibility", "hidden")
.style("background-color", "#ffffff");
this.load();
},
load: function() {
var $container = this.container;
var color = this.color;
var treemap = this.treemap;
var position = this.position;
var tooltip = this.tooltip;
var url = this.base_url + "?" + this.url_fragment();
console.log("D3View.load: " + url);
d3.json(url, function(error, root) {
/* 'root' actually means the data retrieved by the xhr call */
var node = $container.datum(root)
.selectAll(".node")
.data(treemap.nodes);
node.enter().append("div")
.attr("class", "node")
.call(position)
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; })
.on("mouseover", function(d) {
tooltip.html(d.name + ": " + Math.floor(d.value))
.style("visibility", "visible");
this.style.cursor = "hand";
})
.on("mouseout", function(){
tooltip.style("visibility", "hidden");
});
d3.selectAll("input").on("change", function change() {
var functions = {
count: function(d) { return d.count; },
size: function(d) { return d.size; },
avg: function(d) { return d.size / d.count; }
};
var value = functions[this.value];
node
.data(treemap.value(value).nodes)
.transition()
.duration(1500)
.call(position);
});
});
return true;
}
});
Here are things I've done:
- read the
d3.js
code for d3.json and d3.layout.treemap - googled the living daylights out of this
- read Lars Kotthoff's d3.js answers on StackOverflow
- read some articles: Enter and Exit, D3.js: How to handle dynamic JSON Data
- tried
treemap.sticky(false)
- tried adding
node.exit().remove()
I think the problem might relate to stickiness or the absence of a call node.exit().remove()
. I've tried modifying both of those without success. However, to get the interactive clientside UI, I think I need to use treemap.sticky(true)
.
I've confirmed that I get different JSON each time I hit the REST API service. I've confirmed that container.datum().children
changes in size between calls, confirming for me that it is a question of treemap not re-rendering.
D3.js: How to handle dynamic JSON Data
looks highly relevant:
- "It's worth mentioning that if you already have data with the same key, d3.js will store the data in the node but will still use the original data."
- "When I started playing with d3.js, I thought that rendered data would be evented and react to my changing of scales, domains and whatnot. It's not. You have to tell d3.js to update the current stuff or it will stay there, unsynced."
- "I assign the result of data() to a variable because enter() only affects new data that are not bound to nodes."