Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cut y axis to make the chart look better?

What I'm trying to do is create a bar chart using D3.js (v4) that will show 2 or 3 bars that have a big difference in values.

Like shown on the picture below yellow bar has a value 1596.6, whereas the green bar has only 177.2. So in order to show the charts in elegant way it was decided to cut the y axis at a certain value which would be close to green bar's value and continue closer to yellow bar's value.

On the picture the y axis is cut after 500 and continues after 1500.

How one would do that using D3.js?

enter image description here

like image 644
Akbar Avatar asked Dec 03 '25 20:12

Akbar


1 Answers

What you want is, technically speaking, a broken y axis.

It's a valid representation in data visualization (despite being frowned upon by some people), as long as you explicitly tell the user that your y axis is broken. There are several ways to visually indicate it, like these:

enter image description here

However, don't forget to make this clear in the text of the chart or in its legend. Besides that, have in mind that "in order to show the charts in elegant way" should never be the goal here: we don't make charts to be elegant (but it's good if they are), we make charts to clarify an information or a pattern to the user. And, in some very limited situations, breaking the y axis can better show the information.

Back to your question:

Breaking the y axis is useful when you have, like you said in your question, big differences in values. For instance, have a look at this simple demo I made:

var w = 500,
  h = 220,
  marginLeft = 30,
  marginBottom = 30;
var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);
var data = [{
  name: "foo",
  value: 800
}, {
  name: "bar",
  value: 720
}, {
  name: "baz",
  value: 10
}, {
  name: "foobar",
  value: 17
}, {
  name: "foobaz",
  value: 840
}];
var xScale = d3.scaleBand()
  .domain(data.map(function(d) {
    return d.name
  }))
  .range([marginLeft, w])
  .padding(.5);
var yScale = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.value
  })])
  .range([h - marginBottom, 0]);
var bars = svg.selectAll(null)
  .data(data)
  .enter()
  .append("rect")
  .attr("x", function(d) {
    return xScale(d.name)
  })
  .attr("width", xScale.bandwidth())
  .attr("y", function(d) {
    return yScale(d.value)
  })
  .attr("height", function(d) {
    return h - marginBottom - yScale(d.value)
  })
  .style("fill", "teal");
var xAxis = d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0," + (h - marginBottom) + ")"));
var yAxis = d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(" + marginLeft + ",0)"));
<script src="https://d3js.org/d3.v4.js"></script>

As you can see, the baz and foobar bars are dwarfed by the other bars, since their differences are very big.

The simplest solution, in my opinion, is adding midpoints in the range and domain of the y scale. So, in the above snippet, this is the y scale:

var yScale = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.value
  })])
  .range([h - marginBottom, 0]);

If we add midpoints to it, it become something like:

var yScale = d3.scaleLinear()
  .domain([0, 20, 700, d3.max(data, function(d) {
    return d.value
  })])
  .range([h - marginBottom, h/2 + 2, h/2 - 2, 0]);

As you can see, there are some magic numbers here. Change them according to your needs.

By inserting the midpoints, this is the correlation between domain and range:

+--------+-------------+---------------+---------------+------------+
|        | First value | Second value  |  Third value  | Last value |
+--------+-------------+---------------+---------------+------------+
| Domain | 0           | 20            | 700           | 840        |
|        | maps to...  | maps to...    | maps to...    | maps to... |
| Range  | height      | heigh / 2 - 2 | heigh / 2 + 2 | 0          |
+--------+-------------+---------------+---------------+------------+

You can see that the correlation is not proportional, and that's exactly what we want.

And this is the result:

var w = 500,
  h = 220,
  marginLeft = 30,
  marginBottom = 30;
var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);
var data = [{
  name: "foo",
  value: 800
}, {
  name: "bar",
  value: 720
}, {
  name: "baz",
  value: 10
}, {
  name: "foobar",
  value: 17
}, {
  name: "foobaz",
  value: 840
}];
var xScale = d3.scaleBand()
  .domain(data.map(function(d) {
    return d.name
  }))
  .range([marginLeft, w])
  .padding(.5);
var yScale = d3.scaleLinear()
  .domain([0, 20, 700, d3.max(data, function(d) {
    return d.value
  })])
  .range([h - marginBottom, h/2 + 2, h/2 - 2, 0]);
var bars = svg.selectAll(null)
  .data(data)
  .enter()
  .append("rect")
  .attr("x", function(d) {
    return xScale(d.name)
  })
  .attr("width", xScale.bandwidth())
  .attr("y", function(d) {
    return yScale(d.value)
  })
  .attr("height", function(d) {
    return h - marginBottom - yScale(d.value)
  })
  .style("fill", "teal");
var xAxis = d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0," + (h - marginBottom) + ")"));
var yAxis = d3.axisLeft(yScale).tickValues([0, 5, 10, 15, 700, 750, 800])(svg.append("g").attr("transform", "translate(" + marginLeft + ",0)"));
svg.append("rect")
  .attr("x", marginLeft - 10)
  .attr("y", yScale(19.5))
  .attr("height", 10)
  .attr("width", 20); 
svg.append("rect")
  .attr("x", marginLeft - 10)
  .attr("y", yScale(19))
  .attr("height", 6)
  .attr("width", 20)
  .style("fill", "white"); 
<script src="https://d3js.org/d3.v4.js"></script>

As you can see, I had to set the y axis ticks using tickValues, otherwise it'd be a mess.

Finally, I can't stress this enough: do not forget to show that the y axis is broken. In the snippet above, I put the symbol (see the first figure in my answer) between the number 15 and the number 700 in the y axis. By the way, it's worth noting that in the figure you shared its author didn't put any symbol in the y axis, which is not a good practice.

like image 68
Gerardo Furtado Avatar answered Dec 06 '25 00:12

Gerardo Furtado