d3 Dynamically Updating Treemap - javascript

I'm trying to update elements in the treemap:
drawTreeMap = (data) ->
margin =
top: 0
right: 0
bottom: 0
left: 0
width = window.innerWidth
height = window.innerHeight
color = d3.scale.category20c()
treemap = d3.layout.treemap().size([width, height]).sticky(true).value((d) -> d.size)
div = d3.select("body").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px")
node = div.datum(data).selectAll(".node")
.data(treemap.nodes)
node.enter()
.append("div")
.attr("class", "node")
.call(position)
.style("background", (d) -> (if d.children then color(d.name) else null))
.on "click", ->
el = d3.select(this)
data = el[0][0].__data__
while (data.parent.parent)
data = data.parent
console.log("updated to data:")
console.log(data)
drawTreeMap(data)
#updateTreemap(div, treemap, data)
node.exit()
.remove()
node.transition().duration(1500).call position
data is what I want it to be in the console.log statement, but the treemap isn't getting updated. Most of the code is directly from the treemap example.

Like Lars points out, you're creating a new div (the one assigned to var div) every time you call drawTreeMap(). For starters, you need to either simply move that div creation outside of drawTreeMap so that it only runs once. Or, if you want to get fancy, you could leave the creation inside the function, but do something like this:
div = d3.select("body").selectAll("div.treemap-container").data([null])
div.enter()
.append('div')// only creates the div the first time this runs
.attr('class', 'treemap-container')
// etc...
That's the only odd thing I can see. Maybe there's another bug, but it's hard to track down without a working jsFiddle. If you can provide one, post a comment here and I'll take a look further.
As an aside, for style, rather than doing this: data = el[0][0].__data__, you should just do this: data = el.datum()
Finally, note that you're not using a key function for the data binding, so even if you get the treemap to re-render, there would be no object persistence (i.e. existing divs could get reassigned to a different data point arbitrarily).

Related

Bullet Chart Ticks in D3 - Boothead Example

I'm trying to have the chart tickets in a D3 bullet chart follow the data itself, as per the 2nd example here:
Bullet chart ticks & labels in D3.js
The issue is that the source of this (http://boothead.github.io/d3/ex/bullet.html) no longer exists on the internet, the only thing out there is the gif in this post that I've linked.
enter image description here
Does anyone have the original copy of this project or have any advice?
I'm using the first example by mbostock and trying to replicate the bottom one.
Many thanks
In the original bullet.js from the bostock example https://bl.ocks.org/mbostock/4061961
Instead of getting the ticks from the scale you get the values from the range, measure and mark
change around line 109
// var tickVals = x1.ticks(8);
var tickVals = rangez.concat(measurez).concat(markerz);
// Update the tick groups.
var tick = g.selectAll("g.tick")
.data(tickVals, function(d) {
return this.textContent || format(d);
});
Edit
There is a problem if you update the data based on a new fetch from the server. some of the ticks end up on the wrong location. If the number of markers,measures,ranges also change they also end up at the wrong location.
It depends on the selection you supply to the bullet call.
The confusion is caused by the poor naming of the main program.
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
The name suggests that svg is a selection of svg elements. This is incorrect, it is a selection of g elements.
The update() function should reflect this
function updateData() {
d3.json("mailboxes.json", function (error, data) {
d3.select("body")
.selectAll("svg")
.select('g')
.data(data)
.call(chart.duration(500));
});
}
If the number of bullet graphs changes on the update there is the problem that they are not created or deleted if needed. So we need to make a function that can be used for initial and update calls.
function drawCharts() {
d3.json("mailboxes.json", function (error, data) {
var svg = d3.select("body").selectAll("svg").data(data);
svg.exit().remove();
svg.enter().append("svg")
.attr("class", "bullet")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.select("body")
.selectAll("svg")
.select('g')
.data(data)
.call(chart.duration(500));
});
}
A better change in bullet.js [109] would be:
// var tickVals = x1.ticks(8);
var tickVals = [];
rangez.concat(measurez).concat(markerz).forEach(e => {
if (tickVals.indexOf(e) == -1) tickVals.push(e);
});
// Update the tick groups.
var tick = g.selectAll("g.tick").data(tickVals);
That is do not use the value of the tick to match, in case we have multiple values in the ticks, and remove the duplicates.
We also need to change the update of the ticks, about 30 lines down
tickUpdate.select("text")
.attr("y", height * 7 / 6);
to
tickUpdate.select("text")
.text(format)
.attr("y", height * 7 / 6);

Bootstrap get width of div column in pixels

I'm setting up a page with bootstrap. I have the layout working perfectly but one of the elements is a zoomable map of the US (using d3). The zoom function I am using requires the width and height of the div in pixels in order to calculate how to translate and scale the map. I have tried using percentages but I can't get anything going that way. Is there any way to dynamically get the height and width of the div. I have searched all over but the search terms are too generic (or I'm not clever enough to phrase it correctly).
Alternatively, how else might I get the necessary values.
Here is my implementation using hard coded width and height (which won't work if the page resizes).
//make the map element
var width = 1000;
var height = 1000;
var svg = d3.select("#Map")
.append("svg")
.attr("id", "chosvg")
.attr("height", height)
//.attr("viewBox", "0 0 600 600")
.attr("width", width)
.style("preserveAspectRatio", "true");
cbsa = svg.append("g");
d3.json("data/us-cbsa.json", function(json) {
cbsa.selectAll("path")
.attr("id", "cbsa")
.data(json.features)
.enter()
.append("path")
.attr("class", data ? quantize : null) //data ? value_if_true : value_if_false -- ternary operator
.attr("d", path)
.on("click", clicked);
});
in the clicked() function, I have the zoom like this which works, but only
with a certain window width
cbsa.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
For clarity, I am ideally loooking for something like:
var width = column.width() //or something using percentages
I can include my html as well if it helps.
You can get the width of the column by calling:
var bb = document.querySelector ('#Map')
.getBoundingClientRect(),
width = bb.right - bb.left;
Depending on the browser, the bb might already have an width property. Keep in mind that the column might appear wider because the initial size of the svg is too big, so its parent column might bee, too.
https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

Are there any alternatives to .append() in D3.js?

Is the .append() method the only way to add a D3 object to HTML?
I am using the append() method to add a treemap to the body of my HTML file, but I would like to add the treemap inside a element. The problem is that append adds after the current element, not inside the current element. I want to resize the treemap to expand to 100% of the initial height and width of the browser, like this. Adding CSS seems like the most straight-forward way.
I have tried to 'trick' D3.js by appending the treemap to a div within a div, but that didn't work.
Perhaps I am asking the wrong question, and I should resize the treemap within the d3.js script? I have tried to change the default styles provided by bl.ock.org, but I haven't gotten the treemap to expand to the full screen.
Here is the code that I think initializes the treemap.
var margin = {top: 40, right: 10, bottom: 10, left: 10},
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var color = d3.scale.category20c();
var treemap = d3.layout.treemap()
.size([width, height])
.sticky(false)
.value(function(d) { return d.size; });
var div = d3.select("body").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
Here is the code that changes the size of the node to accommodate the javascript values.
var root;
socket.on('update', function(stream) {
console.log('tweets')
div.datum(root).selectAll(".node").remove();
root = stream.masterlist;
var node = div.datum(root).selectAll(".node")
.data(treemap.nodes)
.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; });
});
});
function position() {
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"; });
}
I would approach the problem a little differently.
I would use the viewBox attribute to keep the coordinate system of the visualization constant. Personally, I like <svg viewBox="-100 -100 200 200"> meaning that (-100, -100) is the top-left and (100, 100) is the bottom-right corner. You can choose 0 0 100 100 as well.
Then I will not set any width and height on the svg element explicitly, but instead rely on CSS to give them the correct dimensions (say display: block; width: 80%; margin: auto; height: 80%) and then control how the visualization scales using preserveAspectRatio attribute. Usually, I find that the defaults (xMidYMid and meet) are sufficient for me. However, your mileage might vary.
This is an example of using this approach to resize the visualisation when the size of the container changes without redrawing the visualisation: http://jsfiddle.net/ss47m/
Here: http://jsfiddle.net/ss47m/1/ size of the container is made to cover the whole window.
An alternate approach is binding to the resize event on window (like nvd3 does with windowResize), and redraw your chart each time the window changes size. I find that a bit excessive.
First, to clarify:
The problem is that append adds after the current element, not inside the current element.
That's not correct. If you do d3.select("div.parent").append("div") the new div will be inside the parent div, but after any child content already within the parent. To make the new element the first child of the parent, you would do d3.select("div").insert("div", function(){return this.children[0];}). But I don't think that's really your problem here...
For the question of how to calculate a treemap so that it automatically fills the browser window:
If you just want the map to fill the initial size of the window, and not worry about resizing, you need to (a) find out the window size; (b) set your container div's height and width to that size; and (c) use those dimensions as the input size to the treemap function.
Basically, just change these lines:
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
to
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
and then the rest of your code is the same.
If you also want the treemap to scale with the window, you should set the position and size of the child elements using percentage values. That will be easiest if you initialize the treemap using [100,100] as the size, and then you can simply replace all the "px" in your position function with "%".
However, you'll still need some CSS to make your container div fill up the full size of the browser window and resize with it. How tricky that is depends on how much else you have going on in your page layout. You might also want to check out this answer. If you can't get the CSS working, you can always listen for window resize events and reset the container's width and height based on window size. If the children are all sized base on percentages then they will adjust to whatever size you set for the parent.
Finally, you'll probably want to set min-height and min-width properties on the container to guarantee that your treemap elements will always be a decent size even if it means scrolling.

D3 zoom behavior - mouseup event not firing

I'm trying to have D3 pan & zoom as in this example:
It works except for releasing the zoom/pan on mouseup() - once I click, there's no way to stop panning without reloading the page. Here's the piece that calls the zoom function, which in turn is identical to the example:
var svg = d3.select("#chart").append("svg")
.attr("id", "scatter")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 10]).on("zoom", zoom))
I've been looking through the D3 source code and mouseup() never gets called, in contrast to mousedown() and mousemove() which respond to the respective events:
function mousedown() {
var target = this,
event_ = event.of(target, arguments),
eventTarget = d3.event.target,
moved = 0,
w = d3.select(d3_window).on("mousemove.zoom", mousemove).on("mouseup.zoom", mouseup),
l = location(d3.mouse(target)),
selectEnable = d3_event_userSelectSuppress("zoom");
function mousemove() {
moved = 1;
translateTo(d3.mouse(target), l);
dispatch(event_);
}
function mouseup() {
if (moved) d3_eventCancel();
w.on("mousemove.zoom", null).on("mouseup.zoom", null);
selectEnable();
if (moved && d3.event.target === eventTarget) d3_eventSuppress(w, "click.zoom");
}
}
I'm using D3.v3, whereas the example uses D3.vs2, so perhaps the code changed - but I haven't been able to find any useful hint. I'm new to javascript, so this may of course be a different issue altogether.

creating multi-line charts as a reusable component

I think I'm almost there. I have a working version for a single line that updates dynamically, but am having trouble getting to the multi-line part. I believe it has something to do with the way the data is being filtered at _selection.each. Not sure the best way to proceed from here though. The example found here (Drawing Multiple Lines in D3.js) seems to deal with this without much work.
Here is the jsfiddle for this, but the code is represented below as well:
http://jsfiddle.net/seoulbrother/NhK43/
Thanks in advance. Also, would love to hear best practices regarding this as well.
So if I have a page with a matrix where each row represents a time-series:
<html>
<body>
<div id="container"><div id="viz"></div></div>
</body>
</html>
<script type="text/javascript">
var matrix = [
[23,12,44,22,12,33,14,76,45,55,66,55],
[33,22,11,88,32,63,13,36,35,51,26,25]
];
</script>
I call this using:
mult_line = d3.graph.line(500, 250, "#viz");
d3.select("#container").datum(matrix).call(mult_line);
where d3.graph.line is:
d3.graph.line = function module(w, h, id) {
var svg;
var margin = {top: 10, right: 20, bottom: 20, left: 40},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
function chart(_selection) {
_selection.each(function(_data) {
console.log(_data);
var x = d3.scale.linear().domain([1, _data.length]).range([0, width]);
var y = d3.scale.linear().domain([0, 100]).range([height, 0]);
var xAxis = d3.svg.axis().scale(x).ticks(5).orient("bottom");
var yAxis = d3.svg.axis().scale(y).ticks(5).orient("left");
var line = d3.svg.line()
.x(function(d,i) { return x(i+1); })
.y(function(d) { return y(d); });
if (!svg){
svg = d3.select(id).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.append("path")
.attr("class","line")
.attr("d", line(_data));
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.call(yAxis);
}
var line_m = d3.selectAll("path.line")
.data(_data);
line_m.transition()
.duration(1000)
.attr('d', line(_data));
line_m.enter().append("path")
.attr("d",line(_data));
update_axis();
function update_axis() {
d3.select(".x-axis").call(xAxis);
}
});
}
return chart;
}
I think there may be varying opinions about this, but I wouldn't use selection.datum() as the mechanism of passing data into the chart component. Instead, I would add a .data() setter method as a property of the multiline component. I would do the same for width, height, etc. This is the way d3's components are implemented (for example, check out the source code for the d3.svg.axis). Then your chart creation would look like this:
mult_line = d3.graph.line()
.width(500)
.height(250)
.id("#viz")
.data(matrix);
d3.select("#container").call(mult_line);
You can refine this so that there are 2 data-setting methods of multiline: .data() and .multilineData(). The first would expect data for a single line (an array) and the 2nd would expect an array of arrays (multiline). Internally, they'd always be represented as an array of arrays. That way, the chart drawing code always expects to draw multiline data, except sometimes the data has just one line.

Categories

Resources