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
Related
I am trying to make my d3 multi-series line chart responsive follwoing this reference and here is the full code of this reference.
I add the following resize function to the bottom of my code:
$(window).on("resize", function() {
//update width
var main_width = parseInt(d3.select('#myChart').style('width'), 10);
main_width = main_width - main_margin.left - main_margin.right;
//resize the chart
main_x.range([0, main_width]);
mini_x.range([0, main_width]);
d3.select('#myChart').append("svg")
.attr("width", main_width + main_margin.left + main_margin.right)
.attr("height", main_height + main_margin.top + main_margin.bottom);
svg.selectAll('defs.clipPath.rect')
.attr("width", main_width);
svg.selectAll('rect.overlay')
.attr("width", main_width);
}).trigger("resize");
But nothing is changed when I adjust the screen. No idea why, please advise!
Thanks a lot.
Just detect all svg elements and attributes that have something to do with width main_width, because responsive mean the width will be adjusted when screen resize.
here is the resize() function that works well:
//----------- responsive d3, resize functionality -----------
$(window).on("resize", function() {
//update width
main_width = parseInt(d3.select('#myChart').style('width'), 10);
main_width = main_width - main_margin.left - main_margin.right;
//resize main and min time axis range
main_x.range([0, main_width]);
mini_x.range([0, main_width]);
//pointpoint the 'rect' element about brush functionality and adjust its width
svg.selectAll('rect.brushrect').attr("width", main_width);
//pinpoint the 'rect' element about mousemove and adjust its width
svg.selectAll('rect.overlay').attr("width", main_width);
//update x axis
svg.selectAll("g.x.axis").transition().call(main_xAxis);
//update right y axis
svg.selectAll('g.y.axisRight').attr("transform", "translate(" + main_width + ", 0)").call(main_yAxisRight);
// update main line0 and line4
svg.selectAll("path.line.line0").datum(data).transition().attr("d", main_line0);
svg.selectAll("path.line.line4").datum(data).transition().attr("d", main_line4);
//update min line0 and line4
mini.selectAll("path.mini_line0").datum(data).transition().attr("d", mini_line0);
mini.selectAll("path.mini_line4").datum(data).transition().attr("d", mini_line4);
}).trigger("resize");
Two things to note
The rect attribute with respect to brushing, i.e,
svg.append("defs")
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", main_width)
.attr("height", main_height);
does not have a name, so the following class attribute should be added for pinpointing:
.attr('class', "brushrect")
The class for mini lines are changed to mini_line0 and mini_line4, some the codes have some tiny update related to that.
then all done.
I am trying to use an svg-clippath with d3.js and the zoom behaviour.
The following code creates a rectangle, which will then be clipped by a rectangualar clipping region.
<svg class="chart"></svg>
<script>
var width = 800;
var height = 600;
var svg = d3.select(".chart")
.attr("width", width)
.attr("height", height)
.append("g");
var clip = svg.append("defs")
.append("clipPath")
.attr("id","clip")
.append("rect")
.attr("width",200)
.attr("height",200)
.attr("x",100)
.attr("y",100);
var zoom = d3.behavior.zoom().
on("zoom",zoomed);
function zoomed(){
container.attr("transform", "translate(" + d3.event.translate
+")scale(" + d3.event.scale + ")");
container.attr("clip-path","url(#clip)");
}
svg.call(zoom);
var container = svg.append("g")
.attr("clip-path","url(#clip)");
var rect = container.append("rect")
//.attr("clip-path","url(#clip)")
.attr("class","bar")
.attr("x",150)
.attr("y",150)
.attr("width",350)
.attr("height",350);
</script>
What I want is for the clipping to be applied again after zooming / moving (so that I cannot
move the rectangle outh of the clipping region, which right now i can do without any problems.) How do I do that?
I am assuming that the current behaviour is caused by the fact that the clipping is applied before the transformation.
I had the same problem and spent the last couple of hours trying to figure out a solution. Apparently, the clip-path operates on the object prior to transformation. So I tried to reverse-transform the clip object when performing the zoom transformation, and this worked !
It is something in the spirit of:
var clip_orig_x = 100, clip_orig_y = 100;
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;
// standard zoom transformation:
container.attr("transform", "translate(" + t +")scale(" + s + ")");
// the trick: reverse transform the clip object!
clip.attr("transform", "scale(" + 1/s + ")")
.attr("x", clip_orig_x - t[0])
.attr("y", clip_orig_y - t[1]);
}
where clip is the rectangle in the clipPath. Because of interactions between zooming and translation, you need to set "x" and "y" explicitly instead of using transform.
I am sure experienced d3 programmers out there will come up with a better solution, but this works !
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.
Here's a JSFiddle: http://jsfiddle.net/8p2yc/ (A slightly modified example from here: http://bl.ocks.org/mbostock/3883245)
As you can see in the JSFiddle tick labels along the y axis do not fit in the svg. I know I can increase the left margin, but the thing is I don't know what the data will be in advance. If I just make the margin very large the chart will look awkward if the numbers are short in length.
Is there a way to precompute the maximum label width when creating the chart to set the margin correctly? Or perhaps there's an entirely different solution?
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 400 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var svg = d3.select("body").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 + ")");
Thanks!
You can do this by appending the text for the largest label, measuring it and removing it immediately afterwards:
var maxLabel = d3.max(data, function(d) { return d.close; }),
maxWidth;
svg.append("text").text(maxLabel)
.each(function() { maxWidth = this.getBBox().width; })
.remove();
Then you can use that width to do the translation of the g element:
svg.attr("transform", "translate(" + Math.max(margin.left, maxWidth) + "," + margin.top + ")");
Complete example here.
Edit: Getting the maximum length of the actual labels is a bit more involved because you have to generate them (with the right formatting) and measure them all. This is a better way to do it though as you're measuring what's actually displayed. The code is similar:
var maxWidth = 0;
svg.selectAll("text.foo").data(y.ticks())
.enter().append("text").text(function(d) { return y.tickFormat()(d); })
.each(function(d) {
maxWidth = Math.max(this.getBBox().width + yAxis.tickSize() + yAxis.tickPadding(), maxWidth);
})
.remove();
I'm adding the size of the tick line and the padding between tick line and label to the width here. Complete example of that here.
A chicken-and-egg problem prevents this from being precomputed reliably, assuming you want your axes and plot to combine to fill a fixed space, and your tick labels to be generated automatically.
The problem is that the tick labels, and therefore the space they require, depend on the scale. A time scale, for example, may include longer month names (e.g. 'September') over a short domain, but shorter month names or just years over a long domain. But the scale, at least on the opposite axis, depends on the space left for the range, which depends on the space left over after determining the tick labels.
In a lot of cases, the minimum and maximum values may give you an idea of the widest tick labels, and you can use Lars' method. For example, if the domain is –50 to 10, any tick labels between those will be narrower. But this assumes they're integers! Also, be careful with 'nice' scales; if your maximum value is 8, D3 may try to make the greatest tick label 10, which is a wider string.
d3 v4 Solution for Lars' Method.
calculateMarginForYScaleTicks() {
let maxWidth = 0;
let textFormatter = d3.format(",");
let tempYScale = d3
.scaleLinear()
.range([0, 0])
.domain([0, d3.max(data, d => d.value)]);
d3
.select("#svg")
.selectAll("text.foo")
.data(tempYScale.ticks())
.enter()
.append("text")
.text(d => textFormatter(d))
.each(function() {
maxWidth = Math.max(this.getBBox().width, maxWidth);
})
.remove();
return maxWidth;
}
I'm drawing line charts with d3 and it all works fine. However, I have to leave enough margin on the left of the chart area to fit whatever I think might be the widest y-axis text label. I'd like to adjust this space for each chart, depending on the widest label.
Initially I thought I could find the maximum y value, create a hidden text object, work out how wide that is, and use that value for the left margin when creating the chart. A bit nasty, but it gets me a value.
However, if the maximum y value is, say "1598.538" the top-most y-axis label might be "1500"... ie, a lot narrower.
So I guess I want to find the width of whatever will actually be the top-most label. But I can't think how to do that without drawing the chart and axis, measuring that width, and drawing it again for real. Which sounds nasty! Is there a non-nasty way to do this?
UPDATE
Here's part of my code, using Lars' suggestion, just to show where it fits in:
// I did have
// `.attr("transform", "translate(" + margin.left + "," + margin.top ")")`
// on the end of this line, but I've now moved that to the bottom.
var g = svg.select("g");
// Add line paths.
g.selectAll(".line").data(data)
.enter()
.append("path")
.attr("d", line);
// Update the previously-created axes.
g.select(".axis-x")
.attr("transform", "translate(0," + yScale.range()[0] + ")"))
.call(xAxis);
g.select(".axis-y")
.call(yAxis);
// Lars's suggestion for finding the maximum width of a y-axis label:
var maxw = 0;
d3.select(this).select('.axis-y').selectAll('text').each(function(){
if (this.getBBox().width > maxw) maxw = this.getBBox().width;
});
// Now update inner dimensions of the chart.
g.attr("transform", "translate(" + (maxw + margin.left) + "," + margin.top + ")");
You can put everything inside a g element and set transform based on the max width. Something along the lines of
var maxw = 0;
yAxisContainer.selectAll("text").each(function() {
if(this.getBBox().width > maxw) maxw = this.getBBox().width;
});
graphContainer.attr("transform", "translate(" + maxw + ",0)");