How to group by multiple keys at the same time using D3? - javascript

This works, but I was wondering if there was a better way than creating a string with a and b and later splitting it:
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 }
];
d3.rollups(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => `${d.a} ${d.b}`
)
.map(([key, values]) => {
const [a, b] = key.split(' ');
return {a, b, ...values};
});
// OUTPUT
// [
// {a: "10", b: "20", c: [30, 31], d: [40, 41]},
// {a: "12", b: "22", c: [32], d: [42]}
// ]

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>

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.

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.

Related

Combining the content of multiple objects that have the same key with that key as the property in JavaScript

I want to combine the content of multiple objects that have the same key with that key as the property in JavaScript.
Objects:
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } };
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } };
Expected output:
{
med1: { a: 10, b: 12, e: 18, f: 20 },
med2: { c: 14, d: 16, g: 22, h: 24 }
}
I have tried to use both object spreading and Object.assign with no sucess.
With object spreading, since objects have no iterator this is returning an error. Object.assign would work however since the two objects to combine have the same key, the second object is overwriting the first.
You can iterate over the object keys and create a new object out of their aggregation.
You can use ES6 spread operator (...) which allows us to quickly copy all or part of an existing array or object into another array or object.
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } };
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } };
let resultObject = {};
Object.keys(cat1).map(key => { // iterate over the keys
resultObject = {
...resultObject,
[key]: {...cat1[key], ...cat2[key]} // merge two objects
}
return;
});
console.log(resultObject);
For combining two objects with a single level of indirection 👇
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } }
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } }
const merge = (o1, o2) =>
Object.keys(o1)
.reduce((p, key) =>
(p[key] = { ...o1[key], ...o2[key] }, p), {})
console.log(merge(cat1, cat2))

problem with reduce and memoisation in javascript hardstructure

i have to do a task, where i need to iterate over hardstructure with nested functions arrays objects and other data types. get all the numbers there are in string or not in string into the array and then with reduce method i need to calculate their sum, after calculating their sum i need to do memoisation on this function. but something doesnt work with my reduce even though i have written the most simple task there is, calculating the sum of numbers. my reduce doesnt really calculate the right way it skips some parts of the structure and doesnt gather all of the nubmers also it doesnt really calculate right i think
const ex1 = () => ({ a: 13 });
const ex2 = () => [1, 2, 3, 4, 5, "a,,,,,,", 2, 2, 2];
const ex3 = () => [...ex2()];
const ex4 = new Set([1, 2, 3, 4]);
const hardStructure = [
{
a: 1,
b: "2",
c: [
{
a: "123",
b: "#333z$34",
c: "122,333,4444,5555",
z: ex4,
d: [
[
123,
1,
"1111",
23323,
1222,
55,
[1212, 1212, 1212],
{
a: 111,
b: null,
c: () => 111,
d: 22,
e: "e",
f: () => "fffqqq",
},
],
[
2212,
1,
"111211",
23,
121,
22,
[33, 3, 3],
{
a: 3,
b: null,
c: () => 11221,
d: 2112,
e: "e11",
f: () => "fffqqq",
g: 2,
},
],
[
11,
22,
"dsds",
{
a: {
b: {
c: {
d: {
a: 1,
b: [ex1()],
c: {
a: {
b: {
c: {
d: {
a: new Map([
["a2134", 2123123],
["b", 3],
["c", 3],
]),
b: ex2,
c: [...ex3(), "1", 1],
},
},
},
},
},
},
},
},
},
},
],
],
},
],
},
{
d: [
[
123,
1,
"1111",
23323,
1222,
55,
[121322332, 12132322, 12323212],
{
a: 111,
b: null,
c: () => 1123231,
d: 22,
e: "e",
f: () => "fffqqq",
},
],
[
2212,
1,
"111211",
23,
121,
22,
[33, 3, 3],
{
a: 3,
b: null,
c: () => 1123221,
d: 211,
e: "e1231",
f: () => "fffqq1232312123q",
g: 2123,
},
],
[
11,
22,
"dsds",
{
a: {
b: {
c: {
d: {
a: 1,
b: [ex1()],
c: {
a: {
b: {
c: {
d: {
a: new Map([
[true, 222312],
[2, 322],
["c2", 32],
[() => {}, 32],
]),
b: ex2,
c: [...ex3(), "1121123", 1],
},
},
},
},
},
},
},
},
},
},
],
],
},
{
a: {
b: {
c: {
d: {
a: 112312,
b: [1],
c: {
a: {
b: {
c: {
d: {
a: "",
b: "",
c: "",
v: "v12312323",
},
},
},
},
},
},
},
},
},
},
];
const numbersArr = [];
const iterate = (hardStructure) => {
if (hardStructure === null || hardStructure === undefined) {
return [];
}
if (
hardStructure.constructor === new Set().constructor ||
hardStructure.constructor === new Map().constructor
) {
console.log("console from map or set");
hardStructure.forEach((element) => {
// console.log("123", element);
return iterate(element);
});
}
if (typeof hardStructure === "function") {
return iterate(hardStructure());
}
if (Array.isArray(hardStructure)) {
hardStructure.map((items) => {
return iterate(items);
});
}
if (typeof hardStructure === "object" && !Array.isArray(hardStructure)) {
for (let key in hardStructure) {
// console.log(hardStructure[key]);
return iterate(hardStructure[key]);
}
}
if (typeof hardStructure !== "string") {
const stringItem = String(hardStructure);
for (let i = 0; i < stringItem.length; i++) {
if (!isNaN(+stringItem)) {
numbersArr.push(+stringItem);
}
}
}
for (let i = 0; i < hardStructure.length; i++) {
// console.log(i);
if (!isNaN(+hardStructure[i])) {
// console.log("12345678910", isNaN(+hardStructure[2]));
numbersArr.push(+hardStructure[i]);
}
}
const sumWithInitial = numbersArr.reduce((accumulator, currentValue) => {
console.log(accumulator,currentValue)
return accumulator + currentValue;
},0);
// console.log(numbersArr)
console.log(sumWithInitial);
return sumWithInitial;
};
iterate(hardStructure);
Your requirements aren't entirely clear, especially as to handling of String and Function properties. This solution assumes that you want to call functions and that you can use parseInt for Strings. If you need to change that, it should be clear enough how and where to do so:
const sumNumbers = (xs) =>
xs == null
? 0
: xs .constructor == String
// ? parseInt (xs, 10) || 0 // original version not enough. Below might not be so either
? xs .includes (',') ? sumNumbers (xs .split (',')) : Number (xs .replaceAll (/\D/g, ''))
: xs .constructor == Number
? xs
: xs .constructor === Array
? xs .map (sumNumbers) .reduce ((a, b) => a + b, 0)
: xs .constructor === Set
? sumNumbers ([...xs])
: xs .constructor === Map
? sumNumbers ([...xs .values()])
: xs .constructor === Function
? sumNumbers (xs ()) // or just 0?
: xs .constructor == Object
? sumNumbers (Object .values (xs))
// TODO: Other types possible here?
: 0
const ex1 = () => ({a: 13}),
ex2 = () => [1, 2, 3, 4, 5, "a,,,,,,", 2, 2, 2],
ex3 = () => [...[1, 2, 3, 4, 5, "a,,,,,,", 2, 2, 2]],
ex4 = new Set ([1, 2, 3, 4]),
hardStructure = [{a: 1, b: "2", c: [{a: "123", b: "#333z$34", c: "122,333,4444,5555", z: ex4, d: [[123, 1, "1111", 23323, 1222, 55, [1212, 1212, 1212], {a: 111, b: null, c: () => 111, d: 22, e: "e", f: () => "fffqqq"}], [2212, 1, "111211", 23, 121, 22, [33, 3, 3], {a: 3, b: null, c: () => 11221, d: 2112, e: "e11", f: () => "fffqqq", g: 2}], [11, 22, "dsds", {a: {b: {c: {d: {a: 1, b: [{a: 13}], c: {a: {b: {c: {d: {a: new Map ([["a2134", 2123123], ["b", 3], ["c", 3]]), b: ex2, c: [...ex3(), "1", 1]}}}}}}}}}}]]}]}, {d: [[123, 1, "1111", 23323, 1222, 55, [121322332, 12132322, 12323212], {a: 111, b: null, c: () => 1123231, d: 22, e: "e", f: () => "fffqqq"}], [2212, 1, "111211", 23, 121, 22, [33, 3 , 3], {a: 3, b: null, c: () => 1123221, d: 211, e: "e1231", f: ()=> "fffqq1232312123q", g: 2123}], [11, 22, "dsds", {a: {b: {c: {d: {a: 1, b: [{a: 13}], c: {a: {b: {c: {d: {a: new Map ([[true, 222312], [2, 322], ["c2", 32], [() => {}, 32]]), b: ex2, c: [...ex3 (), "1121123", 1]}}}}}}}}}}]]}, {a: {b: {c: {d: {a: 112312, b: [1], c: {a: {b: {c: {d: {a: "", b: "", c: "", v: "v12312323"}}}}}}}}}}]
console .log (sumNumbers (hardStructure))
We have four base cases:
on a nil value (null or undefined), we return 0
on a Number, we return its value
on a String, we call parseInt on it and return the value. This is the part I think you would most likely need to change
on an unknown type (perhaps a regular expression or a date), we return 0
The others are recursive case:
for an Array, we recursively call our function on each element and then total them together.
for a Function, we call the function with no arguments, and recur on the result.
for our other known types (Object, Set, Map), we extract its values into an Array and recur on the result.

Finding all the keys in the array of objects

Given that I have an array that is return from database that contains object,
var jsObjects = [
{a: 1, b: 2},
{a: 3, b: 4, c: 66},
{a: 5, b: 6, c: 55, d: 66},
{a: 7, b: 8, c: 12, e: 15}
];
How can I get all the keys of the object? I've been using this to obtain the keys, however I notice that index 0 wont always have all the keys. Hence problem lays.
let columns = Object.keys(jsObjects[0]),
However, the first index won't always have all the columns.
My desired output:
["a", "b", "c", "d", "e"]
Sets are good at removing duplicates:
const jsObjects = [
{a: 1, b: 2},
{a: 3, b: 4, c: 66},
{a: 5, b: 6, c: 55, d: 66},
{a: 7, b: 8, c: 12, e: 15}
];
const keys = [...new Set(jsObjects.flatMap(Object.keys))];
console.log(keys);
You could create an object with the count of the keys and get the keys from counts.
const
objects = [{ a: 1, b: 2 }, { a: 3, b: 4, c: 66 }, { a: 5, b: 6, c: 55, d: 66 }, { a: 7, b: 8, c: 12, e: 15 }],
result = Object.keys(objects.reduce((r, o) => {
Object.keys(o).forEach(k => r[k] = true);
return r;
}));
console.log(result);
You can use a Set(), which is like an array with the exception that it doesn't allow multiple occurances of elements.
var jsObjects = [
{a: 1, b: 2},
{a: 3, b: 4, c: 66},
{a: 5, b: 6, c: 55, d: 66},
{a: 7, b: 8, c: 12, e: 15}
];
var keys = new Set();
for(object of jsObjects) {
for(key in object) {
keys = keys.add(key);
}
}
for (const key of keys) {
console.log(key);
}
I tried this solution: iterating on jsObjects elements and pushing keys
into columns array if not present.
var jsObjects = [
{a: 1, b: 2},
{a: 3, b: 4, c: 66},
{a: 5, b: 6, c: 55, d: 66},
{a: 7, b: 8, c: 12, e: 15}
];
let columns = [];
jsObjects.forEach(
o => {
Object.keys(o).forEach(
t => {
if (columns.indexOf(t) < 0 ) {
columns.push(t);
}
}
)
}
)
console.log(columns);

Delete multiple keys from array of Object

Let's say we have an array of object:
var obj = [{a: 1, b: 2, c: 3, d: 4, e: 5 },{a: 6, b: 7, c:
8, d: 9, e: 0 }];
and we want to delete key c,e from both of the objects.
How can it be done? One of the methods I found is:
['c', 'e'].forEach(e => delete obj[e]); //for object
Is there any other way so we don't have to use double for loop.
One way to do it is to use .map() together with object destructuring:
var obj = [
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
{ a: 6, b: 7, c: 8, d: 9, e: 0 },
];
var newObj = obj.map(({ c, e, ...rest }) => rest);
console.log(newObj)
This will create a new array with new objects which contain all of the other keys except for c and e.
You have 2 options to resolve it:
By using object destructuring: map(({ a,b,c,d,e }) => ({a,b,d})
Enhance option 1 by using using [Rest parameters] { c, e, ...rest }
Object destructuring like below
const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 } var {c, e} = obj; // c = 3, e = 5
With option 2, you will have c,e implicit name and the remaining items named rest. After that, you just need to get rest items.
Option 1
var obj =
[
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
{ a: 6, b: 7, c: 8, d: 9, e: 0 },
];
console.log(obj.map(({ a,b,c,d,e }) => ({a,b,d})));
Option 2
var obj =
[
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
{ a: 6, b: 7, c: 8, d: 9, e: 0 },
];
console.log(obj.map(({ c, e, ...rest }) => rest));
// ...rest: the same as `a,b,d`

Javascript findIndex for duplicate values

JavaScript findIndex returns the very first finded index in case of duplicate values
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const index = arr.findIndex(m => m.a === 10)
console.log(index);
The above code will only return 0 index.
What should I do to get index 2 as well.
You could filter the keys of the array like this:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const indices = [...arr.keys()].filter(i => arr[i].a === 10)
console.log(indices)
Or, just use a for loop
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const output = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i].a === 10)
output.push(i)
}
console.log(output)
arr.reduce((result, current, index) => {
return result.concat(current.a == 10 ? [index]: [])
})
You can use of Array.reduce to loop on your arr and returns every indexes
Single line :
const arr = [{
a: 10,
b: 20,
c: 30,
}, {
a: 15,
b: 25,
c: 32,
}, {
a: 10,
b: 23,
c: 350,
}]
// Value to check for
const v = 10;
// we loop on every entry of the arr
// we start with an empty array and look if the entries
// contains the value 10, if they do, we push the index of the
// entry in the array that was empty
//
// we do that for every entry of arr, when we treat all, we return
// the list of matching entries
const idx = arr.reduce((tmp, x, xi) => (x.a === v ? [...tmp, xi] : tmp), []);
console.log(idx);
Explained :
const arr = [{
a: 10,
b: 20,
c: 30,
}, {
a: 15,
b: 25,
c: 32,
}, {
a: 10,
b: 23,
c: 350,
}]
const valToCheck = 10;
const indexes = arr.reduce((tmp, x, xi) => {
if (x.a === valToCheck) {
tmp.push(xi);
}
return tmp;
}, []);
console.log(indexes);
Use map and filter:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}];
const res = arr.map(({ a }, i) => a == 10 ? i : "").filter(String);
console.log(res);
You could map the indices of the found items and filter only valid indices.
const
array = [{ a: 10, b: 20, c: 30 }, { a: 15, b: 25, c: 32 }, { a: 10, b: 23, c: 350 }],
indices = array
.map((m, i) => m.a === 10 ? i : -1)
.filter(i => i != -1);
console.log(indices);
You can just iterate through array and push matching index in new array.
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
let p = []
arr.forEach((ele,index)=> ele.a===10 ? p.push(index) : '')
console.log(p)
You can try like this:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}];
const indices = arr.map((x, index) => {
if (x.a === 10) {
return index;
}
})
.filter(x => x !== undefined);
console.log(indices);
If you want a function which works directly with Array, add this in Array.prototype
Array.prototype.findAllIndexes = function(iteratee){
var resultArray = [];
this.forEach(function(element) {
var validated = iteratee.call(this, element);
if(validated) {
resultArray.push(element);
}
});
return resultArray;
}
Then you can use it anywher you want with array object
(please handle your corner cases based od data type you use)
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const index = arr.findAllIndexes(m => m.a === 10)
console.log(index);
You can use some helper libraries to this kind of stuff like lodash.js, you can include them in your project.

Categories