转换宽度为负值的条形图

Transitioning a bar chart with negative values for the width

我正在使用 d3 创建水平条形图。我在启动时使用动画 "grow" 图表。这是代码。

// Create the svg element
d3.select("#chart-area")
    .append("svg")
    .attr("height", 800)
    .attr("width", 800);

    .data(dataValues) // This data is previously prepared
    .enter().append("rect")
    .style("fill", "blue")

    .attr("x", function () { return xScale(0); }) // xScale is defined earlier
    .attr("y", function (d) { return yScale(d); }) // yScale is defined earlier

    .attr("height", yScale.bandwidth()) // yScale is defined earlier

    // Initial value of "width" (before animation)
    .attr("width", 0)


    // Start of animation transition
    .transition()
    .duration(5000) // 5 seconds

    .ease (d3.easeLinear); 

    // Final value of "width" (after animation)
    .attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })

上面的代码可以正常工作,并且线条会按预期增长,在 5 秒内从 0 到任意宽度。

现在,如果我们将缓动线更改为以下内容

    // This line changed
    .ease (d3.easeElasticIn); 

然后,在达到最终正值之前,ease 会尝试将宽度变为负值。 As you can see here, d3.easeElasticIn returns 随着时间的推移负值,然后回到正值,导致动画中某些点的宽度为负值。因此条形图无法正确呈现(因为 SVG 规范规定如果宽度为负,则使用 0)

我尝试了所有解决方案来让条形负增长然后退出。但找不到任何。我该如何解决这个问题?

谢谢。

如您所知,在您的特定代码中使用 d3.easeElasticIn 将为矩形的宽度创建负值,这是不允许的。

这个基本演示重现了这个问题,控制台(浏览器的控制台,而不是代码段的控制台)填充了错误消息,如下所示:

Error: Invalid negative value for attribute width="-85.90933910798789"

看看:

const svg = d3.select("svg");

const margin = 50;

const line = svg.append("line")
  .attr("x1", margin)
  .attr("x2", margin)
  .attr("y1", 0)
  .attr("y2", 150)
  .style("stroke", "black")

const data = d3.range(10).map(function(d) {
  return {
    y: "bar" + d,
    x: Math.random()
  }
});

const yScale = d3.scaleBand()
  .domain(data.map(function(d) {
    return d.y
  }))
  .range([0, 150])
  .padding(0.2);

const xScale = d3.scaleLinear()
  .range([margin, 300]);

const bars = svg.selectAll(null)
  .data(data)
  .enter()
  .append("rect")
  .attr("x", margin)
  .attr("width", 0)
  .style("fill", "steelblue")
  .attr("y", function(d) {
    return yScale(d.y)
  })
  .attr("height", yScale.bandwidth())
  .transition()
  .duration(2000)
  .ease(d3.easeElasticIn)
  .attr("width", function(d) {
    return xScale(d.x) - margin
  })
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

那么,解决方案是什么?

其中之一是在生成负值时捕获这些负值,然后将矩形向左移动(使用 x 属性)并将这些负数转换为正数。

为此,我们必须在过渡选择中使用 attrTween 而不是 attr

像这样:

.attrTween("width", function(d) {
    return function(t){
        return Math.abs(xScale(d.x) * t);
    };
})
.attrTween("x", function(d) {
    return function(t){
        return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
    };
})

在上面的代码片段中,margin 只是我创建的边距,因此您可以看到轴左侧的条形。

这是演示:

const svg = d3.select("svg");

const margin = 100;

const line = svg.append("line")
  .attr("x1", margin)
  .attr("x2", margin)
  .attr("y1", 0)
  .attr("y2", 150)
  .style("stroke", "black")

const data = d3.range(10).map(function(d) {
  return {
    y: "bar" + d,
    x: Math.random()
  }
});

const yScale = d3.scaleBand()
  .domain(data.map(function(d) {
    return d.y
  }))
  .range([0, 150])
  .padding(0.2);

const xScale = d3.scaleLinear()
  .range([0, 300 - margin]);

const bars = svg.selectAll(null)
  .data(data)
  .enter()
  .append("rect")
  .attr("x", margin)
  .attr("width", 0)
  .style("fill", "steelblue")
  .attr("y", function(d) {
    return yScale(d.y)
  })
  .attr("height", yScale.bandwidth())
  .transition()
  .duration(2000)
  .ease(d3.easeElasticIn)
  .attrTween("width", function(d) {
    return function(t) {
      return Math.abs(xScale(d.x) * t);
    };
  })
  .attrTween("x", function(d) {
    return function(t) {
      return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
    };
  })
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>