如何切割Y轴以使图表看起来更好?

3
我尝试使用D3.js(v4)创建一个条形图,显示2或3个值差异很大的柱形。 如下图所示,黄色柱的值为1596.6,而绿色柱仅为177.2。 为了以优雅的方式显示图表,决定在某个接近绿色柱值的位置处截断y轴,并继续靠近黄色柱值。 在图片上,y轴在500之后被截断,在1500之后继续。 如何使用D3.js实现这一点?

enter image description here

1个回答

5
你需要的是一个技术上称为“断轴”的y轴。
这在数据可视化中是一种有效的表示方法(尽管有些人不赞成),只要你明确告诉用户你的y轴是断开的。有几种视觉方式可以表明它,比如这些:

enter image description here

然而,不要忘记在图表的文本或图例中明确这一点。除此之外,请注意,“为了以优雅的方式展示图表”绝不能成为目标:我们制作图表的目的不是为了让它们优美(但如果它们是优美的那就很好),我们制作图表是为了向用户澄清信息或模式。在某些非常有限的情况下,打破y轴可以更好地展示信息。
回到你的问题:
当你有像你在问题中提到的值的巨大差异时,打破y轴是有用的。例如,请看我做的这个简单演示:

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>

正如您所看到的,bazfoobar条形图因它们之间的差异非常大而被其他条形图所掩盖。
在我看来,最简单的解决方案是在y轴刻度范围和y轴刻度标签中添加中点。因此,在上面的代码片段中,这是y轴刻度范围:
var yScale = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.value
  })])
  .range([h - marginBottom, 0]);

如果我们在其中添加中点,它会变成类似这样的东西:
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]);

如您所见,这里有一些魔法数字。根据您的需求进行更改。

通过插入中点,这是域和范围之间的相关性:

+--------+-------------+---------------+---------------+------------+
|        | 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          |
+--------+-------------+---------------+---------------+------------+

你可以看到相关性不是成比例的,这正是我们想要的。

这就是结果:

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>

如您所见,我必须使用tickValues设置y轴刻度,否则会很混乱。

最后,我再次强调:不要忘记显示y轴已经被打断。在上面的代码段中,我将符号(请参阅我的答案中的第一个图)放置在y轴上数字15和数字700之间。顺便说一句,值得注意的是,在您分享的图中,其作者没有在y轴上放置任何符号,这是不好的做法。


谢谢,Gerardo!它运行得很好 :) 不过我还有一个问题。你使用tickValues来设置y轴刻度,如果你事先知道刻度的值,那么这样做是没问题的。但是如果你的值是动态的(比如从数据库获取),而且无法设置tickValues,那该怎么解决在边缘处出现的混乱情况呢? - undefined
我假设不显示介于下限条的最大值和上限条的最小值之间的刻度值将解决该问题,但我不知道如何使某个范围内的刻度值不会出现。 - undefined
这是一个有趣的问题。然而,请不要在评论中提问。请发布一个新问题,解释动态域的问题,并链接到这个答案(这样其他用户就能更好地理解背景)。 - undefined
是的,你说得对。这是链接:https://stackoverflow.com/questions/48112358/hide-axis-tick-values-under-a-certain-range-in-d3-js - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接