Reputation: 395
I'm making a barchart in D3 using the following data: [-76, -55, -51, -44, -35, 27]
The Y axis should go from -100, to 100.
This is the code:
var data = [-76, -55, -51, -44, -35, 27];
var leftMargin = 50; // Space to the left of first bar; accomodates y-axis labels
var rightMargin = 10; // Space to the right of last bar
var margin = {left: leftMargin, right: rightMargin, top: 10, bottom: 10};
var barWidth = 30; // Width of the bars
var chartHeight = height; // Height of chart, from x-axis (ie. y=0)
var chartWidth = margin.left + data.length * barWidth + margin.right;
/* This scale produces negative output for negatve input */
var yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, chartHeight]);
/*
* We need a different scale for drawing the y-axis. It needs
* a reversed range, and a larger domain to accomodate negaive values.
*/
var yAxisScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight - yScale(d3.min(data)), 0 ]);
svg2
.attr('height', chartHeight + 100)
.attr('width', chartWidth)
.style('border', '1px solid');
svg2
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d, i) { return margin.left + i * barWidth; })
.attr("y", function(d, i) { return chartHeight - Math.max(0, yScale(d));})
.attr("height", function(d) { return Math.abs(yScale(d)); })
.attr("width", barWidth)
.style("fill", "grey")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", function(d, i) { return 1 /*- (i * (1/data.length)); */});
var yAxis = d3.axisLeft(yAxisScale);
svg2.append('g')
.attr('transform', function(d) {
return 'translate(' + margin.left + ', 0)';
})
.call(yAxis);
This is the outputted chart - as you can see the scale is off and the image isn't scaling to the SVG container - the negative bars are going beyond the bottom of the svg.
Can someone please help me properly format this bar chart please?
Upvotes: 2
Views: 957
Reputation: 38151
/* * We need a different scale for drawing the y-axis. It needs * a reversed range, and a larger domain to accomodate negaive values. */
This is not necessary and creates more complication. A single scale is sufficient and will always match your data with your axis. As soon as you create different scales for the drawn axes and data, you'll find greater potential for conflict between the two.
Instead let's create one scale, yScale
here:
var yScale = d3.scaleLinear()
.domain([-100,100]) // min & max of input values
.range([chartHeight,0]) // mapped to the bottom and top of the plot area
For values under 0, negative bars, the top of the bar will be at yScale(0)
. The bottom of the rect will be at yScale(value)
. The difference between the two, the height, will be equal to yScale(value) - yScale(0)
.
For values over 0, the bottom of the bar will be at yScale(0)
, the top of the bar will be at yScale(value)
. To find the height we use yScale(0)-yScale(value)
.
Since the height is just the absolute difference between yScale(0)
and yScale(value)
we can just use Math.abs(yScale(0)-yScale(value))
to find the height of all rectangles.
I've remodelled your example code considering the above:
var data = [-76, -55, -51, -44, -35, 27];
var height = 250;
var width = 500;
var margin = {left: 50, right: 10, top: 20, bottom: 20};
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.style('border', '1px solid')
.append("g")
// apply the margins:
.attr("transform","translate("+[margin.left+","+margin.top]+")");
var barWidth = 30; // Width of the bars
// plot area is height - vertical margins.
var chartHeight = height-margin.top-margin.left;
// set the scale:
var yScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight, 0]);
// draw some rectangles:
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d, i) { return i * barWidth; })
.attr("y", function(d,i) {
if(d < 0) {
return yScale(0); // if the value is under zero, the top of the bar is at yScale(0);
}
else {
return yScale(d); // otherwise the rectangle' top is above yScale(0) at yScale(d);
}
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("stroke", "black")
.style("stroke-width", "1px")
var yAxis = d3.axisLeft(yScale);
svg.append('g')
.call(yAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Additionally, we can simplify a bit further. The minimum of yScale(value)
and yScale(0)
(the value closer to the top of the SVG) will be the top of a the rectangle regardless of the value the rectangle represents.
var data = [-76, -55, -51, -44, -35, 27];
var height = 250;
var width = 500;
var margin = {left: 50, right: 10, top: 20, bottom: 20};
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.style('border', '1px solid')
.append("g")
// apply the margins:
.attr("transform","translate("+[margin.left+","+margin.top]+")");
var barWidth = 30; // Width of the bars
// plot area is height - vertical margins.
var chartHeight = height-margin.top-margin.left;
// set the scale:
var yScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight, 0]);
// draw some rectangles:
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d, i) { return i * barWidth; })
.attr("y", function(d,i) {
return Math.min(yScale(0),yScale(d))
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("stroke", "black")
.style("stroke-width", "1px")
var yAxis = d3.axisLeft(yScale);
svg.append('g')
.call(yAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
By using the minimum of the two it gives us a bit more freedom: we could flip the graph by only changing the domain of the scale (if for some reason we wanted to represent negative values as though they were positive...)
Upvotes: 3