dc.js 使用两个没有简单维度和第二分组阶段的减速器
dc.js Using two reducers without a simple dimension and second grouping stage
在我对此 post 的回复之后快速提问:
只是想完全了解 reducer 以及如何过滤和收集数据,所以我将首先逐步了解我的理解。
数据格式:
{
"SSID": "eduroam",
"identifier": "Client",
"latitude": 52.4505,
"longitude": -1.9361,
"mac": "dc:d9:16:##:##:##",
"packet": "PR-REQ",
"timestamp": "2018-07-10 12:25:26",
"vendor": "Huawei Technologies Co.Ltd"
}
(1) 使用以下内容应生成键值对的输出数组(键 MAC 连接到的网络的地址和值计数):
var MacCountsGroup = mac.group().reduce(
function (p, v) {
p[v.mac] = (p[v.mac] || 0) + v.counter;
return p;
},
function (p, v) {
p[v.mac] -= v.counter;
return p;
},
function () {
return {}; // KV Pair of MAC -> Count
}
);
(2) 然后为了使用该对象,必须将其展平,以便将其传递给图表,如下所示:
function flatten_object_group(group) {
return {
all: function () {
return group.all().map(function (kv) {
return {
key: kv.key,
value: Object.values(kv.value).filter(function (v) {
return v > 0;
})
};
});
}
};
}
var connectionsGroup = flatten_object_group(MacCountsGroup);
(3) 然后我将 mac 作为饼图维度和 connectionsGroup 作为组传递。这会根据我的数据集返回一个包含大约 50,000 个切片的图表。
var packetPie = dc.pieChart("#packetPie");
packetPie
.height(495)
.width(350)
.radius(180)
.renderLabel(true)
.transitionDuration(1000)
.dimension(mac)
.ordinalColors(['#07453E', '#145C54', '#36847B'])
.group(connectionsGroup);
这行得通,我会跟进到这一点。
(4) 现在我想按第一个减速器给出的值进行分组,即我想将所有 mac 地址与 1 个网络连接、2 个网络连接等组合为切片.
作为 "Network connections" 的一个维度,如何做到这一点?我如何生成源数据中不存在的汇总数据,它是从 mac?
生成的
或者这是否需要第一个缩减器和展平之间的中间函数来组合来自第一个缩减器的所有值?
您无需执行所有这些操作即可获得 mac 个地址的饼图。
1-3点理解有误,我想我会先解决的。看起来你复制并粘贴了上一个问题的代码,所以我不确定这是否有帮助。
(1) 如果您有 mac 个地址的维度,像这样减少它不会有任何进一步的影响。最初的想法是 供应商 dimension/group,然后减少每个 mac 地址 的计数。这种减少将按 mac 地址 分组,然后进一步计算每个 bin 中每个 mac 地址的实例,因此它只是一个具有一个键的对象。它将生成一个键值对映射,如
{key: 'MAC-123', value: {'MAC-123': 12}}
(2) 这将在值内展平对象,删除键并只生成一个计数数组
{key: 'MAC-123', value: [12]}
(3) 由于饼图需要简单的 key/value 对,值为数字,因此它可能对获取数组 [12]
这样的值不满意。这些值可能被强制为 NaN
.
(4) 好的,这才是真正的问题,它实际上不像你上一个问题那么简单。我们使用箱形图很容易,因为数据中存在 "dimension"(在交叉过滤器术语中,您过滤和分组的键)。
让我们忘记上面第 1-3 点中的错误引导,并从第一原则开始。
没有办法查看数据的单个行并在不查看任何其他内容的情况下确定它是否属于类别 "has 1 connection"、"has 2 connections" 等。假设您想要为了能够单击饼图中的切片并过滤所有数据,我们必须找到另一种方法来实现它。
但首先让我们看看如何制作 "number of network connections" 的饼图。这有点容易,但据我所知,它确实需要一个真正的 "double reduce".
如果我们在 mac 维度上使用默认缩减,我们将得到一个 key/value 对的数组,其中键是 mac 地址,值为该地址的连接数:
[
{
"key": "1c:b7:2c:48",
"value": 8
},
{
"key": "1c:b7:be:ef",
"value": 3
},
{
"key": "6c:17:79:03",
"value": 2
},
...
我们现在如何生成一个 key/value 数组,其中键是连接数,值是该连接数的 mac 地址数组?
听起来像是一份鲜为人知的工作Array.reduce。这个函数可能是 crossfilter 的 group.reduce()
的灵感来源,但它有点简单:它只是遍历一个数组,将每个值与最后一个值的结果组合起来。它非常适合从数组生成对象:
var value_keys = macPacketGroup.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
太棒了:
{
"1": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
],
"2": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
],
但我们想要一个 key/value 对的数组,而不是一个对象!
var key_count_value_macs = Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
太好了,这看起来就像 "real group" 会产生的结果:
[
{
"key": "1",
"value": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
]
},
{
"key": "2",
"value": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
]
},
...
将所有内容包装在 "fake group" 中,当要求生成 .all()
时,查询原始组并进行上述转换:
function value_keys_group(group) {
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
}
}
}
现在我们可以绘制饼图了!这里唯一有趣的是值访问器应该查看每个值的数组长度(而不是假设值只是一个数字):
packetPie
// ...
.group(value_keys_group(macPacketGroup))
.valueAccessor(kv => kv.value.length);
但是,单击切片将不起作用。稍后我会 return - 只想先点击 "save"!
第 2 部分:基于计数的过滤
正如我在开始时所说的那样,无法创建将根据连接数进行过滤的交叉过滤器维度。这是因为 crossfilter 总是需要查看每一行并仅根据该行中的信息来确定它是属于组还是属于过滤器。
如果此时添加另一个图表并尝试单击切片,everything in the other charts will disappear。这是因为键现在是计数,而计数是无效的 mac 地址,所以我们告诉它过滤到一个不存在的键。
但是,我们显然可以通过 mac 地址进行过滤,而且我们也知道每个计数的 mac 地址!所以这还不错。它只需要一个 filterHandler.
虽然,嗯嗯,在制作假群的过程中,我们好像忘记了value_keys
。它隐藏在函数内部,然后放手。
有点难看,但我们可以解决这个问题:
function value_keys_group(group) {
var saved_value_keys;
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
saved_value_keys = value_keys;
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
},
value_keys: function() {
return saved_value_keys;
}
}
}
现在,每次调用.all()
(每次绘制饼图)时,假组都会隐藏value_keys
对象。这不是一个很好的做法(如果你在 .all()
之前调用它,.value_keys()
会 return undefined
),但基于 dc.js 的工作方式是安全的。
除此之外,饼图的 filterHandler
相对简单:
packetPie.filterHandler(function(dimension, filters) {
if(filters.length === 0)
dimension.filter(null);
else {
var value_keys = packetPie.group().value_keys();
var all_macs = filters.reduce(
(p, v) => p.concat(value_keys[v]), []);
dimension.filterFunction(k => all_macs.indexOf(k) !== -1);
}
return filters;
});
这里有趣的一行是对 Array.reduce
的另一个调用。此函数对于从另一个数组生成一个数组也很有用,在这里我们只是用它来连接所有选定切片(连接计数)的所有值(mac 地址)。
现在我们有了一个有效的过滤器。将它与上一个问题的箱形图结合起来意义不大,但 the new fiddle 表明基于连接数的过滤确实有效。
第 3 部分:关于零?
正如通常出现的那样,crossfilter 认为值为零的 bin 仍然存在,因此我们需要 "remove the empty bins"。但是,在这种情况下,我们向第一个假组添加了一个非标准方法,以允许过滤。 (我们本来可以在那里使用全局变量,但全局变量很乱。)
所以,我们需要"pass through" value_keys
方法:
function remove_empty_bins_pt(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.key !== '0';
});
},
value_keys: function() {
return source_group.value_keys();
}
};
}
packetPie
.group(remove_empty_bins_pt(value_keys_group(macPacketGroup)))
这里的另一个奇怪之处是我们正在过滤掉 key 零,这里是一个字符串!
或者,这里有一个更好的解决方案!在传递给value_keys_group
之前先做bin过滤,然后我们就可以使用普通的remove_empty_bins
!
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
//return Math.abs(d.value) > 0.00001; // if using floating-point numbers
return d.value !== 0; // if integers only
});
}
};
}
packetPie
.group(value_keys_group(remove_empty_bins(macPacketGroup)))
在我对此 post 的回复之后快速提问:
数据格式:
{
"SSID": "eduroam",
"identifier": "Client",
"latitude": 52.4505,
"longitude": -1.9361,
"mac": "dc:d9:16:##:##:##",
"packet": "PR-REQ",
"timestamp": "2018-07-10 12:25:26",
"vendor": "Huawei Technologies Co.Ltd"
}
(1) 使用以下内容应生成键值对的输出数组(键 MAC 连接到的网络的地址和值计数):
var MacCountsGroup = mac.group().reduce(
function (p, v) {
p[v.mac] = (p[v.mac] || 0) + v.counter;
return p;
},
function (p, v) {
p[v.mac] -= v.counter;
return p;
},
function () {
return {}; // KV Pair of MAC -> Count
}
);
(2) 然后为了使用该对象,必须将其展平,以便将其传递给图表,如下所示:
function flatten_object_group(group) {
return {
all: function () {
return group.all().map(function (kv) {
return {
key: kv.key,
value: Object.values(kv.value).filter(function (v) {
return v > 0;
})
};
});
}
};
}
var connectionsGroup = flatten_object_group(MacCountsGroup);
(3) 然后我将 mac 作为饼图维度和 connectionsGroup 作为组传递。这会根据我的数据集返回一个包含大约 50,000 个切片的图表。
var packetPie = dc.pieChart("#packetPie");
packetPie
.height(495)
.width(350)
.radius(180)
.renderLabel(true)
.transitionDuration(1000)
.dimension(mac)
.ordinalColors(['#07453E', '#145C54', '#36847B'])
.group(connectionsGroup);
这行得通,我会跟进到这一点。
(4) 现在我想按第一个减速器给出的值进行分组,即我想将所有 mac 地址与 1 个网络连接、2 个网络连接等组合为切片.
作为 "Network connections" 的一个维度,如何做到这一点?我如何生成源数据中不存在的汇总数据,它是从 mac?
生成的或者这是否需要第一个缩减器和展平之间的中间函数来组合来自第一个缩减器的所有值?
您无需执行所有这些操作即可获得 mac 个地址的饼图。
1-3点理解有误,我想我会先解决的。看起来你复制并粘贴了上一个问题的代码,所以我不确定这是否有帮助。
(1) 如果您有 mac 个地址的维度,像这样减少它不会有任何进一步的影响。最初的想法是 供应商 dimension/group,然后减少每个 mac 地址 的计数。这种减少将按 mac 地址 分组,然后进一步计算每个 bin 中每个 mac 地址的实例,因此它只是一个具有一个键的对象。它将生成一个键值对映射,如
{key: 'MAC-123', value: {'MAC-123': 12}}
(2) 这将在值内展平对象,删除键并只生成一个计数数组
{key: 'MAC-123', value: [12]}
(3) 由于饼图需要简单的 key/value 对,值为数字,因此它可能对获取数组 [12]
这样的值不满意。这些值可能被强制为 NaN
.
(4) 好的,这才是真正的问题,它实际上不像你上一个问题那么简单。我们使用箱形图很容易,因为数据中存在 "dimension"(在交叉过滤器术语中,您过滤和分组的键)。
让我们忘记上面第 1-3 点中的错误引导,并从第一原则开始。
没有办法查看数据的单个行并在不查看任何其他内容的情况下确定它是否属于类别 "has 1 connection"、"has 2 connections" 等。假设您想要为了能够单击饼图中的切片并过滤所有数据,我们必须找到另一种方法来实现它。
但首先让我们看看如何制作 "number of network connections" 的饼图。这有点容易,但据我所知,它确实需要一个真正的 "double reduce".
如果我们在 mac 维度上使用默认缩减,我们将得到一个 key/value 对的数组,其中键是 mac 地址,值为该地址的连接数:
[
{
"key": "1c:b7:2c:48",
"value": 8
},
{
"key": "1c:b7:be:ef",
"value": 3
},
{
"key": "6c:17:79:03",
"value": 2
},
...
我们现在如何生成一个 key/value 数组,其中键是连接数,值是该连接数的 mac 地址数组?
听起来像是一份鲜为人知的工作Array.reduce。这个函数可能是 crossfilter 的 group.reduce()
的灵感来源,但它有点简单:它只是遍历一个数组,将每个值与最后一个值的结果组合起来。它非常适合从数组生成对象:
var value_keys = macPacketGroup.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
太棒了:
{
"1": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
],
"2": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
],
但我们想要一个 key/value 对的数组,而不是一个对象!
var key_count_value_macs = Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
太好了,这看起来就像 "real group" 会产生的结果:
[
{
"key": "1",
"value": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
]
},
{
"key": "2",
"value": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
]
},
...
将所有内容包装在 "fake group" 中,当要求生成 .all()
时,查询原始组并进行上述转换:
function value_keys_group(group) {
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
}
}
}
现在我们可以绘制饼图了!这里唯一有趣的是值访问器应该查看每个值的数组长度(而不是假设值只是一个数字):
packetPie
// ...
.group(value_keys_group(macPacketGroup))
.valueAccessor(kv => kv.value.length);
但是,单击切片将不起作用。稍后我会 return - 只想先点击 "save"!
第 2 部分:基于计数的过滤
正如我在开始时所说的那样,无法创建将根据连接数进行过滤的交叉过滤器维度。这是因为 crossfilter 总是需要查看每一行并仅根据该行中的信息来确定它是属于组还是属于过滤器。
如果此时添加另一个图表并尝试单击切片,everything in the other charts will disappear。这是因为键现在是计数,而计数是无效的 mac 地址,所以我们告诉它过滤到一个不存在的键。
但是,我们显然可以通过 mac 地址进行过滤,而且我们也知道每个计数的 mac 地址!所以这还不错。它只需要一个 filterHandler.
虽然,嗯嗯,在制作假群的过程中,我们好像忘记了value_keys
。它隐藏在函数内部,然后放手。
有点难看,但我们可以解决这个问题:
function value_keys_group(group) {
var saved_value_keys;
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
saved_value_keys = value_keys;
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
},
value_keys: function() {
return saved_value_keys;
}
}
}
现在,每次调用.all()
(每次绘制饼图)时,假组都会隐藏value_keys
对象。这不是一个很好的做法(如果你在 .all()
之前调用它,.value_keys()
会 return undefined
),但基于 dc.js 的工作方式是安全的。
除此之外,饼图的 filterHandler
相对简单:
packetPie.filterHandler(function(dimension, filters) {
if(filters.length === 0)
dimension.filter(null);
else {
var value_keys = packetPie.group().value_keys();
var all_macs = filters.reduce(
(p, v) => p.concat(value_keys[v]), []);
dimension.filterFunction(k => all_macs.indexOf(k) !== -1);
}
return filters;
});
这里有趣的一行是对 Array.reduce
的另一个调用。此函数对于从另一个数组生成一个数组也很有用,在这里我们只是用它来连接所有选定切片(连接计数)的所有值(mac 地址)。
现在我们有了一个有效的过滤器。将它与上一个问题的箱形图结合起来意义不大,但 the new fiddle 表明基于连接数的过滤确实有效。
第 3 部分:关于零?
正如通常出现的那样,crossfilter 认为值为零的 bin 仍然存在,因此我们需要 "remove the empty bins"。但是,在这种情况下,我们向第一个假组添加了一个非标准方法,以允许过滤。 (我们本来可以在那里使用全局变量,但全局变量很乱。)
所以,我们需要"pass through" value_keys
方法:
function remove_empty_bins_pt(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.key !== '0';
});
},
value_keys: function() {
return source_group.value_keys();
}
};
}
packetPie
.group(remove_empty_bins_pt(value_keys_group(macPacketGroup)))
这里的另一个奇怪之处是我们正在过滤掉 key 零,这里是一个字符串!
或者,这里有一个更好的解决方案!在传递给value_keys_group
之前先做bin过滤,然后我们就可以使用普通的remove_empty_bins
!
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
//return Math.abs(d.value) > 0.00001; // if using floating-point numbers
return d.value !== 0; // if integers only
});
}
};
}
packetPie
.group(value_keys_group(remove_empty_bins(macPacketGroup)))