24

我正在使用 d3.stack 创建堆叠面积图,但如果我在每一层中没有相同数量的项目,则会出现错误。我从这样的一组数据开始:

[  
   {key:'Group1',value,date},  
   {key:'Group1',value,date},  
   {key:'Group1',value,date},  
   {key:'Group2',value,date},  
   {key:'Group2',value,date}  
]

在我通过 nest() 和 stack() 运行它之后,我最终得到了这种格式,正如预期的那样:

[  
   {key: 'Group1',  
    values: [ {key,value,date}, {key,value,date}, {key,value,date} ] },  
   {key: 'Group2',  
    values: [ {key,value,date}, {key,value,date} ]  }  
]

我稍微修改了一个堆叠区域示例来演示这个 jsFiddle 中的问题:http: //jsfiddle.net/brentkeller/rTC3c/2/

如果您删除 sourceData 数组中的任何一个数据点,您将在控制台中看到错误消息“无法读取未定义的属性 '1'”。

有没有办法让 d3.stack 只为丢失的数据点假设零值?如果没有,是否有一个优雅的解决方案来填补缺失值?

4

3 回答 3

17

这不是 d3 特定的,而是用于填充键控数据数组中的空白的通用解决方案。我在这里使用以下功能修改了您的 jsfiddle :

function assignDefaultValues( dataset )
{
    var defaultValue = 0;
    var keys = [ 'Group1' , 'Group2', 'Group3' ];
    var hadData = [ true, true, true];
    var newData = [];
    var previousdate = new Date();
    var sortByDate = function(a,b){ return a.date > b.date ? 1 : -1; };

    dataset.sort(sortByDate);
    dataset.forEach(function(row){
        if(row.date.valueOf() !== previousdate.valueOf()){
            for(var i = 0 ; i < keys.length ; ++i){
                if(hadData[i] === false){
                    newData.push( { key: keys[i], 
                                   value: defaultValue, 
                                   date: previousdate });
                }
                hadData[i] = false;
            }
            previousdate = row.date;
        }
        hadData[keys.indexOf(row.key)] = true; 
    });
    for( i = 0 ; i < keys.length ; ++i){
        if(hadData[i] === false){
            newData.push( { key: keys[i], value: defaultValue, 
                            date: previousdate });
        }
    }
    return dataset.concat(newData).sort(sortByDate);
}

它遍历给定的数据集,并在遇到新值时为尚未看到date的任何值分配默认值。keys

于 2013-02-24T04:01:04.587 回答
5

Stack 确实如其所说,堆叠图表,因此您作为用户有责任以正确的格式提供数据。如果您考虑一下,这是有道理的,因为堆栈基本上与数据格式无关。它提供了很大的灵活性,唯一的限制是对于每一层它可以访问相同数量的点。它如何确定缺少哪些点?假设第一层有五个点,第二层有十个点,那么第一层是不是少了五个点?或者两者都缺少点,因为第三层包含更多点。然后如果缺少点,哪些点?开始,结束,中间的某个地方?同样,堆栈实现没有明智的方法来解决这个问题(除非它会强制使用非常严格的数据结构)。

所以,但是你无能为力吗?我想你可以。我不能给你一个完整的实现,但可以给你一些正确方向的指示。我们从这里开始:

var stack = d3.layout.stack()
  .offset("zero")
  .values(function(d) { return d.values; })

在这里,您只需返回值,在您的示例中,这将是嵌套运算符的结果。因此,此时您有能力“修复”这些值。

您需要做的第一件事是确定最大观察次数。

var nested = nest.entries(data);
var max = nested.reduce(function(prev, cur) {
  return Math.max(prev, cur.values.length);
}, 0);

现在是棘手的部分。知道最大元素数后,您需要调整传递给值的函数。在这里,您必须对数据做出假设。从您的问题中,我了解到某些组的值丢失了。所以有两种可能。您可以假设具有最大元素数的组包含该范围内的所有项目,或者您假设某个范围并检查所有组是否包含您范围中每个“刻度”的值。因此,如果您的范围是日期范围(如您的示例中)并且您希望每天(或任何时间间隔)进行测量,则您必须遍历组中的项目并自己填补空白。我将尝试给出一个数值范围的(未经测试的)示例:

// define some calculated values that can be reused in correctedValues
var range = [0, 1];
var step = 0.1;

function correctedValues(d) {
  var values = d.values;
  var result = [];
  var expected = 0;
  for (var i = 0; i < values.length; ++i) {
     var value = values[i];
     // Add null-entries
     while (value.x > expected) {
       result.push({x: expected, otherproperties_you_need... });
       expected += step;
     }
     result.push(value); // Now add the real data point.
     expected = value.x;
  }

  // Fill up the end of of the array if needed
  while(expected < range[1]) {
    result.push({x: expected, otherproperties_you_need... });
    expected += step;
  }
  return result;
}

// Now use our costom function for the stack
var stack = d3.layout.stack()
 .offset("zero")
 .values(correctedValues)
...

如前所述,这部分未经测试,不能直接解决您的问题(因为我使用的是数字范围),但我认为它应该让您了解如何解决问题(以及问题的实际根源是什么)。

于 2013-02-21T17:23:06.900 回答
2

正如其他人所解释的那样,堆积图猜测每个数据点的缺失值是不合理的,因为插值的方法太多,没有明显的选择。

但是,d3.svg.line()似乎为您选择自己的插值方法和填充缺失值提供了一种合理的方法。虽然它是为生成 SVG 路径而设计的,但您可以调整它来定义一般的线条。这里建议使用插值方法:

https://github.com/mbostock/d3/wiki/SVG-Shapes#wiki-line_interpolate

不幸的是,该类目前拥有所有这些美妙的插值方法(在 d3 的其他任何地方都没有出现),但仅限于生成 SVG 路径数据而不是任意中间值。也许如果@mbostock 看到这一点,他会考虑推广该功能。

但是,现在您可能只想创建一个 d3 的分支,并在将line(data)其写入 SVGpath字符串之前获取中间结果,在进行插值的源部分中,如下所示:

  function line(data) {
    var segments = [],
        points = [],
        i = -1,
        n = data.length,
        d,
        fx = d3_functor(x),
        fy = d3_functor(y);

    function segment() {
      segments.push("M", interpolate(projection(points), tension));
    }

    while (++i < n) {
      if (defined.call(this, d = data[i], i)) {
        points.push([+fx.call(this, d, i), +fy.call(this, d, i)]);
      } else if (points.length) {
        segment();
        points = [];
      }
    }

    if (points.length) segment();

    return segments.length ? segments.join("") : null;
  }
于 2013-02-21T20:51:57.350 回答