D3.js - 如何将一般更新模式添加到堆积条形图中

use*_*338 2 javascript d3.js

我有一个项目,尝试创建堆积条形图。其要求之一是图表应该能够更新。我已经使用通用更新模式来完成此任务。但我无法让代码工作。

当图表更新时,新生成的图表从底部进入。所以我猜这意味着代码永远不会执行模式的更新部分,并且始终认为更新的数据是新数据。

我在代码片段中添加了 3 个不同的数据数组,为我面临的问题提供了一个很好的例子。前两个数组包含相同的键,应该更新图表。最后一个数组包含完全不同的键,这里应该输入图表而不是更新。

我还注意到,当新数据添加到图表中时。旧数据永远不会被删除。因此更新模式的退出状态也不会被触发。

我知道更新模式的数据函数中应该定义一个键.data(stackData, d => {return d.key;})。但我似乎不知道应该输入什么键。

我已从原始代码中删除了所需的尽可能多的代码,以便使其适用于代码片段。需要代码片段中包含的所有代码才能使其正常工作。

this.width = 400;
this.height = 200;
var margin = {
  top: 20,
  right: 20,
  bottom: 30,
  left: 40
}

this.index = 0;

this.svg = d3
  .select(".canvas")
  .classed("svg-container", true)
  .append("svg")
  .attr("class", "chart")
  .attr(
    "viewBox",
    `0 0 ${this.width} ${this.height}`
  )
  .attr("preserveAspectRatio", "xMinYMin meet")
  .classed("svg-content-responsive", true)
  .append("g");

const scale = [0, 1200];

// set the scales
this.xScale = d3
  .scaleBand()
  .range([0, width])
  .padding(0.3);

this.yScale = d3.scaleLinear().range([this.height, 0]);

this.svg.append("g").attr("class", "bars");

const update = data => {
  const scale = [0, 1200];

  // Update scales.
  this.xScale.domain(data.map(d => d.key));
  this.yScale.domain([scale[0], scale[1]]);

  const subgroups = ["home", "work", "public"];

  var color = d3
    .scaleOrdinal()
    .domain(subgroups)
    .range(["#206BF3", "#171D2C", "#8B0000"]);

  var stackData = d3.stack().keys(subgroups)(data);

  // Set up transition.
  const dur = 1000;
  const t = d3.transition().duration(dur);

  this.svg
    .append("g")
    .selectAll("g")
    .data(stackData, d => {
      return d.key;
    })
    .join(
      enter => {
        enter
          .append("g")
          .attr("fill", d => color(d.key))
          .selectAll("rect")
          .data(d => {
            return d;
          })
          .join(
            enter => {
              enter
                .append("rect")
                .attr("class", "bar")
                .attr("x", d => {
                  return this.xScale(d.data.key);
                })
                .attr("y", () => {
                  return this.yScale(0);
                })
                .attr("height", () => {
                  return this.height - this.yScale(0);
                })
                .attr("width", this.xScale.bandwidth())
                .transition(t)
                .delay((d, i) => i * 20)
                .attr("y", d => {
                  return this.yScale(d[1]);
                })
                .attr("height", d => {
                  return this.yScale(d[0]) - this.yScale(d[1]);
                });
            },

            update => {
              update
                .transition(t)
                .delay((d, i) => i * 20)
                .attr("x", d => this.xScale(d.key))
                .attr("y", d => {
                  return this.yScale(d[1]);
                })
                .attr("height", d => {
                  return this.yScale(d[0]) - this.yScale(d[1]);
                });
            },

            exit => {
              exit
                .transition()
                .duration(dur / 2)
                .style("fill-opacity", 0)
                .remove();
            }
          );
      },

      update => {
        update
          .transition(t)
          .delay((d, i) => i * 20)
          .attr("x", d => this.xScale(d.key))
          .attr("y", d => {
            return this.yScale(d[1]);
          })
          .attr("height", d => {
            return this.yScale(d[0]) - this.yScale(d[1]);
          });
      },

      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    );
};

const data = [
  [{
      key: "Jan",
      home: 371,
      work: 335,
      public: 300
    },
    {
      key: "Feb",
      home: 343,
      work: 437,
      public: 228
    },
    {
      key: "Mrt",
      home: 359,
      work: 261,
      public: 202
    },
    {
      key: "Apr",
      home: 274,
      work: 217,
      public: 482
    },
    {
      key: "Mei",
      home: 442,
      work: 314,
      public: 477
    },
    {
      key: "Jun",
      home: 464,
      work: 261,
      public: 278
    },
    {
      key: "Jul",
      home: 343,
      work: 244,
      public: 396
    },
    {
      key: "Aug",
      home: 231,
      work: 406,
      public: 338
    },
    {
      key: "Sep",
      home: 380,
      work: 382,
      public: 366
    },
    {
      key: "Okt",
      home: 391,
      work: 408,
      public: 455
    },
    {
      key: "Nov",
      home: 419,
      work: 261,
      public: 226
    },
    {
      key: "Dec",
      home: 217,
      work: 453,
      public: 335
    }
  ],
  [{
      key: "Jan",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "Feb",
      home: 428,
      work: 355,
      public: 216
    },
    {
      key: "Mrt",
      home: 217,
      work: 493,
      public: 280
    },
    {
      key: "Apr",
      home: 304,
      work: 283,
      public: 454
    },
    {
      key: "Mei",
      home: 397,
      work: 406,
      public: 289
    },
    {
      key: "Jun",
      home: 242,
      work: 239,
      public: 232
    },
    {
      key: "Jul",
      home: 327,
      work: 453,
      public: 264
    },
    {
      key: "Aug",
      home: 242,
      work: 240,
      public: 414
    },
    {
      key: "Sep",
      home: 495,
      work: 382,
      public: 368
    },
    {
      key: "Okt",
      home: 285,
      work: 471,
      public: 364
    },
    {
      key: "Nov",
      home: 315,
      work: 421,
      public: 482
    },
    {
      key: "Dec",
      home: 214,
      work: 284,
      public: 297
    }
  ],
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 432,
      public: 324
    },
    {
      key: "3",
      home: 324,
      work: 124,
      public: 432
    },
    {
      key: "4",
      home: 425,
      work: 353,
      public: 532
    }
  ]
];

update(data[this.index]);

const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
  if (this.index < 2) this.index += 1;
  else this.index = 0;
  update(data[this.index]);
});
Run Code Online (Sandbox Code Playgroud)
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
Run Code Online (Sandbox Code Playgroud)

Ste*_*eve 6

这里至少发生了一些事情。正如迈克尔·罗文斯基 (Michael Rovinsky) 所指出的,您将为g每次更新添加一个包含所有酒吧的新组。由于它是一个新组,因此没有绑定任何数据,这将为每个调用创建新的条形。

跟踪群组

相反,您应该只创建一次该组,并且可能将其分配给一个变量以使其更易于使用。我将使用该this.svg.append("g").attr("class", "bars");组,因为它似乎没有用于其他任何用途:

var bars = this.svg.append("g").attr("class", "bars");

然后在您的更新函数中更改:

this.svg
  .append("g")
  .selectAll("g")
  // ...
Run Code Online (Sandbox Code Playgroud)

bars
  .selectAll("g")
Run Code Online (Sandbox Code Playgroud)

返回连接选择的值

当您不返回、和选择时,您的调用join不太标准。当你有一个带有如下主体的箭头函数时:enterupdateexit

enter => {
  enter.append('g') //...
}
Run Code Online (Sandbox Code Playgroud)

它实际上并没有返回输入选择,它应该是

enter => {
  return enter.append('g') //...
}
Run Code Online (Sandbox Code Playgroud)

或者

enter => enter.append('g') //...
Run Code Online (Sandbox Code Playgroud)

虽然从技术上讲您不需要返回enterupdate选择,但这是使用它们的预期方式,并且当您稍后尝试使用组合enter和选择时(我们稍后将这样做),确实会导致问题。update

了解更新过程

您实现嵌套连接的方式表明您不太了解连接的工作原理。当您enter追加新元素时,这些元素没有任何绑定到它们的数据,因此当您使用后续的 时join,嵌套的updateandexit函数将永远不会运行。

enter您还会注意到,您在和中重复了许多相同的代码update

我认为您打算做的事情的更标准方法是在必要时添加新条形,然后使用新条形和旧条形(由join条形段返回并更新它们)。

它看起来像这样:

bars.selectAll('g').data(data).join(
    enter => enter.append('g'),
    update => update, // don't do anything with only the update selection
    exit => exit.remove()
  ) // now we're selecting all of the enter and update g's
   .selectAll('rect')
   .data(d => d).join(
      enter => enter.append('rect'),
      update => update,
      exit => exit.remove()
   ) // now we're selecting all of the enter and update rects
     .transition(t)
     // ...
Run Code Online (Sandbox Code Playgroud)

设置按键

如果查看数据的格式,您会发现 stackData 始终是一个长度为 3 的数组,每个条目都有一个数组。这些嵌套数组根据列数而变化,并且应该在该级别定义键:

    //... group selection
    .selectAll('rect')
    .data(d => d, d => d.data.key)
    //...
Run Code Online (Sandbox Code Playgroud)

修复错误

.attr("x", d => this.xScale(d.key))最后,当您更新应该使用的酒吧位置时,有一些代码d.data.key

全部一起

我整合了大部分冗余的输入和更新代码,并删除了一些无关的代码。总而言之,它看起来像:

this.svg
  .append("g")
  .selectAll("g")
  // ...
Run Code Online (Sandbox Code Playgroud)
bars
  .selectAll("g")
Run Code Online (Sandbox Code Playgroud)