基于 "reduceSummed" 个组的直方图

Histogram based on "reduceSummed" groups

我有具有以下模式的 CSV 数据:

Quarter,productCategory,unitsSold
2018-01-01,A,21766
2018-01-01,B,10076
2018-01-01,C,4060
2018-04-01,A,27014
2018-04-01,B,12219
2018-04-01,C,4740
2018-07-01,A,29503
2018-07-01,B,13020
2018-07-01,C,5549
2018-10-01,A,3796
2018-10-01,B,15110
2018-10-01,C,6137
2019-01-01,A,25008
2019-01-01,B,11655
2019-01-01,C,4630
2019-04-01,A,31633
2019-04-01,B,14837
2019-04-01,C,5863
2019-07-01,A,33813
2019-07-01,B,15442
2019-07-01,C,6293
2019-10-01,A,35732
2019-10-01,B,19482
2019-10-01,C,6841

如您所见,每天有 3 个产品类别售出。我可以制作一个直方图并计算每箱 unitsSold 涉及多少个季度。这里的问题是每个季度都是单独计算的。我想要的是一个直方图,其中 unitsSold 的箱子已经与 Quarter 上的 reduceSum 分组。

这将导致如下结果:

Quarter, unitsSold
2018-01-01,35902
2018-04-01,43973
2018-07-01,48072
2018-10-01,25043
2019-01-01,41293
2019-04-01,52333
2019-07-01,55548
2019-10-01,62055

其中,根据 unitsSold 的 bins,许多 Quarters 将落入其中。例如,50.000 - 70.000 的 bin 将计为 3 个季度(2019-04-01、2019-07-01 和 2019-10-01)

通常我会这样做:

const histogramChart = new dc.BarChart('#histogram');
const histogramDim = ndx.dimension(d => Math.round(d.unitsSold / binSize) * binSize);
const histogramGroup = histogramDim.group().reduceCount();

但在理想情况下,直方图是在已经“reducedSummed”的东西上创建的。以这样的条形图直方图结束(数据与此示例不匹配):

如何使用 dc.js/crossfilter.js 完成此操作?

按值重新分组数据

我认为您的问题与 this previous question 之间的主要区别在于您希望在“重组”数据时对数据进行分类。 (有时这被称为“双重减少”......这些东西没有明确的名称。)

这是一种使用偏移量和宽度的方法:

function regroup(group, width, offset = 0) {
  return {
    all: function() {
      const bins = {};
      group.all().forEach(({key, value}) => {
        const bin = Math.floor((value - offset) / width);
        bins[bin] = (bins[bin] || 0) + 1;
      });
      return Object.entries(bins).map(
        ([bin, count]) => ({key: bin*width + offset, value: count}));
    }
  }
}

我们在这里做的是遍历原始组和

  1. 将每个值映射到其 bin 编号
  2. 增加该 bin 编号的计数,或从 1 开始
  3. 将 bin 映射回原始数字,计数

正在测试

我用下面的图表展示了你的原始数据(懒得算季度了,虽然我觉得最近的D3不难):

const quarterDim = cf.dimension(({Quarter}) => Quarter),
    unitsGroup = quarterDim.group().reduceSum(({unitsSold}) => unitsSold);

quarterChart.width(300)
    .height(200)
  .margins({left: 50, top: 0, right: 0, bottom: 20})
    .dimension(quarterDim)
  .group(unitsGroup)
  .x(d3.scaleTime().domain([d3.min(data, d => d.Quarter), d3.timeMonth.offset(d3.max(data, d => d.Quarter), 3)]))
  .elasticY(true)
  .xUnits(d3.timeMonths);

新图表

const rg = regroup(unitsGroup, 10000);
countQuartersChart.width(500)
  .height(200)
  .dimension({})
  .group(rg)
  .x(d3.scaleLinear())
  .xUnits(dc.units.fp.precision(10000))
  .elasticX(true)
  .elasticY(true);

(注意空维度,它会禁用过滤。过滤可能是可能的,但你必须映射回原始维度键,所以我现在跳过它。)

这是我得到的图表,一眼看去是正确的:

Demo fiddle.

向图表添加过滤

要对这个“按值的季度数”直方图进行过滤,首先让我们通过将按值图表放在其自己的维度上来启用按值图表和季度图表之间的过滤:

const quarterDim2 = cf.dimension(({Quarter}) => Quarter),
    unitsGroup2 = quarterDim2.group().reduceSum(({unitsSold}) => unitsSold);
const byvaluesGroup = regroup(unitsGroup2, 10000);
countQuartersChart.width(500)
    .height(200)
  .dimension(quarterDim2)
  .group(byvaluesGroup)
  .x(d3.scaleLinear())
  .xUnits(dc.units.fp.precision(10000))
  .elasticX(true)
  .elasticY(true);

然后,我们用

实现过滤
countQuartersChart.filterHandler((dimension, filters) => {
  if(filters.length === 0)
    dimension.filter(null);
  else {
    console.assert(filters.length === 1 && filters[0].filterType === 'RangedFilter');
    const range = filters[0];
    const included_quarters = unitsGroup2.all()
        .filter(({value}) => range[0] <= value && value < range[1])
        .map(({key}) => key.getTime());
    dimension.filterFunction(k => included_quarters.includes(k.getTime()));
  }
  return filters;
});

这会找到 unitsGroup2 中所有值都在该范围内的季度。然后它将维度的过滤器设置为仅接受这些季度的日期。

零星

宿舍

D3 支持 interval.every 的宿舍:

const quarterInterval = d3.timeMonth.every(3);

chart.xUnits(quarterInterval.range);

正在消除第 0 个 bin

正如评论中所讨论的那样,当其他图表启用过滤器时,最终可能会有许多季度的销量低于 10000 件,从而导致非常高的零条扭曲图表。

可以使用

删除第零个 bin
  delete bins[0];

regroup()return之前

按值画笔舍入

如果需要对齐条形图,您可以使用

启用它
.round(x => Math.round(x/10000)*10000)

否则,筛选范围可以在条形内部开始或结束,并且 the way the bars are colored when brushed is somewhat inaccurate 如下所示。

Here's the new fiddle.