【问题标题】:How to cut y axis to make the chart look better?如何切割 y 轴以使图表看起来更好?
【发布时间】:2018-01-05 04:58:39
【问题描述】:

我想要做的是使用 D3.js (v4) 创建一个条形图,它将显示 2 或 3 个值有很大差异的条形图。

如下图所示,黄色条的值为 1596.6,而绿色条的值为 177.2。因此,为了以优雅的方式显示图表,决定将 y 轴切割为某个值,该值将接近绿色条的值并继续接近黄色条的值。

图片上y轴在500之后被切割,在1500之后继续。

如何使用 D3.js 做到这一点?

【问题讨论】:

    标签: d3.js data-visualization


    【解决方案1】:

    从技术上讲,您想要的是断开的 y 轴

    只要您明确告诉用户您的 y 轴已损坏,这是数据可视化中的有效表示(尽管有些人对此不满意)。有几种方法可以直观地表示它,如下所示:

    但是,不要忘记在图表的文本或图例中说明这一点。除此之外,请记住“为了以优雅的方式显示图表”从不成为这里的目标:我们不会让图表变得优雅(但如果它们是很好的),我们制作图表以澄清用户的信息或模式。而且,在一些非常有限的情况下,打破 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 刻度的范围和域中添加中点。所以,在上面的 sn-p 中,这是 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 轴已损坏。在上面的 sn-p 中,我在 y 轴上的数字 15 和数字 700 之间放置了符号(参见我的答案中的第一个图)。顺便说一句,值得注意的是,在您分享的图中,其作者没有在 y 轴上放置任何符号,这不是一个好习惯。

    【讨论】:

    • 谢谢,杰拉尔多!它就像一种魅力:) 不过我还有一个问题。您使用 tickValues 设置 y 轴刻度,只要您事先知道您的值将是什么就可以了。但是,如果您的值是动态的(例如,您从数据库中获取它们)并且您无法设置您的 tickValues。你知道如何克服在突破边缘发生的混乱吗?
    • 我假设不显示介于较低柱的最大值和较高柱的最小值之间的刻度值将解决该问题,但我不知道如何使刻度值在某些范围不会显示。
    • 这是一个有趣的问题。但是,请不要在 cmets 中提问。只需发布一个 问题,解释动态域的问题并链接此答案(以便其他用户可以获得更好的上下文)。
    • 是的,你是对的。这是:stackoverflow.com/questions/48112358/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-05
    相关资源
    最近更新 更多