d3.js 带有可切换系列的堆叠条

Posted

技术标签:

【中文标题】d3.js 带有可切换系列的堆叠条【英文标题】:d3.js stacked bar with toggleable series 【发布时间】:2017-08-24 03:21:40 【问题描述】:

这一次我试图创建一个带有可切换系列的堆叠条 - 基于 Mike Bostock 的示例(再次感谢 Mike!)我已经成功地使其具有响应性和可缩放性,并且通过图例的可切换系列是最后一个剩下的东西。

我创建了图例项,并使用键应用了正确的颜色:

var legendItem = d3.select(".legend")
  .selectAll("li")
  .data(keys)
  .enter()
  .append("li")
  .on('click', function(d) 
    keys.forEach(function(c) 
      if (c != d) tKeys.push(c)
    );
    fKeys = tKeys;
    tKeys = [];
    redraw();
  );

legendItem
  .append("span")
  .attr("class", "color-square")
  .style("color", function(d) 
    return colorScale5(d);
  );

legendItem
  .append("span")
  .text(function(d) 
    return (d)
  );

基于结构,为了创建可切换项目,我得出的结论是,我必须能够以某种方式从键和数据集切换它 - 还是有其他方法可以做到这一点?我已经设法从键中删除了一个特定的键,但不是从数据集中,我不知道如何正确映射它。

第二个问题是我想不出切换键的方法,而只是将其移除。这是原始数据集:

var data = [
  "country": "Greece",
  "Vodafone": 57,
  "Wind": 12,
  "Cosmote": 20
, 
  "country": "Italy",
  "Vodafone": 40,
  "Wind": 24,
  "Cosmote": 35
, 
  "country": "France",
  "Vodafone": 22,
  "Wind": 9,
  "Cosmote": 9
]

在从嵌套数据集提供的值中,我可以为每个对象附加一个名为“启用”的键,并且可以轻松过滤数据集,但无法弄清楚如何附加键以帮助过滤过程。

edit3从问题中删除了无用的信息:

这是一个有效的小提琴: https://jsfiddle.net/fgseaxoy/2/

【问题讨论】:

...如果有人能指出我正确的方向,那就太好了! 您还在寻找答案吗? @blackmiaool,如果有人可以提供更优雅/更清晰的解决方案(无意冒犯@SergGr),那将非常受欢迎 【参考方案1】:

有几处需要修复:

首先,javascript 通过引用分配对象。这意味着之后

var fKeys = keys;

fKeyskeys 都指向同一个数组。这不是你想要的。你想要复制一些东西,例如:

var fKeys = keys.slice();

那么你的legendItem“点击”处理程序是错误的,因为它并没有真正切换选定的项目。你想要的是类似的东西

        .on('click', function (keyToToggle) 
            // Go through both keys and fKeys to find out proper
            // position to insert keyToToggle if it is to be inserted
            var i, j;
            for (i = 0, j = 0; i < keys.length; i++) 
                // If we hit the end of fKeys, keyToToggle
                // should be last
                if (j >= fKeys.length) 
                    fKeys.push(keyToToggle);
                    break;
                
                // if we found keyToToggle in fKeys - remove it
                if (fKeys[j] == keyToToggle) 
                    // remove it
                    fKeys.splice(j, 1);
                    break;
                

                // we found keyToToggle in the original collection
                // AND it was not found at fKeys[j]. It means
                // it should be inserted to fKeys at position "j"
                if (keys[i] == keyToToggle) 
                    // add it
                    fKeys.splice(j, 0, keyToToggle);
                    break;
                

                if (keys[i] == fKeys[j])
                    j++;
            

            redraw();
        );

接下来,当您调用data 以获取stackedBars 时,您要提供key 功能。这很重要,因为否则数据将被索引绑定,并且总是会删除最后一条数据。

    var stackedData = d3.stack().keys(fKeys)(dataset);
    var stackedBars = g
            .selectAll(".d3-group")
            .data(stackedData , function (__data__, i, group) 
                return __data__.key;
            );

最后,当您更新 '.d3-rect' 时,您想再次调用 data,因为子节点缓存上次绘制的数据,并且您想用新数据覆盖它

        stackedBars.selectAll('.d3-rect')
                .data(function (d) 
                    return d; // force override with updated parent's data
                )
                .attr("x", function (d) 
                    return xz(d.data.country);
                )
                ...

如果没有这样的调用,隐藏第一条数据(“Vodafone”)不会将其他堆叠的数据向下移动。

还有太多的全局变量(即太少的vars)和一些不必要的变量。

更新(自动缩放 y)

如果您还希望更新您的 Y 比例,请将 var stackedData 移到 redraw 的代码中更高的位置,以便您可以使用它来计算您的 y,如下所示

    var stackedData = d3.stack().keys(fKeys)(dataset);
    var autoScaleY = true; // scale Y according to selected data or always use original range
    var stackedDataForMax;
    if (autoScaleY && stackedData.length > 0) 
        // only selected data
        stackedDataForMax = stackedData;
    
    else 
        // full range
        stackedDataForMax = d3.stack().keys(keys)(dataset);
    
    var maxDataY = 1.2 * d3.max(stackedDataForMax.map(function (d) 
                return d3.max(d, function (innerD) 
                    return innerD[1];
                );
            ));
    y.domain([0, maxDataY]).rangeRound([height, 0]);

你可以在the fork of your original fiddle找到完整的代码。

【讨论】:

您好,抱歉这么久才回复。谢谢你的努力!我不得不说,必须有一种更优雅的方式来做到这一点。您提供了一些很棒的想法——比如复制原始密钥数据集并比较我们是否想要添加新的数据集,但我仍然认为这太费力了。另外,y尺度应该适应变化,所以我猜数据集也需要一些改变。 @scooterlord,我不知道自动缩放 Y 是您的目标之一,但它很容易做到(请参阅答案中的更新和更新的小提琴)。至于“更优雅的方式”,您可能是对的,我只是从您的代码开始并修复了错误,而不是从头开始创建新的东西。 @scooterlord,我不是 D3 专家。我只是相当擅长 JS 和调试。因此,如果某个真正的 D3 专家提出了一个更好的解决方案,我完全可以接受。不过,出于好奇,您认为“理想解决方案”在哪些方面会更好? @scooterlord,该代码的目标是修改fKeys,以便(未删除)键的顺序与原始keys 中的相同。这个想法是我有两个“同步”索引ij 应该指向keysfKeys 中的相同值。更具体地说,j 指向fKeys 中的一个位置,现在或应该插入等于keys[i] 的值。停止算法有两个原因:1)我们在keys 中找到了keyToToggle - 简单的一个。 2) keyToTogglekeys 的最后一个元素之一,它(以及它之后的所有元素)都被删除了。 然后j 将在i 到达keyToToggle 位于keys 的位置之前到达fKeys 的末尾,因此尝试执行(fKeys[j] == keyToToggle) 检查将可能不正确。但是在这种情况下,很明显我们 a) 需要将 keyToToggle 添加到 fKeys 并且 b) 需要将其添加到最后一个位置。示例:keys = [Vodafone, Wind, Cosmote]fKeys = [Vodafone]keyToToggle = Cosmote。在循环的第一次迭代keys[i] === fKeys[j] 所以j 将被递增,我们已经超出了fKeys 的末尾。 @blackmiaool 有另一种使用位掩码的有趣方法【参考方案2】:

SergGr 的代码运行良好,但有些部分可以更简洁。

点击

var fKeys = keys.slice();

//a helper object to record the state of keys 
var fKeyReference = fKeys.map(function () 
    return true; //used to indicate if the corresponding key is active
);

function getActiveKeys(reference) 
    return reference.map(function (state, index) 
        if (state) 
            return keys[index]; //just keep keys whoes state is true
        
        return false; //return false to be filered
    ).filter(function (name) 
        return name
    );


...
.on('click', function (d) 
    if (fKeys.length === 1 && fKeys[0] === d) 
        return;
    

    var index = keys.indexOf(d);
    fKeyReference[index] = !fKeyReference[index]; // toggle state of fKeyReference
    fKeys = getActiveKeys(fKeyReference);
    redraw();
);

.stack()

g.selectAll(".d3-group").remove();//remove all groups and draw them all again
stackedBars = g
    .selectAll(".d3-group")
    .data(d3.stack().keys(fKeys)(dataset));

更新坐标轴 (y.domain)

y.domain([
    0,
    1.2 * d3.max(dataset, function (d) 
        return fKeys.reduce(function (pre, key) //calculate the sum of values of fKeys
            return pre + d[key];
        , 0);
    )
]);

最后,jsfiddle

【讨论】:

您的代码不允许切换多个系列。例如,如果您在“Vodafone”上单击一次,在“Wind”上单击一次,而不是将它们都隐藏起来,第二次单击会切换第一次。 @scooterlord 好的,我会解决的 如果可能的话,请多分析一下你的cmets;例如为什么你必须再次复制键数组? @scooterlord 好的。由于您的第一条评论,这是一个错误。我会删除它。 @scooterlord 现在可以在 IE11 中使用。它不起作用的原因是我在其中使用了es6。关于它的元数据有很多discussions。 SO use es6有很多答案,如果你想让它们在IE11中工作,你可以使用babel

以上是关于d3.js 带有可切换系列的堆叠条的主要内容,如果未能解决你的问题,请参考以下文章

D3.js 入门系列 — 11 入门总结

D3.js 入门系列 — 0 简介和安装

d3.js 将图例添加到多线系列图表

D3.js系列——

D3.js系列 --- 初识

D3.js 入门系列 --- 8 对话操作(事件)