【问题标题】:dc.js Incorporate regression chart into existing scatterplot with crossfilterdc.js 使用交叉过滤器将回归图合并到现有散点图中
【发布时间】:2017-03-19 21:03:27
【问题描述】:

我正在使用 dc.js 和 crossfilter.js 创建一个 d3 仪表板,并且想知道如何将回归线实现到响应过滤的散点图中。

我一直在玩一些添加回归线的示例,但我未能成功提取和合并代码。

我对数学没有问题,而是如何从维度访问过滤后的数据,然后如何将回归线添加到过滤后的散点图中(以便回归线也响应未来过滤)。

jsFiddle Demo

var data = [
{"record":"record","date":"date","cars":"cars","bikes":"bikes"},
{"record":"1","date":"01/05/2012","cars":"1488.1","bikes":"49.73"},
{"record":"2","date":"02/05/2012","cars":"1374.29","bikes":"52.44"},
{"record":"3","date":"03/05/2012","cars":"1353.01","bikes":"47.92"},
{"record":"4","date":"04/05/2012","cars":"1420.33","bikes":"50.69"},
{"record":"5","date":"05/05/2012","cars":"1544.11","bikes":"47.47"},
{"record":"6","date":"06/05/2012","cars":"1292.84","bikes":"47.75"},
{"record":"7","date":"07/05/2012","cars":"1318.9","bikes":"48.64"},
{"record":"8","date":"08/05/2012","cars":"1686.3","bikes":"50.9"},
{"record":"9","date":"09/05/2012","cars":"1603.99","bikes":"53.44"},
{"record":"10","date":"10/05/2012","cars":"1420.1","bikes":"53.29"},
{"record":"11","date":"11/05/2012","cars":"1410.8","bikes":"54.06"},
{"record":"12","date":"12/05/2012","cars":"1374.62","bikes":"51.24"},
{"record":"13","date":"13/05/2012","cars":"1279.53","bikes":"53.96"},
{"record":"14","date":"14/05/2012","cars":"1330.47","bikes":"49.5"},
{"record":"15","date":"15/05/2012","cars":"1377.61","bikes":"52.32"},
{"record":"16","date":"16/05/2012","cars":"1302.12","bikes":"51.96"},
{"record":"17","date":"17/05/2012","cars":"1326.9","bikes":"49.86"},
{"record":"18","date":"18/05/2012","cars":"1181.55","bikes":"50.25"},
{"record":"19","date":"19/05/2012","cars":"1493.75","bikes":"51.24"},
{"record":"20","date":"20/05/2012","cars":"1463.9","bikes":"50.88"},
{"record":"21","date":"21/05/2012","cars":"1370.16","bikes":"51.09"},
{"record":"22","date":"22/05/2012","cars":"1403.3","bikes":"51.67"},
{"record":"23","date":"23/05/2012","cars":"1277.65","bikes":"49.3"},
{"record":"24","date":"24/05/2012","cars":"1361.94","bikes":"50.47"},
{"record":"25","date":"25/05/2012","cars":"1400.8","bikes":"51.55"},
{"record":"26","date":"26/05/2012","cars":"1289.09","bikes":"47.17"},
{"record":"27","date":"27/05/2012","cars":"1258.39","bikes":"52.12"},
{"record":"28","date":"28/05/2012","cars":"1288.71","bikes":"49.28"},
{"record":"29","date":"29/05/2012","cars":"1511.86","bikes":"50.73"},
{"record":"30","date":"30/05/2012","cars":"1300.38","bikes":"52.39"},
{"record":"31","date":"31/05/2012","cars":"1455.19","bikes":"49.53"},
{"record":"32","date":"01/06/2012","cars":"1311.89","bikes":"50.37"},
{"record":"33","date":"02/06/2012","cars":"1368.64","bikes":"50.87"},
{"record":"34","date":"03/06/2012","cars":"1360.05","bikes":"50.51"},
{"record":"35","date":"04/06/2012","cars":"1382.56","bikes":"49.67"},
{"record":"36","date":"05/06/2012","cars":"1304.15","bikes":"47.6"},
{"record":"37","date":"06/06/2012","cars":"1271.57","bikes":"50.22"},
{"record":"38","date":"07/06/2012","cars":"1442.38","bikes":"50.8"},
{"record":"39","date":"08/06/2012","cars":"1406.38","bikes":"53.14"},
{"record":"40","date":"09/06/2012","cars":"1724.16","bikes":"49.66"},
{"record":"41","date":"10/06/2012","cars":"1931.05","bikes":"53"},
{"record":"42","date":"11/06/2012","cars":"1669.47","bikes":"53.71"},
{"record":"43","date":"12/06/2012","cars":"1794.06","bikes":"51.78"},
{"record":"44","date":"13/06/2012","cars":"1625.98","bikes":"51.58"},
{"record":"45","date":"14/06/2012","cars":"1371.51","bikes":"52.36"},
{"record":"46","date":"15/06/2012","cars":"1418.05","bikes":"47.64"},
{"record":"47","date":"16/06/2012","cars":"1431","bikes":"53.14"},
{"record":"48","date":"17/06/2012","cars":"1527.21","bikes":"48.63"},
{"record":"49","date":"18/06/2012","cars":"1320.95","bikes":"51.7"},
{"record":"50","date":"19/06/2012","cars":"1396.93","bikes":"52.92"}
];
tSel1 = "cars";
tSel2 = "bikes";

data.forEach(function (d) {
	d[tSel1] = +d[tSel1];
	d[tSel2] = +d[tSel2];
});

var facts = crossfilter(data);

var allDimension = facts.groupAll();
var scatterDimension = facts.dimension(function(d) {return [+d[tSel1], +d[tSel2]];});
var scatterGroup = scatterDimension.group().reduceSum(function(d) { return d[tSel1]; });

var maxY1 = d3.max(data, function(d) {return d[tSel1]});
var maxY2 = d3.max(data, function(d) {return d[tSel2]});
var maxY1Plus = maxY1 + (maxY1 * 0.1);
var maxY2Plus = maxY2 + (maxY2 * 0.1);

var minY1 = d3.min(data, function(d) {return d[tSel1]});
var minY1Minus = minY1 * 0.9;
var minY2 = d3.min(data, function(d) {return d[tSel2]});
var minY2Minus = minY2 * 0.9;

xyScatterChart = dc.scatterPlot("#scatterPlot");
xyScatterChart	
	.width(600)
	.height(400)
	.margins({top: 20, right: 20, bottom: 20, left: 60})
	.dimension(scatterDimension)
	.group(scatterGroup)
	.symbolSize(6)
	.highlightedSize(15)
	.brushOn(false)
	.excludedOpacity(0.5)
	.excludedSize(5)
	.renderHorizontalGridLines(true)
	.renderVerticalGridLines(true)

	.x(d3.scale.linear().domain([minY1Minus,maxY1Plus]))
	.y(d3.scale.linear().domain([minY2Minus,maxY2Plus]));

dc.renderAll();
dc.redrawAll();
<link href="http://dc-js.github.io/dc.js/css/dc.css" rel="stylesheet"/>
<script src="http://dc-js.github.io/dc.js/js/d3.js"></script>
<script src="http://dc-js.github.io/dc.js/js/crossfilter.js"></script>
<script src="http://dc-js.github.io/dc.js/js/dc.js"></script>
<div id="scatterPlot"></div>

参考资料:

https://groups.google.com/forum/#!topic/dc-js-user-group/HaQMegKa_U0

https://bl.ocks.org/ctufts/298bfe4b11989960eeeecc9394e9f118

【问题讨论】:

    标签: d3.js dc.js crossfilter


    【解决方案1】:

    包含example in dc.js 会很棒,因为很多人都可以使用它。

    也许我们可以合作解决这个问题?我不知道数学,但这里有一种简单的方法,可以使用复合图表在从聚合组计算的数据上显示一条线。

    首先,这是嵌入了旧散点图的复合图表:

    var composite = dc.compositeChart("#composite");
    composite   
        .width(600)
        .height(400)
        .margins({top: 20, right: 20, bottom: 20, left: 60})
        .dimension(scatterDimension)
        .group(scatterGroup)
      .compose([
      dc.scatterPlot(composite)
        .symbolSize(6)
        .highlightedSize(15)
        .brushOn(false)
        .excludedOpacity(0.5)
        .excludedSize(5)
        .renderHorizontalGridLines(true)
        .renderVerticalGridLines(true),
      dc.lineChart(composite)
      .group(regressionGroup(scatterGroup))
    ])
        .x(d3.scale.linear().domain([minY1Minus,maxY1Plus]))
        .y(d3.scale.linear().domain([minY2Minus,maxY2Plus]));
    

    请注意,我们将散点组同时提供给复合图和散点图。那只是因为复合图表需要一个组,即使它实际上并没有使用它。

    我们已将与坐标有关的参数移至主(复合)图表,但散点图特有的所有内容仍保留在其上。我们还在合成中添加了一个折线图,它使用基于散点组的"fake group"

    这个假组特别假,但应该足以让你入门。由于今天没时间学数学,我就假装第一个点和最后一个点是回归:

    function regressionGroup(group) {
      return {
        all: function() {
          var _all = group.all();
          var first, last;
          for(var i=0; i < _all.length; ++i) {
            var key = _all[i].key;
            if(!isNaN(key[0]) && !isNaN(key[1])) {
              var kv = {key: key[0], value: key[1]};
              if(!first)
                first = kv;
              last = kv;
            }
          }
          return [first, last];
        }
      };
    }
    

    与所有假组一样,我们的想法是在图表要求时(并且很快)根据另一个组计算一些类似组的数据。这里的计算不是很有趣,因为你知道如何计算回归而我不知道。您需要用实际计算替换 firstlast 以及 for 循环;所有这一切都是检查有效点并保留它找到的第一个和最后一个。

    有趣的是,散点图获取键包含 x 和 y 坐标的数据,但折线图获取键为 x 且值为 y 的数据。这就是为什么我们要进行转换kv = {key: key[0], value: key[1]}

    后记

    请注意,如果您将回归指南点放在域之外 - the stack mixin is too aggressive about clipping points to the domain,则会遇到 dc.js 错误。在这种情况下,有一个简单而难看的解决方法:告诉折线图它有一个序数 x 刻度,即使它没有:

    var composite = dc.compositeChart("#composite"),
      lineChart;
    composite   
        .width(600)
      // ...
      .compose([
      // ...
      lineChart = dc.lineChart(composite)
      .group(regressionGroup(scatterGroup))
    ])
    lineChart.isOrdinal = d3.functor(true);
    

    呸!但它有效!这种 hack 可能只适用于复合材料!

    https://jsfiddle.net/gordonwoodhull/5tpcxov1/12/

    【讨论】:

    • 当然可以。我想我已经更好地解释了假组,但如果您有任何问题,请给我留言。
    • 您可能想要做的另一件事是将回归线延伸到图表边缘之外,以便很好地剪裁在边缘。如果计算返回斜率 + 截距应该很容易。只要您像上面那样手动设置域,这应该可以正常工作 - 或者需要一点魔法将 elasticX 推迟到一般情况下的散点图。
    • 啊!由于我建议超出图表边缘,您遇到了 dc.js 错误。对于那个很抱歉。在上面的后记中添加了一个可怕的解决方法。
    • 我在 dc.js 中添加了一个基本的library regression example
    【解决方案2】:

    我有一个功能齐全的回归示例。当我来这里寻求帮助并找到您的问题时,我正是这样做的。它需要regression.js (here)。

    这遵循了 Gordon 对“假组”的极好建议,实际上应该将其称为内联组、直接组,甚至动态组。这是我的:

    function myRegressionGroup(group, min, max, filter = false) {
      return {
        all: function() {
          var _all = group.all();
          var first, last;
          if(filter) reg = regression.linear(_all.filter(function(k,v) {if(k.key[0]) return k.key}).map((k,v) => k.key));
          else reg = regression.linear(_all.map((k,v) => k.key));
          first = reg.predict(min);
          last = reg.predict(max)
          return [{key:first[0], value: first[1]}, {key: last[0], value: last[1]}]
        }
      };
    }
    

    请注意,此功能需要交叉过滤器组以及 x-scale 中的 minmax。由于您通常会为您的 xScale 计算这些值,因此只需在此处重用它们。这是因为该函数使用predict 方法的极值来计算回归线的两个点。

    可选的filter 数据管理员让您决定是否删除 x 上的空值。

    @Gordon,我应该怎么做才能在Examples of using dc.js 中包含我的回归示例?

    【讨论】:

    • 嗨@Ricardo,我直到现在才看到这个。如果您仍然感兴趣,向web-src/examples 添加示例的拉取请求会很棒。是的,“假集团”是一个愚蠢的名字,在过去的四年里变得更加愚蠢。最初的想法是它“伪造”(或模拟)一个交叉过滤器组。起初这似乎是一个坏主意,但它真的被取消了,而且可以做很多事情。
    猜你喜欢
    • 2014-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多