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.
Related
I'm trying to implement drag events in d3 (v6.2), and I'm having some trouble interpreting the d3 x and y coordinates. Consider the following code. When I inspect the console output, it seems to me like, in the drag handler:
event.x and event.y are the SUM of the user (graph) coordinate object locations and the total movement/distance in SVG coordinates?
event.dx and event.dy are indicator that tell you whether since the last update, you've moved left/up (-1), not moved (0), or right/down (1)?
event.subject.x and event.subject.y give the location of the object being dragged?
if I want the current coordinates of the drag (either in user/graph coordinates or SVG coordinates), I need to calculate them myself (see example in code, which seems to work)?
I can't find where the specifics of this are documented. My questions are:
Are the above impressions correct?
If not correct, what's the best way to get the current drag coordinates?
If correct, why would one SUM the values from two different coordinate systems? That doesn't make sense to me.
<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
<style>
circle.circ {
fill: red;
stroke: black;
stroke-width: 1px;
}
</style>
<script>
function dragHandler(e, d){
const objx = scaleX.invert(e.x - d.x + scaleX(d.x))
const objy = scaleY.invert(e.y - d.y + scaleY(d.y))
console.log(`x: ${e.x}, dx: ${e.dx} sx: ${e.subject.x} objx: ${objx}\ny: ${e.y} dy: ${e.dy} sy: ${e.subject.y} objy: ${objy}`)
}
var drag = d3
.drag()
.on('drag', dragHandler)
var x = [{x: 150, y: 150}]
var svg = d3.select("svg")
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
}
var width = svg.attr("width") - margin.left - margin.right
var height = svg.attr("height") - margin.top - margin.bottom
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var scaleX = d3.scaleLinear().range([0, width]).domain([140, 160])
var scaleY = d3.scaleLinear().range([height, 0]).domain([140, 160])
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(scaleX));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(scaleY))
var circ = g.selectAll(".circ")
.data(x)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) { return scaleX(d.x) })
.attr("cy", function(d) { return scaleY(d.y) })
.attr("class", "circ")
.call(drag);
</script>
</body>
<html>
Your impression is right. You can either
compute the distance in local coordinates as the difference between event.x/y and event.subject.x/y
retrieve the screen distance with event.clickDistance() as a getter
These two work in different coordinate systems.
The client coordinate system is that of the browser viewport. They are just read from the native DOM mousemove event.
The user coordinate system is the one of the thing being dragged, using the same coordinates as those used when writing SVG markup. They are computed from the client coordinates with a transform matrix the SVG API provides. The source code is here.
What you did was the in-between way that normally is not needed: by applying the scaleX/Y transform, you computed the coordinates in the system of the SVG viewBox, i. e. the coordinate system that is found when the viewBox is applied to the outermost <svg> element. That can be the client coordinate system, but only if the attribute is not set, or the width and height values of the element match the contents of the viewBox.
In almost every practical use case, the two will differ. The "Scalable" in SVG is just about that distinction. Leaving out the viewBox like you did deprives you of the possibility to easily scale your grafic as a whole.
Adding to #ccprog's answer above ( that worked for me ). It is also useful to add .container(selection.node) to your drag object. This makes the coordinates work well. Pay attention not to pass the selection but the selection.node() to .container(...).
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
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 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).
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)");