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)
- 首先,我们需要原始列与其定义的映射,因为这些将成为行。我们可以在这里使用
d3.map
,它的作用类似于 well-behaved JavaScript object.
- 我们将创建一个新的假维度和一个新的列定义数组。伪维度只有一个
.bottom()
方法,和上面那个一样
.bottom()
的定义将需要所有原始数据,按键(类别名称)索引。所以我们也将其放入 d3.map
object。
- 现在我们可以构建假维度数据了。第一列只是标题(现在是 headers 列),因此我们将跳过它。该行的数据将是新标题(以前的列标签)和每个类别的字段。这些字段由原始维度中的行填充。
- 新的列定义需要替换label、column,其余由类目名称生成。
- 每列的标签现在是类别名称,
.format()
调用原始列的 format
,使用类别名称获取数据。
新截图:
这是另一种解决方案,它产生的结果更接近所要求的结果,尽管代码比 Gordon 的多得多。
简介
我同意 Gordon 的观点,没有合理的方法可以直接用 crossfilter
实现你想要的。 Crossfilter
是面向行的,您想根据列生成多行。所以唯一的办法就是做一些 "fake" 的步骤。而 "fake" 步骤隐含意味着当原始数据源更改时,结果不会更新。我看不出有什么办法可以修复它,因为 crossfilter
隐藏了它的实现细节(例如 filterListeners
、dataListeners
和 removeDataListeners
)。
然而,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
是这里所有繁重工作的原因。本质上它执行两个步骤:
首先groupAll
获取所有运算符和所有键的叉积中每个组合的聚合数据。
将大型对象 grouped
拆分为一个数组,该数组可以作为另一个 crossfilter
的数据源
第 2 步是我的 "fake" 所在的位置。在我看来,"fake" 比 Gordon 的解决方案少得多,因为它不依赖于 crossfilter
或 dc
的任何内部细节(参见 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; });
};
小助手方法 buildColumns
为 srcKeys
中的每个原始键创建列 + 操作标签的附加列
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 那里偷来的代码,用于添加折线图以进行额外的过滤。
正在尝试创建一个 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)
- 首先,我们需要原始列与其定义的映射,因为这些将成为行。我们可以在这里使用
d3.map
,它的作用类似于 well-behaved JavaScript object. - 我们将创建一个新的假维度和一个新的列定义数组。伪维度只有一个
.bottom()
方法,和上面那个一样 .bottom()
的定义将需要所有原始数据,按键(类别名称)索引。所以我们也将其放入d3.map
object。- 现在我们可以构建假维度数据了。第一列只是标题(现在是 headers 列),因此我们将跳过它。该行的数据将是新标题(以前的列标签)和每个类别的字段。这些字段由原始维度中的行填充。
- 新的列定义需要替换label、column,其余由类目名称生成。
- 每列的标签现在是类别名称,
.format()
调用原始列的format
,使用类别名称获取数据。
新截图:
这是另一种解决方案,它产生的结果更接近所要求的结果,尽管代码比 Gordon 的多得多。
简介
我同意 Gordon 的观点,没有合理的方法可以直接用 crossfilter
实现你想要的。 Crossfilter
是面向行的,您想根据列生成多行。所以唯一的办法就是做一些 "fake" 的步骤。而 "fake" 步骤隐含意味着当原始数据源更改时,结果不会更新。我看不出有什么办法可以修复它,因为 crossfilter
隐藏了它的实现细节(例如 filterListeners
、dataListeners
和 removeDataListeners
)。
然而,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
是这里所有繁重工作的原因。本质上它执行两个步骤:
首先
groupAll
获取所有运算符和所有键的叉积中每个组合的聚合数据。将大型对象
grouped
拆分为一个数组,该数组可以作为另一个crossfilter
的数据源
第 2 步是我的 "fake" 所在的位置。在我看来,"fake" 比 Gordon 的解决方案少得多,因为它不依赖于 crossfilter
或 dc
的任何内部细节(参见 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; });
};
小助手方法 buildColumns
为 srcKeys
中的每个原始键创建列 + 操作标签的附加列
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 那里偷来的代码,用于添加折线图以进行额外的过滤。