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)");
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 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);
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
again I am struggling with d3.js. I have a working Line Chart and partially working mouseover. The goal is to limit the mouseover solely to the svg element, like Mark has it working in his answer Multiseries line chart with mouseover tooltip
I have created a Plunker with it. My is-situation is like that.
http://plnkr.co/edit/Jt5jZhnPQy4VpjwY3YBv?p=preview
And I have tried things like:
http://plnkr.co/edit/lRMfa0OiDWEXWYBAjoPd?p=preview
by adding:
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
But it's always pushing the circles and the bar out of the chart, I am fiddling for some days now and would be extremely glad if someone happens to point me in the right direction.
Thank you in advance.
Here is the plunker:
http://plnkr.co/edit/MEtbBqN5qr82yr0CNhUN?p=preview
I simply changed the size of your rectangle:
mouseG.append('rect')
.attr("x", margin.left)
.attr("y", margin.top)
.attr('width', w - margin.left - margin.right)
.attr('height', height - margin.bottom - margin.top)
PS: I don't know if you want the line limited to the chart area as well, but if you want, this is the plunker: http://plnkr.co/edit/RP4uYKBYnHtX1SvYsLKq?p=preview
Instead of giving width as width :
mouseG.append('svg:rect')
.attr('width', width)
do this (give the width of the group same as domain x for the line chart)
mouseG.append('svg:rect')
.attr('width', w - padding * 2)
Reason:
var xScale = d3.time.scale()
.domain([xExtents[0], xExtents[1]])
.range([padding, w - padding * 2]);
Your width of the x scale is w - padding * 2 so the width of the group listening to the mouse event should be same.
working code here
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;
}