score:3

Accepted answer

/* * 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...)


Related Query

More Query from same tag