dc.js 使用三列的最小最大值创建数据表

dc.js create dataTable with min max avg values for three columns

正在尝试创建一个 d3/dc/xfilter 数据表,其中包含示例数据中 3 列的最小值、最大值和平均值。苦苦挣扎了几个小时,但无法理解如何将 reduceAdd、reduceRemove、reduceInitial 函数集成到 dataTable 中以创建三个必要的行。

所需的输出将如下所示:

------------------------------------------
|  Value   |  Cars  |  Bikes  |  Trucks  |
------------------------------------------
|    Min   |   125  |   310   |    189   |
------------------------------------------
|    Max   |   230  |   445   |    290   |
------------------------------------------
|    Avg   |   178  |   385   |    245   |
------------------------------------------

也看不到如何添加第一(标签)列。我知道 reduceInitial 可以 return 一个数组(例如 ['min', 'max', 'avg'])但是如何从中引用标签?

var myCSV = [ 
{"shift":"1","date":"01/01/2016/08/00/00","car":"178","truck":"255","bike":"317","moto":"237"},
{"shift":"2","date":"01/01/2016/17/00/00","car":"125","truck":"189","bike":"445","moto":"273"},
{"shift":"3","date":"02/01/2016/08/00/00","car":"140","truck":"219","bike":"328","moto":"412"},
{"shift":"4","date":"02/01/2016/17/00/00","car":"222","truck":"290","bike":"432","moto":"378"},
{"shift":"5","date":"03/01/2016/08/00/00","car":"200","truck":"250","bike":"420","moto":"319"},
{"shift":"6","date":"03/01/2016/17/00/00","car":"230","truck":"220","bike":"310","moto":"413"},
{"shift":"7","date":"04/01/2016/08/00/00","car":"155","truck":"177","bike":"377","moto":"180"},
{"shift":"8","date":"04/01/2016/17/00/00","car":"179","truck":"203","bike":"405","moto":"222"},
{"shift":"9","date":"05/01/2016/08/00/00","car":"208","truck":"185","bike":"360","moto":"195"},
{"shift":"10","date":"05/01/2016/17/00/00","car":"150","truck":"290","bike":"315","moto":"280"},
{"shift":"11","date":"06/01/2016/08/00/00","car":"200","truck":"220","bike":"350","moto":"205"},
{"shift":"12","date":"06/01/2016/17/00/00","car":"230","truck":"170","bike":"390","moto":"400"},
];


dataTable = dc.dataTable('#dataTable');
lc1 = dc.lineChart("#line1");
lc2 = dc.lineChart("#line2");
lc3 = dc.lineChart("#line3");

var dateFormat = d3.time.format("%d/%m/%Y/%H/%M/%S");

myCSV.forEach(function (d) {
 d.date = dateFormat.parse(d.date);
});

myCSV.forEach(function (d) {
 d['car'] = +d['car'];
 d['bike'] = +d['bike'];
 d['moto'] = +d['moto'];
});

//console.log(myCSV);

var facts = crossfilter(myCSV);
var dateDim = facts.dimension(function (d) {return d.date});

var carDim = facts.dimension(function (d) {return d['car']});
var dgCar = dateDim.group().reduceSum(function (d) {return d['car']});

var bikeDim = facts.dimension(function (d) {return d['bike']});
var dgBike = dateDim.group().reduceSum(function (d) {return d['bike']});

var motoDim = facts.dimension(function (d) {return d['moto']});
var dgMoto = dateDim.group().reduceSum(function (d) {return d['moto']});

var minDate = new Date ("2016-01-01T08:00:00.000Z");
var maxDate = new Date ("2016-01-03T17:00:00.000Z"); 

var maxY = d3.max(myCSV, function(d) {return d['car']});

function reduceAdd(i,d){ return i+1; }
function reduceRemove(i,d){return i-1; }
function reduceInitial(){ return ['min','max','avg'];}


dataTable
  .width(jsTablWidth)
  .height(400)
  .dimension(dateDim)
  .group( function(d){return '';} )
  .columns([
    {
      label: 'Value',
      format: function(d) { return dateGroup1.reduce(reduceAdd,reduceRemove,reduceInital); }
    },
    {
      label: tSel1.replace(/_/g, " "),
      format: function(d) { return //avg cars ; }
    },
    {
      label: tSel2.replace(/_/g, " "),
      format: function(d) { return //avg bikes ; }
    },
    {
      label: tSel3.replace(/_/g, " "),
      format: function(d) { return //avg moto; }
    }
  ]);


dc.renderAll();
dc.redrawAll();
svg{height:280px;}
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.1/crossfilter.min.js"></script>
<script src="http://dc-js.github.io/dc.js/js/dc.js"></script>
<link href="http://dc-js.github.io/dc.js/css/dc.css" rel="stylesheet"/>

<svg id="dataTable"></svg>
<svg id="line1"></svg>
<svg id="line2"></svg>
<svg id="line3"></svg>

好的,希望您可以在对角线上调换 table,将交通方式作为行而不是列。如果不弄清楚那部分,这个解决方案已经很古怪了。

除了跟踪所有值之外,确实没有办法计算最小值和最大值。所以我们将使用 complex reductions example 中的减少量。这些实际上根本不减少,而是维护过滤行的排序数组。

我们需要一个唯一的键来保持排序后的数组(以便我们删除正确的行。幸运的是你在 shift 字段中有它。

所以这里是那些函数,或者更确切地说是在给定唯一键访问器的情况下生成缩减器的函数。

  function groupArrayAdd(keyfn) {
      var bisect = d3.bisector(keyfn);
      return function(elements, item) {
          var pos = bisect.right(elements, keyfn(item));
          elements.splice(pos, 0, item);
          return elements;
      };
  }
  function groupArrayRemove(keyfn) {
      var bisect = d3.bisector(keyfn);
      return function(elements, item) {
          var pos = bisect.left(elements, keyfn(item));
          if(keyfn(elements[pos])===keyfn(item))
              elements.splice(pos, 1);
          return elements;
      };
  }
  function groupArrayInit() {
      return [];
  }

因为这些保留了对整行的引用,所以我们只需要一组;我们将在计算以下指标时使用更具体的访问器。

这里我们想要 crossfilter.groupAll,它可以将所有内容减少到一个 bin。这是因为行没有按任何键分区;每一行都有助于所有交通方式:

var filteredRows = facts.groupAll().reduce(
  groupArrayAdd(dc.pluck('shift')),
  groupArrayRemove(dc.pluck('shift')),
  groupArrayInit
);

最荒谬的部分来了。我们将创建您见过的最虚假的维度 object。重要的是它是一个 object 和一个 .bottom() 方法动态计算每一行:

var fakeDim = {
  bottom: function() {
    return [
      {key: 'Car', value: filteredRows.value(), acc: dc.pluck('car')},
      {key: 'Truck', value: filteredRows.value(), acc: dc.pluck('car')},
      {key: 'Bike', value: filteredRows.value(), acc: dc.pluck('bike')},
      {key: 'Moto', value: filteredRows.value(), acc: dc.pluck('moto')}
    ];
  }
};

除了,等等,它看起来根本没有进行任何计算,只是获取值?那有什么奇怪的 acc?

好吧,我们正在生成生成 table 行所需的源数据,我们将使用下面的 format 访问器来实际计算所有内容。我们将为 "label column" 使用 key,我们将保留 value 成员中的原始行;我们将提供一个访问器 acc 来计算指标。

数据 table 定义如下所示:

dataTable
  .width(400)
  .height(400)
  .dimension(fakeDim)
  .group( function(d){return '';} )
  .columns([
    {
      label: 'Value',
      format: function(d) { 
        return d.key;
      }
    },
    {
      label: 'Min',
      format: function(d) {
        return d3.min(d.value, d.acc);
      }
    },
    {
      label: 'Max',
      format: function(d) {
        return d3.max(d.value, d.acc);
      }
    },
    {
      label: 'Avg',
      format: function(d) {
        return d3.mean(d.value, d.acc);
      }
    }
  ]);

这里是最终计算所有指标的地方。我们将拥有所有可用的行,并且每个 table 行都有一个访问器。 d3-array 具有用于计算数组的最小值、最大值和平均值的便捷函数。繁荣,完成。

我在这个 fiddle 中放入了一个堆叠图表进行测试。 (我知道堆叠这些值可能没有意义,它只是有助于过滤。)

http://jsfiddle.net/gordonwoodhull/g4xqvgvL/21/

旋转数据表

这方面的额外赏金提醒我,我从未解决 table 换位问题,所以我想我应该看一看,因为它很有趣。我仍然认为赏金应该发给@SergGr,但这里有一个基于类别、维度和列 accessors/formatters.

转置 table 的解决方案

首先,我们需要类别列表,所以让我们更好地构建类别和字段名称:

var categories = {  
  Car: 'car',
  Truck: 'truck',
  Bike: 'bike',
  Moto: 'moto'
};

现在假尺寸可以简化了,因为它是从这个类别地图生成的:

function fake_dimension(cats) {
  return {
    bottom: function() {
      return Object.keys(cats).map(function(k) {
        return {
          key: k,
          value: filteredRows.value(),
          acc: dc.pluck(cats[k])
        };
      });
    }
  };
}
var fakeDim = fake_dimension(categories);

我们需要从图表定义中提取列定义,因为我们要转换它们:

var columns = [
  {
    label: 'Value',
    format: function(d) { 
      return d.key;
    }
  },
  {
    label: 'Min',
    format: function(d) {
      return d3.min(d.value, d.acc);
    }
  },
  {
    label: 'Max',
    format: function(d) {
      return d3.max(d.value, d.acc);
    }
  },
  {
    label: 'Avg',
    format: function(d) {
      return d3.mean(d.value, d.acc);
    }
  }
];

终于可以写转置函数了:

function transpose_datatable(cats, dim, cols) {
  var cols2 = d3.map(cols, function(col) { // 1
    return col.label;
  });
  return {
    dim: { // 2
      bottom: function() {
        var dall = d3.map(dim.bottom(Infinity), function(row) { // 3
          return row.key;
        });
        return cols.slice(1).map(function(col) { // 4
          var row = {
            label: col.label
          };
          Object.keys(cats).forEach(function(k) {
            row[k] = dall.get(k);
          });
          return row;
        });
      }
    },
    cols: [ // 5
      {
        label: cols[0].label,
        format: function(d) {
          return d.label;
        }
      }
    ].concat(Object.keys(cats).map(function(k) { // 6
      return {
        label: k,
        format: function(d) {
          return cols2.get(d.label).format(d[k]);
        }
      }
    }))
  };
}

var transposed = transpose_datatable(categories, fakeDim, columns)
  1. 首先,我们需要原始列与其定义的映射,因为这些将成为行。我们可以在这里使用 d3.map,它的作用类似于 well-behaved JavaScript object.
  2. 我们将创建一个新的假维度和一个新的列定义数组。伪维度只有一个.bottom()方法,和上面那个一样
  3. .bottom() 的定义将需要所有原始数据,按键(类别名称)索引。所以我们也将其放入 d3.map object。
  4. 现在我们可以构建假维度数据了。第一列只是标题(现在是 headers 列),因此我们将跳过它。该行的数据将是新标题(以前的列标签)和每个类别的字段。这些字段由原始维度中的行填充。
  5. 新的列定义需要替换label、column,其余由类目名称生成。
  6. 每列的标签现在是类别名称,.format() 调用原始列的 format,使用类别名称获取数据。

新截图:

这是另一种解决方案,它产生的结果更接近所要求的结果,尽管代码比 Gordon 的多得多。

简介

我同意 Gordon 的观点,没有合理的方法可以直接用 crossfilter 实现你想要的。 Crossfilter 是面向行的,您想根据列生成多行。所以唯一的办法就是做一些 "fake" 的步骤。而 "fake" 步骤隐含意味着当原始数据源更改时,结果不会更新。我看不出有什么办法可以修复它,因为 crossfilter 隐藏了它的实现细节(例如 filterListenersdataListenersremoveDataListeners)。

然而,dc 的实现方式是默认情况下在各种事件之后重绘所有图表(因为它们都在同一个全局组中)。因此 "fake objects" 如果实施得当,也可能会根据更新后的数据重新计算。

因此我的代码包含 min/max 的两个实现:

  • 快(呃)但是如果你不做任何额外的过滤就不安全
  • 慢(嗯)但安全,以防您需要额外的过滤

请注意,如果您使用了快速但不安全的实现并进行了额外的过滤,您将遇到异常并且其他功能也可能会被破坏。

代码

所有代码都可以在 https://jsfiddle.net/4kcu2ut1/1/ 获得。我们把它分成逻辑块,一一看。

首先去一些辅助方法和对象。每个 Op 对象本质上都包含传递给 reduce 所必需的方法 + 额外的可选 getOutput 如果累加器包含更多数据则只是结果,例如 avgOp of [=105] 的情况=] "safe" ops.

var minOpFast = {
    add: function (acc, el) {
        return Math.min(acc, el);
    },
    remove: function (acc, el) {
        throw new Error("Not supported");
    },
    initial: function () {
        return Number.MAX_VALUE;
    }
};

var maxOpFast = {
    add: function (acc, el) {
        return Math.max(acc, el);
    },
    remove: function (acc, el) {
        throw new Error("Not supported");
    },
    initial: function () {
        return Number.MIN_VALUE;
    }
};


var binarySearch = function (arr, target) {
    var lo = 0;
    var hi = arr.length;
    while (lo < hi) {
        var mid = (lo + hi) >>> 1; // safe int division
        if (arr[mid] === target)
            return mid;
        else if (arr[mid] < target)
            lo = mid + 1;
        else
            hi = mid;
    }
    return lo;
};

var minOpSafe = {
    add: function (acc, el) {
        var index = binarySearch(acc, el);
        acc.splice(index, 0, el);
        return acc;
    },
    remove: function (acc, el) {
        var index = binarySearch(acc, el);
        acc.splice(index, 1);
        return acc;
    },
    initial: function () {
        return [];
    },
    getOutput: function (acc) {
        return acc[0];
    }
};

var maxOpSafe = {
    add: function (acc, el) {
        var index = binarySearch(acc, el);
        acc.splice(index, 0, el);
        return acc;
    },
    remove: function (acc, el) {
        var index = binarySearch(acc, el);
        acc.splice(index, 1);
        return acc;
    },
    initial: function () {
        return [];
    },
    getOutput: function (acc) {
        return acc[acc.length - 1];
    }
};

var avgOp = {
    add: function (acc, el) {
        acc.cnt += 1;
        acc.sum += el;
        acc.avg = acc.sum / acc.cnt;
        return acc;
    },
    remove: function (acc, el) {
        acc.cnt -= 1;
        acc.sum -= el;
        acc.avg = acc.sum / acc.cnt;
        return acc;
    },
    initial: function () {
        return {
            cnt: 0,
            sum: 0,
            avg: 0
        };
    },
    getOutput: function (acc) {
        return acc.avg;
    }
};

然后我们准备源数据并指定我们想要的转换。 aggregates 是上一步的操作列表,另外用 key 修饰以将临时数据存储在复合累加器中(它必须是唯一的)和 label 以显示在输出中。 srcKeys 包含将由 aggregates lits 中的每个操作处理的属性名称列表(所有这些必须具有相同的形状)。

var myCSV = [
    {"shift": "1", "date": "01/01/2016/08/00/00", "car": "178", "truck": "255", "bike": "317", "moto": "237"},
    {"shift": "2", "date": "01/01/2016/17/00/00", "car": "125", "truck": "189", "bike": "445", "moto": "273"},
    {"shift": "3", "date": "02/01/2016/08/00/00", "car": "140", "truck": "219", "bike": "328", "moto": "412"},
    {"shift": "4", "date": "02/01/2016/17/00/00", "car": "222", "truck": "290", "bike": "432", "moto": "378"},
    {"shift": "5", "date": "03/01/2016/08/00/00", "car": "200", "truck": "250", "bike": "420", "moto": "319"},
    {"shift": "6", "date": "03/01/2016/17/00/00", "car": "230", "truck": "220", "bike": "310", "moto": "413"},
    {"shift": "7", "date": "04/01/2016/08/00/00", "car": "155", "truck": "177", "bike": "377", "moto": "180"},
    {"shift": "8", "date": "04/01/2016/17/00/00", "car": "179", "truck": "203", "bike": "405", "moto": "222"},
    {"shift": "9", "date": "05/01/2016/08/00/00", "car": "208", "truck": "185", "bike": "360", "moto": "195"},
    {"shift": "10", "date": "05/01/2016/17/00/00", "car": "150", "truck": "290", "bike": "315", "moto": "280"},
    {"shift": "11", "date": "06/01/2016/08/00/00", "car": "200", "truck": "220", "bike": "350", "moto": "205"},
    {"shift": "12", "date": "06/01/2016/17/00/00", "car": "230", "truck": "170", "bike": "390", "moto": "400"},
];

var dateFormat = d3.time.format("%d/%m/%Y/%H/%M/%S");

myCSV.forEach(function (d) {
    d.date = dateFormat.parse(d.date);
    d['car'] = +d['car'];
    d['bike'] = +d['bike'];
    d['moto'] = +d['moto'];
    d['truck'] = +d['truck'];
    d.shift = +d.shift;
});

//console.table(myCSV);

var aggregates = [
    // not compatible with addtional filtering
    /*{
        key: 'min',
        label: 'Min',
        agg: minOpFast
    },**/
    {
        key: 'minSafe',
        label: 'Min Safe',
        agg: minOpSafe
    },
    // not compatible with addtional filtering
    /*{
        key: 'max',
        label: 'Max',
        agg: maxOpFast
    },*/
    {
        key: 'maxSafe',
        label: 'Max Safe',
        agg: maxOpSafe
    },
    {
        key: 'avg',
        agg: avgOp,
        label: 'Average'
    }
];

var srcKeys = ['car', 'bike', 'moto', 'truck'];

现在进入魔法buildTransposedAggregatesDimension 是这里所有繁重工作的原因。本质上它执行两个步骤:

  1. 首先groupAll获取所有运算符和所有键的叉积中每个组合的聚合数据。

  2. 将大型对象 grouped 拆分为一个数组,该数组可以作为另一个 crossfilter

  3. 的数据源

第 2 步是我的 "fake" 所在的位置。在我看来,"fake" 比 Gordon 的解决方案少得多,因为它不依赖于 crossfilterdc 的任何内部细节(参见 Gordon 解决方案中的 bottom 方法) .

在第 2 步进行拆分也是实际转置数据以满足您的要求的地方。显然,可以轻松修改代码以不执行此操作并以与 Gordon 解决方案相同的方式产生结果。

另请注意,重要的是,额外的步骤不会进行额外的计算,而只会将已计算的值转换为适当的格式。这对于过滤后的更新工作至关重要,因为在这种情况下 table 绑定到 buildTransposedAggregatesDimension 的结果仍然有效地绑定到原始 crossfilter 数据源。

var buildTransposedAggregatesDimension = function (facts, keysList, aggsList) {
    // "grouped" is a single record with all aggregates for all keys computed
    var grouped = facts.groupAll()
            .reduce(
            function add(acc, el) {
                aggsList.forEach(function (agg) {
                    var innerAcc = acc[agg.key];
                    keysList.forEach(function (key) {
                        var v = el[key];
                        innerAcc[key] = agg.agg.add(innerAcc[key], v);
                    });
                    acc[agg.key] = innerAcc;
                });
                return acc;
            },
            function remove(acc, el) {
                aggsList.forEach(function (agg) {
                    var innerAcc = acc[agg.key];
                    keysList.forEach(function (key) {
                        var v = el[key];
                        innerAcc[key] = agg.agg.remove(innerAcc[key], v);
                    });
                    acc[agg.key] = innerAcc;
                });
                return acc;
            },
            function initial() {
                var acc = {};
                aggsList.forEach(function (agg) {
                    var innerAcc = {};
                    keysList.forEach(function (key) {
                        innerAcc[key] = agg.agg.initial();
                    });
                    acc[agg.key] = innerAcc;
                });
                return acc;
            }).value();

    // split grouped back to array with element for each aggregation function
    var groupedAsArr = [];
    aggsList.forEach(function (agg, index) {
        groupedAsArr.push({
            sortIndex: index, // preserve index in aggsList so we can sort by it later
            //agg: agg,
            key: agg.key,
            label: agg.label,
            valuesContainer: grouped[agg.key],
            getOutput: function (columnKey) {
                var aggregatedValueForKey = grouped[agg.key][columnKey];
                return agg.agg.getOutput !== undefined ?
                        agg.agg.getOutput(aggregatedValueForKey) :
                        aggregatedValueForKey;
            }
        })
    });
    return crossfilter(groupedAsArr).dimension(function (el) {   return el;    });
};

小助手方法 buildColumnssrcKeys 中的每个原始键创建列 + 操作标签的附加列

var buildColumns = function (srcKeys) {
    var columns = [];
    columns.push({
        label: "Aggregate",
        format: function (el) {
            return el.label;
        }
    });
    srcKeys.forEach(function (key) {
        columns.push({
            label: key,
            format: function (el) {
                return el.getOutput(key);
            }
        });
    });
    return columns;
};

所以现在让我们齐心协力创造一个table。

var facts = crossfilter(myCSV);
var aggregatedDimension = buildTransposedAggregatesDimension(facts, srcKeys, aggregates);
dataTable = dc.dataTable('#dataTable'); // put such a <table> in your HTML!

dataTable
        .width(500)
        .height(400)
        .dimension(aggregatedDimension)
        .group(function (d) { return ''; })
        .columns(buildColumns(srcKeys))
        .sortBy(function (el) { return el.sortIndex; })
        .order(d3.ascending);

//dataTable.render();
dc.renderAll();

还有一段无耻地从 Gordon 那里偷来的代码,用于添加折线图以进行额外的过滤。