【问题标题】:How to dynamically render horizontal stacked bar charts in D3如何在 D3 中动态渲染水平堆积条形图
【发布时间】:2021-12-28 19:47:55
【问题描述】:

出于两个原因,我将把它写得有点冗长:

  1. 为了证明我已经努力解决我提出的问题,因为互联网上有太多短暂的东西
  2. D3 很棒,因为它很复杂,很难完全理解。最近的主要版本强调使用数据连接,旨在简化一般更新模式。

因此,这可能是一篇文章,但我真的很想了解我的问题的答案,所以请多多包涵。

上下文

我想创建一个动态堆叠水平条形图,以可视化single transferable voting 流程的不同阶段(准确地说是在苏格兰的地方选举中)。

D3 堆栈

这非常类似于 Bostock 的 Stacked Bar Chart, Horizontal,显示美国各州的人口按年龄组划分。

毫不奇怪,Bostock 使用 D3's stack 生成器,我很失望地发现它将 SVG 矩形组织成跨 Y 轴的分组(svg g 元素)。以下是基于 Bostock 的Stacked Bar Chart, Horizontal 示例的实验:

const data = [
    {month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
    {month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
    {month: "March", apples:  640, bananas:  960, cherries: 640, dates: 400},
    {month: "Apr", apples:  3120, bananas:  1480, cherries: 640, dates: 400}
];

上述数据使用与 Bostock 示例中相同的方法进行旋转,并传递给 StackedBarChart(),我添加了一个转换以导致呈现以下内容:

在上面的示例中,数据绑定不是按月份,而是按水果。

最终状态很好,但动态(过渡)数据更改会很困难。

这是一个有些复杂的领域。我毫不掩饰地承认我不理解 Bostock 对堆栈的使用,取自他上面提到的示例:

// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
  .keys(zDomain)
  .value(([, I], z) => X[I.get(z)])
  .order(order)
  .offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));

嵌套数据连接

也许有人可以进一步阐明上述内容。也许我不了解堆栈(很可能;)。也许如果图表是垂直的而不是水平的,事情会更容易吗?不知道。

我决定放弃 D3 堆栈,转而使用数据连接嵌套数据,有点回归基础。

重复阅读 Bostock 在 general update pattern 上已有近十年历史的帖子以及阅读有关新的 join method 的文章很有帮助,旨在进一步抽象一般更新模式。-*

以下代码是对documentation nested data join example 的改编(只要确保在某处加载了 d3 库):

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

    function doD3() {
        svg
            .selectAll("g")
            .data(generate, (d) => d[0].id.split("_")[0])
            .join("g")
            .attr("transform", (d, i) => "translate(0, "  + i * 30 + ")")
            .selectAll("text")
            .data(d => d)
            .join(enter => enter
                    .append("text").attr("fill", "green"),
                update => update,
                exit => exit.remove())
            .attr("x", (d, i) => i * 50)
            .text(d => d.value);
    }

    function generateRow(rowId, cols) {
        let row = [];

        for(let i = 0; i < cols; i++) {
            row.push({"id": rowId + "_" + i, "value": highRandom()});
        }

        return row;

    }

    const lowRandom = d3.randomInt(3, 15);
    const highRandom = d3.randomInt(1e2, 1e5);

    function generate() {
        const cols = lowRandom();
        const rows = lowRandom();

        let matrix = [];

        for(let i = 0; i < rows; i++) {
            matrix.push(generateRow(i, cols));
        }

        return matrix;


    }

    window.setInterval(doD3, 2000);

可能是原始的,但旨在演示与关键功能的成功数据绑定。 generategenerateRow 函数一起生成具有 id 和 value 的对象的随机矩阵。

每两秒调用一次。所有节点(文本)都在输入时呈现。我做错了什么?

感谢您的阅读和新年快乐:))

-* 不幸的是,我无法发布到 JSFiddle 的链接,因为支持的最新 D3 版本是版本 5,而我(可能不必要地)使用的是最新版本 7。Bostock 已经开始使用一个允许实验的平台,称为 Observable 但是我觉得很混乱。

【问题讨论】:

  • 您能解释一下您希望过渡如何工作吗?
  • @Dan,感谢您的回复。我希望每个水平条从宽度 = 0 过渡到 x_scale(d.total_value)。此tota_value 是其堆栈值的总和。因此,要为一个水平条设置动画,您必须以类似多米诺骨牌的方式应用它(可能无法使用非线性函数)。我不认为使用堆栈是要走的路。我想成功地将关键函数应用于嵌套数据。我需要在“行”级别绑定数据: .selectAll("g") .data(generate, (d) => d[0].id.split("_")[0]) 但是还有另一个加入。这让我很困惑。

标签: d3.js


【解决方案1】:

这是一个使用水果数据集的示例。该图表是动画的,以便一次显示一种水果的条形图。我通过为每种水果的条形图提供不同的过渡来做到这一点delay

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="chart"></div>

  <script>
    // set up

    const margin = { top: 10, right: 10, bottom: 20, left: 40 };

    const width = 300 - margin.left - margin.right;
    const height = 200 - margin.top - margin.bottom;

    const svg = d3.select('#chart')
      .append('svg')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
      .append('g')
        .attr('transform', `translate(${margin.left},${margin.top})`);

    // data

    const data = [
      {month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
      {month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
      {month: "March", apples:  640, bananas:  960, cherries: 640, dates: 400},
      {month: "Apr", apples:  3120, bananas:  1480, cherries: 640, dates: 400}
    ];

    const fruit = Object.keys(data[0]).filter(d => d != "month");
    const months = data.map(d => d.month);

    const stackedData = d3.stack()
        .keys(fruit)(data);

    const xMax = d3.max(stackedData[stackedData.length - 1], d => d[1]);

    // scales


    const x = d3.scaleLinear()
        .domain([0, xMax]).nice()
        .range([0, width]);

    const y = d3.scaleBand()
        .domain(months)
        .range([0, height])
        .padding(0.25);

    const color = d3.scaleOrdinal()
        .domain(fruit)
        .range(d3.schemeTableau10);

    // axes

    const xAxis = d3.axisBottom(x).ticks(5, '~s');
    const yAxis = d3.axisLeft(y);

    svg.append('g')
        .attr('transform', `translate(0,${height})`)
        .call(xAxis)
        .call(g => g.select('.domain').remove());

    svg.append("g")
        .call(yAxis)
        .call(g => g.select('.domain').remove());

    // draw bars

    // create one group for each fruit
    const layers = svg.append('g')
      .selectAll('g')
      .data(stackedData)
      .join('g')
        .attr('fill', d => color(d.key));

    // transition for bars
    const duration = 1000;
    const t = d3.transition()
        .duration(duration)
        .ease(d3.easeLinear);

    layers.each(function(_, i) {
      // this refers to the group for a given fruit
      d3.select(this)
        .selectAll('rect')
        .data(d => d)
        .join('rect')
          .attr('x', d => x(d[0]))
          .attr('y', d => y(d.data.month))
          .attr('height', y.bandwidth())
        .transition(t)
          // i is the index of this fruit.
          // this will give the bars for each fruit a different delay
          // so that the fruits will be revealed one at a time.
          // using .each() instead of a normal data join is needed
          // so that we have access to what fruit each bar belongs to.
          .delay(i * duration)
          .attr('width', d => x(d[1]) - x(d[0]));
    });


  </script>
</body>

</html>

【讨论】:

  • 非常感谢您花时间帮助我。这非常有用,我会花一些时间研究和试验。
猜你喜欢
  • 2020-04-10
  • 1970-01-01
  • 2014-02-28
  • 1970-01-01
  • 1970-01-01
  • 2013-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多