Reputation: 303
I have a bit of a problem building a bar chart. I'm learning d3.js for the first time and being someone who always worked with PHP/MySQL, I haven't had to learn javascript. As a result, I'm struggling a bit.
My question is more conceptual in nature. If, let's say in a bar chart, the Y axis is contained in a g element and the bars are contained in another one, how can I ensure that my axis takes a dyanmic width based on the data presented?
I managed to generate a bar chart and it works great, but the padding is a fixed number (let's say 50px). it works great now, because my numbers go from 0 to 50, so everything fits. What happens if I get trillions instead? The width of the axis will change, yet my padding remains 50px, which means it will clip my content.
What is the "convention" when it comes to this? Any tricks?
Thanks
Upvotes: 0
Views: 2240
Reputation: 108512
One trick you might use here is what I like to call the "double-render". You essentially draw the axis first (before the rest of the plot) and get the width of the greatest tick label. The, You can draw the plot conventionally with that value as the margin. This trick is especially useful for string "category" labels, but will work for numbers as well.
Here's a commented example. Run it multiple times to see how it refits the axis:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis--x path {
display: none;
}
</style>
<svg width="300" height="300"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//chancejs.com/chance.min.js"></script>
<script>
// set up some random data
// pick a random max value to render on the yaxis
var maxVal = chance.integer({
min: 1,
max: chance.pickone([1e1, 1e5, 1e10])
}),
// generate some fake data
data = [{
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}];
// create svg and set up a y scale, the height value doesn't matter
var svg = d3.select("svg"),
y = d3.scaleLinear().rangeRound([100, 0]);
// set domain
y.domain([0, d3.max(data, function(d) {
return d.y;
})]);
// draw fake axis
var yAxis = svg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
// determine max width of text label
var mW = 0;
yAxis.selectAll(".tick>text").each(function(d) {
var w = this.getBBox().width;
if (w > mW) mW = w;
});
// remove fake yaxis
yAxis.remove();
// draw plot normally
var margin = {
top: 20,
right: 20,
bottom: 30,
left: mW + 10 // max with + padding fudge
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// reset to actual height
y.range([height, 0]);
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
x.domain(data.map(function(d) {
return d.x;
}));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", x.bandwidth())
.attr("height", function(d) {
return height - y(d.y);
});
</script>
Upvotes: 6