首页 > 解决方案 > 基于“reduceSummed”组的直方图

问题描述

我有以下模式的 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 个产品类别。我可以制作一个直方图并计算每箱 unitSold 涉及多少个 Quarters。这里的问题是每个季度都是单独计算的。我想要的是一个直方图,其中unitSold 的箱已经与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 的箱子,会有许多 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 做到这一点?

标签: dc.jscrossfilter

解决方案


按值重新组合数据

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

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

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. 将垃圾箱映射回原始数字,并带有计数

测试一下

我用下面的图表显示了你的原始数据(懒得计算季度,虽然我认为最近的 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);

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

这是我得到的图表,一目了然:

双重缩小图表

演示小提琴

向图表添加过滤

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

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.ever的宿舍:

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

chart.xUnits(quarterInterval.range);

消除第零个 bin

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

第零个 bin 可以用

  delete bins[0];

在返回之前regroup()

舍入按值画笔

如果需要捕捉到条形图,您可以使用

.round(x => Math.round(x/10000)*10000)

否则,过滤的范围可以在条形内部开始或结束,并且在刷过时条形的着色方式有些不准确,如下所示。

带过滤和宿舍

这是新的小提琴


推荐阅读