How To Group By Multiple Keys At The Same Time Using D3?
Solution 1:
As you already know d3.rollups()
will create nested arrays if you have more than one key:
If more than one key is specified, a nested Map [or array] is returned.
Therefore, as d3.rollups
doesn't fit your needs, I believe it's easier to create a plain JavaScript function (I'm aware of "using D3" in your title, but even in a D3 code nothing forbids us of writing plain JS solutions where D3 has none).
In the following example I'm purposefully writing a verbose function (with comments) so each part of it is clear, avoiding more complex features which could make it substantially short (but more cryptic). In this function I'm using reduce
, so the data array is looped only once. myKeys
is the array of keys you'll use to rollup.
Here is the function and the comments:
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
//Find the object in the acc with all 'myKeys' equivalent to the current
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
//if found, push the value for each key which is not in 'myKeys'
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
//if not found, push the current object with all non 'myKeys' keys as arrays
} else {
const copiedObject = Object.assign({}, c);//avoids mutation
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
Here is the demo:
const data = [{
a: 10,
b: 20,
c: 30,
d: 40
},
{
a: 10,
b: 20,
c: 31,
d: 41
},
{
a: 12,
b: 22,
c: 32,
d: 42
}
];
const keys = ["a", "b"];
console.log(groupedRollup(data, keys))
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
} else {
const copiedObject = Object.assign({}, c);
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
And here is a demo with a more complex data:
const data = [{
a: 10,
b: 20,
c: 30,
d: 40,
e: 5,
f: 19
},
{
a: 10,
b: 55,
c: 37,
d: 40,
e: 5,
f: 19
},
{
a: 10,
b: 20,
c: 31,
d: 48,
e: 5,
f: 18
},
{
a: 80,
b: 20,
c: 31,
d: 48,
e: 5,
f: 18
},
{
a: 1,
b: 2,
c: 3,
d: 8,
e: 5,
f: 9
},
{
a: 10,
b: 88,
c: 44,
d: 33,
e: 5,
f: 19
}
];
const keys = ["a", "e", "f"];
console.log(groupedRollup(data, keys))
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
} else {
const copiedObject = Object.assign({}, c);
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
Finally, pay attention that this function will push duplicated values (in the above example d: [40, 40, 33]
). If that's not what you want then just check for duplicates.
Solution 2:
With d3 v7 released, there is now a better way to do this using the new d3.flatRollup
.
const data = [
{ a: 10, b: 20, c: 30, d: 40 },
{ a: 10, b: 20, c: 31, d: 41 },
{ a: 12, b: 22, c: 32, d: 42 }
];
const result = d3.flatRollup(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => d.a,
d => d.b
);
console.log(result);
const flattened = result.map(([a, b, values]) => ({a, b, ...values}));
console.log(flattened);
<script src="https://cdn.jsdelivr.net/npm/d3-array@3.0.2/dist/d3-array.min.js"></script>
Solution 3:
The approach below allows you to remove the split
, but does not prevent the need to create a string for the compound key. In this case, using JSON.stringify({a: d.a, b: d.b})
instead of ${d.a} ${d.b}
, allows for the map
to return an object where the c
and d
properties can be assigned to the parse
of the key.
This preserves some of the 'd3-ishness' of your question and the utility of rollups
to deal with the creation of the arrays for c
and d
.
const data = [
{ a: 10, b: 20, c: 30, d: 40 },
{ a: 10, b: 20, c: 31, d: 41 },
{ a: 12, b: 22, c: 32, d: 42 }
];
const groups = d3.rollups(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => JSON.stringify({a: d.a, b: d.b}) // compare with `${d.a} ${d.b}`
).map(arr => Object.assign(JSON.parse(arr[0]), arr[1]));
console.log(groups);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
The approach can accommodate the extensibility of @Gerado Furtado's answer, but I fear it's getting a little hectic:
const data = [
{a: 10, b: 20, c: 30, d: 40, e: 5, f: 19},
{a: 10, b: 55, c: 37, d: 40, e: 5, f: 19},
{a: 10, b: 20, c: 31, d: 48, e: 5, f: 18},
{a: 80, b: 20, c: 31, d: 48, e: 5, f: 18},
{a: 1, b: 2, c: 3, d: 8, e: 5, f: 9},
{a: 10, b: 88, c: 44, d: 33, e: 5, f: 19}
];
const keys = ["a", "e", "f"];
const groupedRollup = (data, keys) => {
const others = Object.keys(data[0])
.filter(k => !keys.includes(k)); // finds b, c, d as not part of compound key
return d3.rollups(
data,
x => Object.assign(
{},
...others.map(k => {
return {[k]: x.map(d => d[k])} // dynamically create reducer
})
),
d => JSON.stringify(
Object.assign(
{},
...keys.map(k => {
return {[k]: d[k]} // dynamically add keys
})
)
) // and stringify for compound key
).map(arr => Object.fromEntries( // sorting the output object
Object.entries( // keys in alpha order
Object.assign(JSON.parse(arr[0]), arr[1])).sort() // same approach
)
);
}
console.log(groupedRollup(data, keys));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
There's some interesting talk about the introduction of use of InternMap
in rollups
and the associated functions - but I don't see either that it's ready, or that it's useful for what you are trying to do.
Post a Comment for "How To Group By Multiple Keys At The Same Time Using D3?"