Unable to display x axis categories and make y axis start at 0 - d3.js

In this D3.js version 6.7 bar chart I am trying to align the x axis to show the categories and show the y axis to start at 0. Extending the height of the svg and changing the transforms does not appear to be working. How can I make the x axis categories appear under the bars and make the y axis start at 0? Thank you.
async function barChart() {
const dataset = await d3.csv("https://assets.codepen.io/417105/bodypart-injury-clean.csv");
console.log(dataset);
const width = 400;
const height = 400;
const margin = {top:20, right:30, bottom:30, left:40};
const canvas = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
const wrapper = canvas.append("g").style("transform",`translate(${margin.left}px,${margin.top}px)`);
const xScale = d3.scaleBand()
.domain(["Arm","Eye","Head","Hand","Leg","Other"])
.range([0,width - margin.left])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset, d => +d.Total))
.range([height,0]);
console.log(xScale("Leg"));
console.log(yScale(1700));
const barRect = wrapper.append("g")
.selectAll('rect')
.data(dataset)
.join('rect')
.attr('x', d => xScale(d.BodyRegion))
.attr('y', d => yScale(+d.Total))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(+d.Total))
.attr('fill', 'teal');
const yAxis = d3.axisLeft().scale(yScale);
wrapper.append("g").call(yAxis);
const xAxis = d3.axisBottom().scale(xScale);
wrapper.append("g").attr('transform', `translate(0,${height-margin.bottom})`).call(xAxis);
}
barChart();

The Y scale
The scale's domain sets the extent of the scale in your data's units, the scale's range sets the scale's extent in scaled units (pixels here). The first value in the domain is mapped to the first value in the range.
Your domain is set to:
.domain(d3.extent(dataset, d => +d.Total))
d3.extent returns the minimum and maximum matching values, as your minimum value is not zero, your scale's domain does not start at 0. If you want to set the scale's domain's lower bounds to zero, you need to set that, like so:
.domain([0,d3.max(dataset,d=> +d.Total)])
.domain/.range take arrays, these arrays for a linear scale must have the same number of elements
But you also don't want your scale's range to be [height,0] because of margins:
.range([height-margin.bottom,margin.top])
You want the data to be scaled from between within the two margins, height-margin.bottom is the furthest down the page you want to plot data, and margin.top is the furthest to the top of the SVG you want to plot data.
Now your bars are a bit off, that's because you aren't accounting for the margin in the height attribute:
.attr('height', d => height - yScale(+d.Total))
you need:
.attr('height', d => height - margin.bottom - yScale(+d.Total))
Note, a common approach to avoid having to constantly reference the margin is to apply the margin to a parent g and have width height reflect the size of the plot area within that g (not the entire SVG).
The X Axis
Now that the y scale is configured, let's look at the x axis. All you need to do here is boost the bottom margin: the text is appended (you can inspect the page to see it is there, just not visible). Try margin.bottom = 50.

Related

d3js grouped bar chart, is this possible?

I want to create a grouped bar chart where each bar is unique and not part of a series.
For example, imagine a bar chart showing the population of each major city, grouped by state.
Is this possible with d3js and any pointers on how to to get started?
Thanks
Yes it is possible!
As with most visualisation the process is one of working out the structure of the chart and then working out how to get the data in to that structure. In this case I think the data in it's simplest form is going to look something like this ...
let groups = [
{ name:'one', values:[1,3,6,2] },
{ name:'two', values:[3,5,7,3,2,5] },
{ name:'three', values:[9,2,5] },
{ name:'four', values:[6] }
];
An array of groups, where each group has some properties including an array of values to be represented as individual bars.
The tricky bit is working out the position of each group and each bar along the horizontal axis. The way I think about this is as follows: If we say each bar has a width of 1 unit, how much space do we want between the groups? Maybe a bar and a half, so 1.5 units. Now each group is going to take up space equal to the number of bars in it (the length of the values array) and the total width required by the chart will be the sum of those values plus the spaces between them.
i.e.
let dataWidth = d3.sum(groups, d=>d.values.length) + (groups.length-1) * groupPadding;
We also want to go through the groups and work out their "start position", where the group is placed horizontally in terms of bar units, like this:
let groupPadding = 1.5;
let currentWidth = 0;
groups = groups.map(group=>{
group.width = group.values.length;
group.startPosition = currentWidth;
currentWidth += group.width+groupPadding;
return group;
});
Next we need to make an x and a y-scale using d3-scale.
For the y-scale let's keep it simple and hard code the maximum value
const yScale = d3.scaleLinear()
.domain([0,10])
.range([chartHeight, 0]); // chart height is just your charts height in pixels
and for the x scale we use the dataWidth we calculated above as the maximum for the domain, we'll use this scale to convert bar units into screen pixels
const xScale = d3.scaleLinear()
.domain([0, barWidth)
.range([0, chartWidth]); //chartWidth, the width of your chart in pixels
OK. Now we have everything we need to know to draw the chart. The structure of the chart as I see envisage it...
Each group in groups is an svg g element positioned according to its startPosition. Each of those g elements contains a set of rect elements, one for each value. Inside the g element you may also want to put stuff like a group label.
Broadly this would look something like this...
Create the groups
const barGroups = chart.selectAll('g.bar-group')
.data(groups)
.enter()
.append('g')
.classed('bar-group', true)
.attr('transform', group=>`translate(${xScale(group.startPosition)}, 0)`);
Add the rectangles
barGroups.each(function(group){
const barGroup = d3.select(this);
barGroup.selectAll('rect')
.data(group.values)
.enter()
.append('rect')
.attr('width', xScale(1))
.attr('height', value => yScale(value))
.attr('x', (value, i) => xScale(i))
.attr('y', value => chartHeight - yScale(value));
Here's a bl.ock that puts it all together and fills in the gaps to give a working example: Grouped Bars

d3 bar chart scale reversed on y-axis, doesn't look like the classic problem, why is it acting odd?

Drawing a histogram in d3 (vertical bar chart), works fine except the y-axis scale is reversed. Here is the code that (almost) works:
let maxFrequency = d3.max(histData, d=> d.length);
let yScale = d3.scaleLinear()
.range([0,config.height - config.margin.top - config.margin.bottom])
.domain([0,maxFrequency]); //<-- this is wrong, right? It should be [maxFrequency,0]
let xScale = d3.scaleBand()
.range([0, (config.width - config.margin.left - config.margin.right)])
.domain(histData.map(d => d.Name))
.padding(0.2);
//Draw the histogram bars
let bars = config.svgContainer.selectAll("rect")
.data(histData);
bars.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("height", (d)=> yScale(d.length))
.attr("x", (d) => xScale(d.Name) + config.margin.left)
.attr("y", (d)=> config.height - config.margin.bottom - yScale(d.length))
.attr("fill", "red");//"#2a5599");
let axisX = d3.axisBottom(xScale)
config.svgContainer.append("g")
.style("transform", `translate(${config.margin.left}px,${config.height -
config.margin.bottom}px)`)
.call(axisX)
let axisY = d3.axisLeft(yScale)
config.svgContainer.append("g")
.style('transform',`translate(${config.margin.left}px,${config.margin.top}px)`)
.call(axisY);
This code produces this graph.
enter image description here
Normally you need to reverse the order of the domain attribute for the y-axis. Here is the graph when I use the ".domain([maxFrequency,0])" code with no other changes.
enter image description here
You can see that the y-axis now is correct but the bars look wonky. On closer inspection you can see that the data (1,9,40,80) is still being represented, but the bars are a now reversed. The bars are now height 79 (80-1), 71 (80-9), 40 (80-40), and 0 (80-80).
I hope this is something simple I'm not seeing. I can't find any posted solutions for something that sounds like this.

d3js: Unable to place horizontally bars within the axis

I'm learning to make charts in d3 from scratch without taking someone else code and modifying it. I can successfully create a x & y axis vertical bar chart. But when it comes to transform the same chart to horizontal bar chart I end up in a mess. Here is my code so far:
var data = [{
name: "China",
value: 1330141295
}, {
name: "India",
value: 1173108018
}, {
name: "Indonesia",
value: 242968342
}, {
name: "Russia",
value: 139390205
}];
//set margins
var margin = {
top: 30,
right: 30,
bottom: 30,
left: 40
};
var width = 960 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
//set scales & ranges
var yScale = d3.scaleBand().range([0, height]).padding(0.1)
var xScale = d3.scaleLinear().range([0, width])
//draw the svg
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom).append("g")
.attr("transform", "translate(" + margin.left * 2 + "," + margin.top + ")")
//load the data
data.forEach(function(d) {
d.population = +d.population;
});
//set domains
xScale.domain([0, d3.max(data, function(d) {
return d.population
})])
yScale.domain(data.map(d => d.name))
//add x & y Axis
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale))
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", d => yScale(d.name))
.attr("height", d => yScale(d.name))
.attr("x", d => width - xScale(d.population))
.attr("width", yScale.bandwidth())
Thank you very much.
You need to change a lot of things in your code.
TL;DR
change value to population in the array
scales are used to convert values to proportional pixel values
height is the vertical size of an element. you should use yScale(d.name)
width is the horizontal size of an element. you should use xScale(d.population)
y is the vertical position of an element. you should use yScale.bandwidth()
x is the vertical position of an element. you should use 0
use selectAll("rect") on a new appended g or the svg element not the same g element that has the axises on it
add fill attribute so that your rects have color
You have the population field labelled value but you're calling population through out the code to use it. So replace value with population in your data objects.
Next you need to change the way you're setting up the rects. use selectAll on the svg element directly or append another g to the svg element and add the rects on that element. Right now your code attempts to add the rects to the same g element as the x axis.
Make sure you are setting the attributes of the rects correctly. Height is the size in pixels of the rect element not the position. y is the position of the rects vertically from the top downwards. this means the height attribute should use yScale(d.name) and width should use xScale(d.population) because they are the width and length of the rectangles, or rect elements. x and y are the coordinate positions of the element from the top left corner of the svg element. Use them to determine the starting position of the top left pixel of your rects. You should have y as yScale.bandwidth() and x as 0.

D3 v4 - make a horizontal bar chart with fixed width

I have made a horizontal bar chart using d3 v4, which works fine except for one thing. I am not able to make the bar height fixed. I am using bandwidth() currently and if i replace it with a fixed value say (15) the problem is that it does not align with the y axis label/tick http://jsbin.com/gigesi/3/edit?html,css,js,output
var w = 200;
var h = 400;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("transform", "translate(80,30)");
var data = [
{Item: "Item 1", count: "11"},
{Item: "Item 2", count: "14"},
{Item: "Item 3", count: "10"},
{Item: "Item 4", count: "14"}
];
var xScale = d3.scaleLinear()
.rangeRound([0,w])
.domain([0, d3.max(data, function(d) {
return d.count;
})]);
var yScale = d3.scaleBand()
.rangeRound([h,0]).padding(0.2)
.domain(data.map(function(d) {
return d.Item;
}));
var yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class','axis')
.call(yAxis);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', function(d,i) {
return xScale(d.count);
})
.attr('height', yScale.bandwidth())
.attr('y', function(d, i) {
return yScale(d.Item);
}).attr("fill","#000");
The y axis seemed to be off SVG in the link you provided. (Maybe you have overflow: visible; for the SVG.
Anyway, I've added a few margins to the chart so that the whole chart is visible. Here it is (ignore the link description):
DEMO: H BAR CHART WITH HEIGHT POSITIONING TO THE TICKS
Relevant code changes:
As you are using a scale band, the height is computed within. You just need to use .bandWidth().
.attr('height', yScale.bandwidth())
Added a margin and transformed the axis and the bars to make the whole chart visible :
: I'm assigning margins so that the y-axis is within the viewport of the SVG which makes it easier to adjust the left margin based on the tick value as well. And I think this should be a standard practice.
Also, if you notice, the rects i.e. bars are now a part of <g class="bars"></g>. Inspect the DOM if you'd like. This would be useful for complex charts with a LOT of elements but it's always a good practice.
var margin = {top: 10, left: 40, right: 30, bottom: 10};
var xScale = d3.scaleLinear()
.rangeRound([0,w-margin.left-margin.right])
var yScale = d3.scaleBand()
.rangeRound([h-margin.top-margin.bottom,0]).padding(0.2)
svg.append('g')
.attr('class','axis')
.attr('transform', 'translate(' + margin.left+', '+margin.top+')')
Try changing the data and the bar height will adjust and align according to the ticks. Hope this helps. Let me know if you have any questions.
EDIT:
Initially, I thought you were facing a problem placing the bars at the center of the y tick but as you said you needed fixed height bars, here's a quick addition to the above code that lets you do that. I'll add another approach using the padding (inner and outer) sometime soon.
Updated JS BIN
To position the bar exactly at the position of the axis tick, I'm moving the bar from top to the scale's bandwidth that is calculated by .bandWidth() which will the position it starting right from the tick and now subtracting half of the desired height half from it so that the center of the bar matches the tick y. Hope this explains.
.attr('height', 15)
.attr('transform', 'translate(0, '+(yScale.bandwidth()/2-7.5)+')')

D3 log scale displaying wrong numbers

I'm trying to wrap my head around the log scales provided by D3.js. It should be noted that as of yesterday, I had no idea what a logarithmic scale was.
For practice, I made a column chart displaying a dataset with four values: [100, 200, 300, 500]. I used a log scale to determine their height.
var y = d3.scale.log()
.domain([1, 500])
.range([height, 1]);
This scale doesn't work (at least not when applied to the y-axis as well). The bar representing the value 500 does not reach the top of the svg container as it should. If I change the domain to [100, 500] that bar does reach the top but the axis ticks does not correspond to the proper values of the bars. Because 4e+2 is 4*10^2, right?
What am I not getting here? Here is a fiddle.
Your scale already reverses the range to account for the SVG y-coordinates starting at the top of the screen -- ie, you have domain([min, max]) and range([max, min]). This means your calcs for the y position and height should be reversed because your scale already calculated y directly:
bars.append("rect")
.attr("x", function (d, i) { return i * 20 + 20; })
.attr("y", function (d) { return y(d); })
.attr("width", 15)
.attr("height", function (d) { return height - y(d); });
Here's an updated Fiddle: http://jsfiddle.net/findango/VeNYj/2/

Resources