correctly sizing a D3 bar graph - javascript

I am attempting to add labels/axis/titles/etc to a D3 bar graph. I can get something close to the right size, however I end up clipping off part of the last bar (so the last bar is skinnier than the others).
Here is the pertinent code:
var x = d3.scale.linear()
.domain([0, object.data.length])
.range([0, object.width]);
var y = d3.scale.linear()
.domain([object.min-(object.max-object.min)*.15, object.max (object.max-object.min)*.15])
.rangeRound([ object.height - 30, 0]);
var vis = d3.select(object.ele)
.append("svg:svg")
.attr("width", object.width)
.attr("height", object.height)
.append("svg:g");
vis.selectAll("g")
.data(object.data)
.enter().append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("y", function(d) { return object.height - y(d.value); })
.attr("width", object.width/object.data.length - 1)
.attr("height", function(d) { return y(d.value); })
.attr("transform", "translate(30,-30)");
At the moment everything (labels, axis, and so on) is 30px. How do I correctly alter the graph to make room for whatever else I need?

You are cutting off your last bar because you use translate the x coordinate but your the range of your x is only to the width without the extra 30 pixels.
Also it may be easier to simplify your y domain to use .domain([object.min, object.max]) then have the "y" and "height" functions reversed. This way you start the rect at the y(d.value) and make it's height object.height - y(d.value).
I would create three groups initially, one for your y axis, one for x axis, and then another for the bars. Draw your bars inside the last group and then translate the whole group itself instead of each individual bar. Increase the size of your object.width and object.height to match the total space you want.

Related

d3js v5 x-axis bar chart

I tried a lot of datasets and I don't know why, I have a issue with data_histo.csv. The x axis seems reverse and then, bars can't be displayed. With data_histo2.csv or data_histo3.csv, all is good.
An explanation could be nice!
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/d3#5.0.0/dist/d3.min.js"></script>
</head>
<body>
<svg class="histogramme" width="960" height="600"></svg>
<script>
//select
let svgHisto = d3.select(".histogramme")
//dimension
let margin = {top: 20, right: 10, bottom: 20, left: 80}
let width = +svgHisto.attr("width") - margin.left - margin.right
let height = +svgHisto.attr("height") - margin.top - margin.bottom;
let g1 = svgHisto.append("g")
.attr("transform",`translate(${margin.left}, ${margin.top})`);
//data
d3.csv("data_histo.csv").then(function(data) {
console.log(data);
//define x and y axis
let x = d3.scaleLinear();
let y = d3.scaleBand();
x.domain(d3.extent(data, function(d) { return d.value0; })).nice()
.rangeRound([0, width]);
y.domain(data.map(function(d) { return d.libreg; }))
.rangeRound([0, height]).padding(0.1);
//add x axis
g1.append("g")
.attr("class", "axis x_axis")
.attr("transform",`translate(0,${height})`)
.call(d3.axisBottom(x));
//add y axis
g1.append("g")
.attr("class", "axis y_axis")
.call(d3.axisLeft(y));
//bar chart
g1.selectAll(".bar1")
.data(data)
.enter().append("rect")
.attr("class", "bar bar1")
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
.attr("y", function(d) { return y(d.libreg) + 10; })
.attr("width", 0)
.attr("height", y.bandwidth() - 20);
//animate
g1.selectAll(".bar1")
.transition()
.duration(1000)
.delay(function(d,i){return i*100})
.attr("width", function(d) { return x(d.value0); });
});
</script>
</body>
</html>
With data_histo.csv
"codreg","libreg","year0","value0","year1","value1"
"03","Guyane",2009,4,2014,4.6
"04","La Réunion",2009,8.2,2014,9.8
"11","Île-de-France",2009,12.6,2014,13.9
"01","Guadeloupe",2009,13.3,2014,15.8
"32","Hauts-de-France",2009,14.7,2014,16.1
"02","Martinique",2009,14.7,2014,17.6
"44","Grand Est",2009,16.5,2014,18
"84","Auvergne-Rhône-Alpes",2009,16.8,2014,18.3
"52","Pays de la Loire",2009,17.1,2014,18.6
"28","Normandie",2009,17.2,2014,19
"53","Bretagne",2009,18.5,2014,20.2
"24","Centre-Val de Loire",2009,18.7,2014,20.4
"27","Bourgogne-Franche-Comté",2009,18.8,2014,20.7
"76","Occitanie",2009,19.3,2014,20.8
"93","Provence-Alpes-Côte d''Azur",2009,19.5,2014,21.3
"94","Corse",2009,20.2,2014,21.5
"75","Nouvelle-Aquitaine",2009,20.2,2014,21.8
With data_histo2.csv
"codreg","libreg","year0","value0","year1","value1"
"84","Auvergne-Rhône-Alpes",2013,39.1,2017,41.7
"27","Bourgogne-Franche-Comté",2013,42.3,2017,44.4
"53","Bretagne",2013,39.6,2017,44.7
"24","Centre-Val de Loire",2013,40.5,2017,46.8
"94","Corse",2013,24.2,2017,30.8
"44","Grand Est",2013,41.3,2017,45.4
"01","Guadeloupe",2013,55.5,2017,56.5
"03","Guyane",2013,33.1,2017,33.2
"32","Hauts-de-France",2013,45.8,2017,47.3
"11","Île-de-France",2013,40.1,2017,42.6
"02","Martinique",2013,52.5,2017,50.2
"28","Normandie",2013,42.6,2017,46.2
"75","Nouvelle-Aquitaine",2013,40,2017,44.4
"76","Occitanie",2013,40.3,2017,43.7
"52","Pays de la Loire",2013,40.6,2017,45.8
"93","Provence-Alpes-Côte d''Azur",2013,38.5,2017,42.6
"04","La Réunion",2013,54.2,2017,54.6
"06","Mayotte",2013,,2017,
Here is my code : https://plnkr.co/edit/B8qEQ4dlUdRHhkQvzjZx?p=preview
There are two issues with your code:
D3 parses csv/tsv/dsv entries as text. So when you load your csv, you get rows that look like this:
{
"codreg": "03",
"libreg": "Guyane",
"year0": "2009",
"value0": "4",
"year1": "2014",
"value1": "4.6"
}
When you set your scale with extent, you aren't using the numerical extent. You could coerce your data to a number like so:
data.forEach(function(d) {
d.value0 = +d.value0;
})
Secondly, if you do this you'll notice some peculiar behavior in the placement of the bars:
You can see that the bars start to the left of the plot area. The reason is that you are using a linear scale, and plot the start of the bars like so:
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
You want your bars to start at x(4) - which is where the x value that marks the interception with the y axis. Instead you are using x(0), which will naturally be to the left of where you want. This works in your second example, because x(0) also happens to be the x value that intercepts the y axis. Instead, you can simply use:
.attr("x",0)
This marks the left edge of the plot area, which is likely where you want all your bars to be anchored to.
Here's a forked plunkr.
One thing to note, is that your shortest bar will always be non-visible: the start and end points will be the same. This is because the extent of the scale goes from the smallest value to the largest value - and the smallest value, marking the left boundary of the plot area, is the value of the shortest bar. To change this you can modify the scale's domain, perhaps using 0 as the first value and then using d3.max to find the uppermost value.

D3 Categorical Area Chart - Scale Issue

I'm trying to create an area chart for statistics on each US state. I have a single number statistic for each state; an element of my data list looks like the following:
{'state':'CA','count':4000}
Currently, my area chart looks like this. The task is mainly complete, but you may notice how the very last category (in this case, UTAH) isn't filled. I'm not quite sure how to get around this. close_up
I am using a scaleBand axis; this felt appropriate. Perhaps it is not the correct approach. Here is the JS behind the chart:
var svg_area = d3.select("#area")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom),
g_area = svg_area.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand().range([0, width]),
y = d3.scaleLinear().range([height, 0]);
var area = d3.area()
.x(function(d) { return x(d.state); })
.y0(height)
.y1(function(d) { return y(d.count); });
d3.csv('data/states.csv', function(data) {
data.forEach(function(d) {
d.count = +d.count;
});
data.sort(function(a, b){
return b.count-a.count;
});
data = data.slice(0,30);
x.domain(data.map(function(d) { return d.state; }));
y.domain([0, d3.max(data, function(d) { return d.count; })]);
g_area.append('path')
.datum(data)
.attr('fill', solar[1])
.attr("class", "area")
.attr('d', area);
g_area.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g_area.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(0," + 0 + ")")
.call(d3.axisLeft(y));
});
Any suggestions on how I can fix this? Thanks for any feedback!
Contrary to your question's title (now edited), the area chart is not "leaving out the last data point".
What you're seeing is the expected result, since you are using a band scale. Actually, that value just above the horizontal axis (just in the "edge" of the area chart) is Utah value! Try to understanding it with this explanation: Imagine a bar chart with your data. Each bar has, of course, a given width. Now, draw a path going from the top left corner of one bar to the top left corner of the next bar, starting at the first bar and, when reaching the last bar, going down from the top left corner to the axis. That's the area you have right now.
There are two solutions here. The first one is using a point scale instead:
var x = d3.scalePoint().range([0, width])
However, this will trim the "margins" of the area path, before the first state and after the last state (Utah). That means, the area chart will start right over California tick and end right over Utah tick.
If you don't want that there is a second solution, which is hacky, but will keep those "margins": add the bandwidth() to the last state in the area generator:
var area = d3.area()
.x(function(d, i) {
return i === data.length - 1 ?
x(d.state) + x.bandwidth() : x(d.state)
})
It may be worth noting that, using a band scale, your chart is technically incorrect: the values in the area for each state are not over the tick for that state.

In D3 v4, how do I keep the width of a bar centered around a tick mark?

I am using D3 v4 (most, if not all, of the examples out there are for v3). Back in v3, they had something called rangeBand() which would be able to dynamically position everything neatly on the x-axis for me.
Now, in v4, I am wondering how to do that.
I have a bar chart:
var barEnter = vis.selectAll("g")
.data(rawdata)
.enter()
.append('g')
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", canvas_width / rawdata.length);
It is the width of this bar this is messing me up. If I set it to canvas_width / rawdata.length, it nicely positions the bars centered around each tick on the x-axis. The problem is that all the bars are pressed together and there is no padding in between.
So, naturally, I tried to do x.paddingInner(.5) which does add some padding but now the bars are not centered around the tick marks. Doing anything with x.paddingOuter() messes things up even more.
After searching around, I found that rangeBand() is what I want but that's only for v3. In the v4 docs, there is nothing that quite looks like it. Is it rangeRound()? Is it align()? I'm not sure. If anyone can point me in the right direction, it'd be greatly appreciated.
Without seeing your code for the axis, I suppose you're using scaleOrdinal(). If that's the case, you can change for scaleBand(), in which it's very easy to center the bar around the tick.
All you need is:
band.paddingInner([padding]): Sets the inner padding of the bars
band.bandwidth(): Gives you the bandwidth of each bar.
Then, you set the x position using the corresponding variable in your data and the width using bandwidth().
This is a small snippet to show you how it works:
var w = 300, h = 100, padding = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var data = [{name: "foo", value:50},
{name: "bar", value:80},
{name: "baz", value: 20}];
var xScale = d3.scaleBand()
.range([0,w])
.domain(data.map(function(d){ return d.name}))
.paddingInner(0.2)
.paddingOuter(0.2);
var xAxis = d3.axisBottom(xScale);
var bars = svg.selectAll(".bars")
.data(data)
.enter()
.append("rect");
bars.attr("x", function(d){ return xScale(d.name)})
.attr("width", xScale.bandwidth())
.attr("y", function(d){ return (h - padding) - d.value})
.attr("height", function(d){ return d.value})
.attr("fill", "teal");
var gX = svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

Adjusting D3 Chart Bar Height

I'm putting together a D3 vertical bar chart and can't adjust the bar's height. When I adjust the barHeight variable from the following javascript, when rendered the bar's look exactly the same. Meaning if I took off the *50 in barHeight nothing would change. I'm probably overlooking something really really simple here, but how do I adjust the height?
var bars = svg.selectAll("rect")
.data(data_votes);
bars.enter()
.append("rect")
// .transition()
// .duration(1000);
bars.attr("x", function(d, i) {
return i * (w / data_votes.length);
})
.attr("y", function(d){
return h-d;
})
// Less bars == more width
.attr("width", w / data_votes.length - barPadding)
.attr("height", function(d){
var barHeight = d*50;
return barHeight;
})
.attr("fill", "teal");
bars.exit().remove();
Here is a working example with some corrected code:
And a very good example of why links to other sites that don't include the code are a bad idea. Tributary.io is not responding in December of 2019.
http://tributary.io/inlet/5767993
(You can click on any number and change it with the slider to see it's effect on the output)
I think the main issue was that the scale factor needed to be applied to both the "y" attribute and the height attribute.
You're right, I was adjusting the offset with the height so while the height pixel was actually increasing, it was offset proportionally and hidden off the edge of the svg canvas.
Here's how I adjusted it
.attr("y", function(d){
return h-(d*4);
})
// Less bars == more width
.attr("width", w / data_votes.length - barPadding)
.attr("height", function(d) {
return d * 4 ;
})

d3.js - evenly spaced bars on a time scale

I'm building a bar plot in d3.js in which each bar represents total TB cases during a month. The data essentially consists of a date (initially strings in %Y-%m format, but parsed using d3.time.format.parse) and an integer. I'd like the axis labels to be relatively flexible (show just year boundaries, label each month, etc.), but I'd also like the bars to be evenly spaced.
I can get flexible axis labeling when I use a date scale:
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
... but the bars aren't evenly spaced due to varying numbers of days in each month (e.g., February and March are noticeably closer together than other months). I can get evenly-spaced bars using a linear scale:
var xScaleLinear = d3.scale.linear()
.domain([0, thisstat.length])
.range([0, width - margin.left - margin.right]);
... but I can't figure out how to then have date-based axis labels. I've tried using both scales simultaneously and only generating an axis from the xScaleDate, just to see what would happen, but the scales naturally don't align quite right.
Is there a straightforward way to achieve this that I'm missing?
You can combine ordinal and time scales:
// Use this to draw x axis
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
// Add an ordinal scale
var ordinalXScale = d3.scale.ordinal()
.domain(d3.map(thisstat, function(d) { return d.date; }))
.rangeBands([0, width], 0.4, 0);
// Now you can use both of them to space columns evenly:
columnGroup.enter()
.append("rect")
.attr("class", "column")
.attr("width", ordinalXScale.rangeBand())
.attr("height", function (d) {
return height - yScale(d.value);
})
.attr("x", function (d) {
return xScaleDate(d.date);
})
.attr("y", function (d){
return yScale(d.value);
});
I've created an example a while ago to demonstrate this approach: http://codepen.io/coquin/pen/BNpQoO
I had the same problem, I've ended up accepting that some months are longer than others and adjusting the column bar width so that the gap between bars remains constant. So tweaking the barPath function in the crossfilter home page demo (http://square.github.com/crossfilter/ - uses d3) I got something like this:
var colWidth = Math.floor(x.range()[1] / groups.length) - 1;//9;
if (i < n - 1) {
//If there will be column on the right, end this column one pixel to the left
var nextX = x(groups[i+1].key)
colWidth = nextX - x(d.key) - 1;
}
path.push("M", x(d.key), ",", height, "V", yVal, "h",colWidth,"V", height);
Try d3.scale.ordinal:
var y = d3.scale.ordinal()
.domain(yourDomain)
.rangeRoundBands([0, chartHeight], 0.2);
Tweek 0.2 parameter.

Categories