dc.js 动态瀑布图

dc.js Dynamic Waterfall Chart

Vistas 在 github 上有一个很好的示例,其中包含如何在 dc.js 中制作瀑布的设置。它使用第二个数据集来实际创建堆叠条形图的底部。但是,如果您在第一个数据集中进行过滤,它将无法正常工作,因为堆叠图表的底部值是固定的。

因此我的问题是是否可以根据此公式计算 d.value,因此不需要第二个数据集 (dummy_data):

Dummy value of current column = previous dummy value + previous real data value

第一列和最后一列的值设置为 0

JSFiddle

代码

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='utf-8'>
  <meta content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' name='viewport'>

  <title>Waterfall Chart with DC.js</title>

  <script src='js/d3.js' type='text/javascript'></script>
  <script src='js/crossfilter.js' type='text/javascript'></script>
  <script src='js/reductio.js' type='text/javascript'></script>
  <script src='js/dc.js' type='text/javascript'></script>
  <link href='css/dc.css' rel='stylesheet' type='text/css'>
</head>
<body>
  <div class='pie-graph span6' id='dc-waterfall-chart'></div>
<script>
  var waterfallChart = dc.barChart("#dc-waterfall-chart");
  var original_data = [];
  var dummy_data = [];

  //creating example data - could easily be any data reading process from sources like CSV or JSON
  original_data.push({item: "x0", value: 10});
  original_data.push({item: "x1", value: 2});
  original_data.push({item: "x2", value: -1});
  original_data.push({item: "x3", value: -3});
  original_data.push({item: "x4", value: 8});

  //creating the dummy data, the invisible columns supporting the waterfall chart. 
  //This is going to be the first stack whereas the real data (original_data) is the 
  //second stack
  dummy_data.push({item: "x0", value: 0});
  dummy_data.push({item: "x1", value: 10});
  dummy_data.push({item: "x2", value: 12});
  dummy_data.push({item: "x3", value: 11});
  dummy_data.push({item: "x4", value: 0});

  //creating crossfilter based off of the real data. Again, you can have your own crossfilter creation process here.
  var ndx = crossfilter(original_data);
  var itemDimension = ndx.dimension(function (d) { return d.item; });
  var reducerValue = reductio().count(true).sum(function(d) { return d.value; }).avg(true); 
  var itemGroup = itemDimension.group();
  var grp = reducerValue(itemGroup);

  // we should also have a similar cross filter on the dummy data
  var ndx_dummy = crossfilter(dummy_data);
  var itemDimension_dummy = ndx_dummy.dimension(function (d) { return d.item; });
  var reducerValue_dummy = reductio().count(true).sum(function(d) { return d.value; }).avg(true); 
  var itemGroup_dummy = itemDimension_dummy.group();
  var dummy_grp = reducerValue_dummy(itemGroup_dummy);

  waterfallChart.width(600)
  .height(400)
  .margins({top: 5, right: 40, bottom: 80, left: 40})
  .dimension(itemDimension)
  .group(dummy_grp)
  .valueAccessor(function (d) { // specific to reductio
    return d.value.sum; 
    })
  .title(function(d){ 
    return (d.key + "  (" + d.value.sum+ ")" );
  })
  .transitionDuration(1000)
  .centerBar(false) 
  .gap(7)                    
  .x(d3.scaleBand())
  .xUnits(dc.units.ordinal)
    .controlsUseVisibility(true)
    .addFilterHandler(function(filters, filter) {return [filter];})
  .elasticY(true)
  .xAxis().tickFormat(function(v) {return v;});

  waterfallChart.stack(grp,"x")

  waterfallChart.on("pretransition",function (chart) {
    //coloring the bars
    chart.selectAll("rect.bar").style("fill", function(d){return "white";});
    chart.selectAll("rect.bar").style("stroke", "#ccc");//change the color to white if you want a clean waterfall without dashed boundaries
    chart.selectAll("rect.bar").style("stroke-dasharray", "1,0,2,0,1");

    // stack._1 is your real data, whereas stack._0 is the dummy data. You want to treat the styling of these stacks differently
    chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("fill", function(d){console.log(d.data.value.sum);if (d.data.value.sum >0) return '#ff7c19'; else return '#7c7c7c';});
    chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke", "white");
    chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke-dasharray", "1");
    // chose the color of deselected bars, but only for the real data.
    chart.selectAll("svg g g.chart-body g.stack._1 rect.deselected").style("fill", function (d) {return '#ccc';});
    chart.selectAll('g.x text').style('fill', '#8e8e8e');
    chart.selectAll('g.y text').style('fill', '#777777');
    chart.selectAll('g.x text').style('font-size', '10.5px');
    chart.selectAll('g.y.axis g.tick line').style("stroke", "#f46542");
    chart.selectAll('.domain').style("stroke","#c6c6c6");
    chart.selectAll('rect.bar').on("contextmenu",function(d){d3.event.preventDefault();});
  });
  dc.renderAll();
</script>
</body>
</html>

我们可以使用 fake group 以基线和最终值所需的方式累积值:

  function waterfall_group(group, endkey, acc) {
    acc = acc || (x => x);
    return {
      all: () => {
        let cumulate = 0;
        let all = group.all().map(({key,value}) => {
          value = acc(value)
            const kv = {
            key,
            value: {
                baseline: cumulate,
              data: value
            }
            };
          cumulate += value;
          return kv;
        });
        return all.concat([{key: endkey, value: {baseline: 0, data: cumulate}}]);
      }
    };
  }

此函数采用最终 "sum total" 条的键和访问函数,此处需要,因为 reductio 将值包装在一个额外的对象中。

它 returns 一组值为 {baseline,data},其中 baseline 是不可见堆栈所需的虚拟值,data 是条形图的值。

一样构建假群
var waterfall_group = waterfall_group(grp, 'x5', x => x.sum);

并将其传递给 .group().stack() 使用访问器来获取子值:

waterfallChart
  .group(waterfall_group, 'baseline', kv => kv.value.baseline)
  .stack(waterfall_group, 'data', kv => kv.value.data)

我还更改了着色代码以获取新的数据格式:

chart.selectAll("svg g g.chart-body g.stack._1 rect.bar")
  .style("fill", function(d){if (d.data.value.data >0) return '#ff7c19'; else return '#7c7c7c';});

为了测试它,我添加了另一个 "category" 字段和一个饼图。请注意,瀑布图可能会进入一些具有负值和零值的奇怪状态(例如单击 "C"),但它们看起来是正确的。

Fork of your fiddle.

请注意,由于最后一项(此处为 x5)是纯合成的,与任何基础数据无关,因此通过单击该项目进行过滤将导致其他图表空白。我不确定如何禁用对某一特定项目的点击。