Selectively flattening a nested JSON structure - javascript

So this is a problem that I have no idea where to even start so even just a pointer in the right direction would be great.
So I have data that looks like so:
data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
{
...
},
{
...
},
...
]
}
}
Now from the example, within 'agg' there are N 'agg1' results, within each 'agg1' result there is a 'keyWeWant'. Each 'agg1' result also has a list of 'some_nested_agg' results which each contain a 'keyWeWant2'. Each 'keyWeWant2' value is associated a single 'keyWeWant' value somewhere up in the hierarchy. Similarly each 'keyWeWant2' also contains a set of results for 'some_nested_agg2' (not a list but rather a map this time). Each of the set of results contains a 'keyWeWant3'.
Now I want to flatten this structure while still preserving the association between 'keyWeWant', 'keyWeWant2', and 'keyWeWant3' (I'm essentially de-normalizing) to get something like so:
What I want the function to look like:
[
{
"keyWeWant" : "*-20",
"keyWeWant2" : 20,
"keyWeWant3" : 2.857142857142857
},
{
"keyWeWant" : "*-20",
"keyWeWant2" : 25,
"keyWeWant3" : 6.375
},
{
...
},
{
...
}
]
This is an example where there is only depth 3 but there could be arbitrary depth with some nested values being lists and some being arrays/list.
What I would like to do is write a function to take in the keys I want and where to find them, and then go get the keys and denormalize.
Something that looks like:
function_name(data_map, {
"keyWeWant" : ['agg', 'agg1'],
"keyWeWant2" : ['agg', 'agg1', 'some_nested_agg'],
"keyWeWant" : ['agg', 'agg1', 'some_nested_agg', 'some_nested_agg2']
})
Any ideas? I'm familiar with Java, Clojure, Java-script, and Python and am just looking for a way to solve this that's relatively simple.

Here is a JavaScript (ES6) function you could use:
function flatten(data, keys) {
var key = keys[0];
if (key in data)
keys = keys.slice(1);
var res = keys.length && Object.keys(data)
.map( key => data[key] )
.filter( val => Object(val) === val )
.reduce( (res, val) => res.concat(flatten(val, keys)), []);
return !(key in data) ? res
: (res || [{}]).map ( obj => Object.assign(obj, { [key]: data[key] }) );
}
// Sample data
var data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
]
}
};
// Flatten it by array of keys
var res = flatten(data, ['keyWeWant', 'keyWeWant2', 'keyWeWant3']);
// Output result
console.log(res);
Alternative using paths
As noted in comments, the above code does not use path information; it just looks in all arrays. This could be an issue if the keys being looked for also occur in paths that should be ignored.
The following alternative will use path information, which should be passed as an array of sub-arrays, where each sub-array first lists the path keys, and as last element the value key to be retained:
function flatten(data, [path, ...paths]) {
return path && (
Array.isArray(data)
? data.reduce( (res, item) => res.concat(flatten(item, arguments[1])), [] )
: path[0] in data && (
path.length > 1
? flatten(data[path[0]], [path.slice(1), ...paths])
: (flatten(data, paths) || [{}]).map (
item => Object.assign(item, { [path[0]]: data[path[0]] })
)
)
);
}
// Sample data
var data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
]
}
};
// Flatten it by array of keys
var res = flatten(data, [
['agg', 'agg1', 'keyWeWant'],
['some_nested_agg', 'keyWeWant2'],
['some_nested_agg2', 'keyWeWant3']]);
// Output result
console.log(res);

There is probably a better way to solve this particular problem (using some ElasticSearch library or something), but here's a solution in Clojure using your requested input and output data formats.
I placed this test data in a file called data.json:
{
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}]
}]}
}
Then Cheshire JSON library parses the data to a Clojure data structure:
(use '[cheshire.core :as cheshire])
(def my-data (-> "data.json" slurp cheshire/parse-string))
Next the paths to get are defined as follows:
(def my-data-map
{"keyWeWant" ["agg", "agg1"],
"keyWeWant2" ["agg", "agg1", "some_nested_agg"],
"keyWeWant3" ["agg", "agg1", "some_nested_agg", "some_nested_agg2"]})
It is your data_map above without ":", single quotes changed to double quotes and the last "keyWeWant" changed to "keyWeWant3".
find-nested below has the semantics of Clojure's get-in, only then it works on maps with vectors, and returns all values instead of one.
When find-nested is given a search vector it finds all values in a nested map where some values can consist of a vector with a list of maps. Every map in the vector is checked.
(defn find-nested
"Finds all values in a coll consisting of maps and vectors.
All values are returned in a tree structure:
i.e, in your problem it returns (20 25) if you call it with
(find-nested ['agg', 'agg1', 'some_nested_agg', 'keyWeWant2']
my-data).
Returns nil if not found."
[ks c]
(let [k (first ks)]
(cond (nil? k) c
(map? c) (find-nested (rest ks) (get c k))
(vector? c) (if-let [e (-> c first (get k))]
(if (string? e) e ; do not map over chars in str
(map (partial find-nested (rest ks)) e))
(find-nested ks (into [] (rest c)))) ; create vec again
:else nil)))
find-nested finds the values for a search path:
(find-nested ["agg", "agg1", "some_nested_agg", "keyWeWant2"] my-data)
; => (20 25)
If all the paths towards the "keyWeWant's are mapped over my-data these are the slices of a tree:
(*-20.0
(20 25)
(2.857142857142857 6.375))
The structure you ask for (all end results with paths getting there) can be obtained from this tree in function-name like this:
(defn function-name
"Transforms data d by finding (nested keys) via data-map m in d and
flattening the structure."
[d m]
(let [tree (map #(find-nested (conj (second %) (first %)) d) m)
leaves (last tree)
leaf-indices (range (count leaves))
results (for [index leaf-indices]
(map (fn [slice]
(if (string? slice)
slice
(loop [node (nth slice index)]
(if node
node
(recur (nth slice (dec index)))))))
tree))
results-with-paths (mapv #(zipmap (keys m) %) results)
json (cheshire/encode results-with-paths)]
json))
results uses a loop to step back if a leaf-index is larger than that particular slice. I think it will work out for deeper nested structures as well -if a next slice is always double the size of a previous slice or the same size it should work out -, but I have not tested it.
Calling (function-name my-data my-data-map) leads to a JSON string in your requested format:
[{
"keyWeWant": "-20.0",
"keyWeWant2": 20,
"keyWeWant3": 2.857142857142857 }
{
"keyWeWant": "-20.0",
"keyWeWant2" 25,
"keyWeWant3" 6.375 }]
/edit
I see you were looking for a relatively simple solution, that this is not. :-) maybe there is one without having it available in a library. I would be glad to find out how it can be simplified.

Related

Nested object of unequal length into array of objects

So I have an interesting problem which I have been able to solve, but my solution is not elegant in any way or form, so I was wondering what others could come up with :)
The issue is converting this response here
const response = {
"device": {
"name": "Foo",
"type": "Bar",
"telemetry": [
{
"timeStamp": "2022-06-01T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-02T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-03T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-04T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-05T00:00:00.000Z",
"temperature": 100,
"pressure": 50
}
]
}
};
Given this selection criteria
const fields = ['device/name', 'device/telemetry/timeStamp', 'device/telemetry/temperature']
and the goal is to return something like this
[
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-01T00:00:00.000Z", "device/telemetry/temperature": 100},
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-02T00:00:00.000Z", "device/telemetry/temperature": 100},
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-03T00:00:00.000Z", "device/telemetry/temperature": 100},
...,
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-05T00:00:00.000Z", "device/telemetry/temperature": 100},
]
If you are interested, here is my horrible brute force solution, not that familiar with typescript yet, so please forgive the horribleness :D
EDIT #1
So some clarifications might be needed. The response can be of completely different format, so we can't use our knowledge of how the response looks like now, the depth can also be much deeper.
What we can assume though is that even if there are multiple arrays in the reponse (like another telemetry array called superTelemetry) then the selection criteria will only choose from one of these arrays, never both :)
function createRecord(key: string, value: any){
return new Map<string, any>([[key, value]])
}
function getNestedData (data: any, fieldPath: string, records: Map<string, any[]>=new Map<string, any[]>()) {
let dataPoints: any = [];
const paths = fieldPath.split('/')
paths.forEach((key, idx, arr) => {
if(Array.isArray(data)){
data.forEach(
(row: any) => {
dataPoints.push(row[key])
}
)
} else {
data = data[key]
if(idx + 1== paths.length){
dataPoints.push(data);
}
}
})
records.set(fieldPath, dataPoints)
return records
}
function getNestedFields(data: any, fieldPaths: string[]){
let records: Map<string, any>[] = []
let dataset: Map<string, any[]> = new Map<string, any[]>()
let maxLength = 0;
// Fetch all the fields
fieldPaths.forEach((fieldPath) => {
dataset = getNestedData(data, fieldPath, dataset)
const thisLength = dataset.get(fieldPath)!.length;
maxLength = thisLength > maxLength ? thisLength : maxLength;
})
for(let i=0; i<maxLength; i++){
let record: Map<string, any> = new Map<string, any>()
for(let [key, value] of dataset){
const maxIdx = value.length - 1;
record.set(key, value[i > maxIdx ? maxIdx : i])
}
records.push(record)
}
// Normalize into records
return records
}
As per my understanding you are looking for a solution to construct the desired result as per the post. If Yes, you can achieve this by using Array.map() along with the Array.forEach() method.
Try this :
const response = {
"device": {
"name": "Foo",
"type": "Bar",
"telemetry": [
{
"timeStamp": "2022-06-01T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-02T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-03T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-04T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-05T00:00:00.000Z",
"temperature": 100,
"pressure": 50
}
]
}
};
const fields = ['device/name', 'device/telemetry/timeStamp', 'device/telemetry/temperature'];
const res = response.device.telemetry.map(obj => {
const o = {};
fields.forEach(item => {
const splittedItem = item.split('/');
o[item] = (splittedItem.length === 2) ? response[splittedItem[0]][splittedItem[1]] : obj[splittedItem[2]];
});
return o;
})
console.log(res);
In what follows I will be concerned with just the implementation and runtime behavior, and not so much the types. I've given things very loose typings like any and string instead of the relevant generic object types. Here goes:
function getNestedFields(data: any, paths: string[]): any[] {
If data is an array, we want to perform getNestedFields() on each element of the array, and then concatenate the results together into one big array. So the first thing we do is check for that and make a recursive call:
if (Array.isArray(data)) return data.flatMap(v => getNestedFields(v, paths));
Now that we know data is not an array, we want to start gathering the pieces of the answer. If paths is, say, ['foo/bar', 'foo/baz/qux', 'x/y', 'x/z'], then we want to make recursive calls to getNestedFields(data.foo, ["bar", "baz/qux"]) and to getNestedFields(data.x, ["y", "z"]). In order to do this we have to split each path element at its first slash "/", and collect the results into a new object whose keys are the part to the left of the slash and whose values are arrays of parts to the right. In this example it would be {foo: ["bar", "baz/qux"], x: ["y", "z"]}.
Some important edge cases: for every element of paths with no slash, then we have a key with an empty value... that is, ["foo"] should result in a call like getNestedFields(data.foo, [""]). And if there is an element of paths that's just the empty string "", then we don't want to do a recursive call; the empty path is the base case and implies that we're asking about data itself. That is, instead of a recursive call, we can just return [{"": data}]. So we need to keep track of the empty path (hence the emptyPathInList variable below).
Here's how it looks:
const pathMappings: Record<string, string[]> = {};
let emptyPathInList = false;
paths.forEach(path => {
if (!path) {
emptyPathInList = true;
} else {
let slashIdx = path.indexOf("/");
if (slashIdx < 0) slashIdx = path.length;
const key = path.substring(0, slashIdx);
const restOfPath = path.substring(slashIdx + 1);
if (!(key in pathMappings)) pathMappings[key] = [];
pathMappings[key].push(restOfPath);
}
})
Now, for each key-value pair in pathMappings (with key key and with value restsOfPath) we need to call getNestedFields() recursively... the results will be an array of objects whose keys are relative to data[key], so we need to prepend key and a slash to their keys. Edge cases: if there's an empty path we shouldn't add a slash. And if data` is nullish then we will have a runtime error recursing down into it, so we might want to do something else there (although a runtime error might be fine since it's a weird input):
const subentries = Object.entries(pathMappings).map(([key, restsOfPath]) =>
(data == null) ? [{}] : // <-- don't recurse down into nullish data
getNestedFields(data[key], restsOfPath)
.map(nestedFields =>
Object.fromEntries(Object.entries(nestedFields)
.map(([path, value]) =>
[key + (path ? "/" : "") + path, value])))
)
Now subentries is an array of all the separate recursive call results, with the proper keys. We want to add one more entry correpsonding to data if emptyPathInList is true:
if (emptyPathInList) subentries.push([{ "": data }]);
And now we need to combine these sub-entries by taking their Cartesian product and spreading into a single object for each entry. By Cartesian product I mean that if subentries looks like [[a,b],[c,d,e],[f]] then I need to get [[a,c,f],[a,d,f],[a,e,f],[b,c,f],[b,d,f],[b,e,f]], and then for each of those we spread into single entries. Here's that:
return subentries.reduce((a, v) => v.flatMap(vi => a.map(ai => ({ ...ai, ...vi }))), [{}])
}
Okay, so let's test it out:
console.log(getNestedFields(response, fields));
/* [{
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-01T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-02T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-03T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-04T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-05T00:00:00.000Z",
"device/telemetry/temperature": 100
}] */
That's what you wanted. Even though you said you will never walk into different arrays, this version should support that:
console.log(getNestedFields({
a: [{ b: 1 }, { b: 2 }],
c: [{ d: 3 }, { d: 4 }]
}, ["a/b", "c/d"]))
/* [
{ "a/b": 1, "c/d": 3 },
{ "a/b": 2, "c/d": 3 },
{ "a/b": 1, "c/d": 4 },
{ "a/b": 2, "c/d": 4 }
]*/
There are probably all kinds of crazy edge cases, so anyone using this should test thoroughly.
Playground link to code

Convert object with nested objects into array of objects,re- groupped by inner object's keys

I have the Following JavaScript Object JSON1
{
"1": {
"Average": 32.31,
"Count": 19,
"Sum": 32.6,
"Color": "red"
},
"2": {
"Average": 32.72,
"Count": 18,
"Sum": 32.96,
"Color": "blue"
},
"3": {
"Average": 31.4,
"Count": 18,
"Sum": 31.48,
"Color": "green"
}
}
and I want to convert into the following format using javascript ES6 methods. JSON2
[{
"title": "Average",
"val1": 32.31,
"val2": 32.72,
"val3": 31.4
}, {
"title": "Count",
"val1": 19,
"val2": 18,
"val3": 18
}, {
"title": "Sum",
"val1": 32.6,
"val2": 32.96,
"val3": 31.48
}, {
"title": "Color",
"val1": "red",
"val2": "blue",
"val3": "green"
}]
Object.keys(json1).forEach((item, index) => {
let statsList = [];
Object.keys(json1[item]).forEach(objItem => {
statsList.push({
title: objItem,
val1: boxObj[1][objItem],
val2: boxObj[2][objItem],
val3: boxObj[3][objItem]
});
});
console.log(statsList)
});
var json1 = {
"1": {
"Average": 32.31,
"Count": 19,
"Sum": 32.6,
"Color": "red"
},
"2": {
"Average": 32.72,
"Count": 18,
"Sum": 32.96,
"Color": "blue"
},
"3": {
"Average": 31.4,
"Count": 18,
"Sum": 31.48,
"Color": "green"
}
};
Object.keys(json1).forEach((item, index) => {
let statsList = [];
Object.keys(json1[item]).forEach(objItem => {
statsList.push({
title: objItem,
val1: boxObj[1][objItem],
val2: boxObj[2][objItem],
val3: boxObj[3][objItem]
});
});
console.log(statsList)
});
Here in the JSON1, the Number of objects can be any number. It has to format it Dynamically. In the JSON2, Instead of val1,val2 anything can be used uniquely identifiable keys in all the objects present in the array. I have tried using forEach, I was able to achieve it with static keys provided, and with the multiple looping statements. I just want with dynamic keys and avoiding multiple loops and I want to know what is the best and easiest way to do this formatting in Javascript. Advance Thanks for your help.
You may traverse source Object.values() with Array.prototype.reduce() to make up an object that will map each category to all possible values.
Then, you may Array.prototype.map() resulting Object.entries() to return an array of objects with desired structure:
const src = {"1":{"Average":32.31,"Count":19,"Sum":32.6,"Color":"red"},"2":{"Average":32.72,"Count":18,"Sum":32.96,"Color":"blue"},"3":{"Average":31.4,"Count":18,"Sum":31.48,"Color":"green"}},
resultMap = Object
.values(src)
.reduce((r,o,i) => (
Object
.entries(o)
.forEach(([key,value]) =>
(r[key]=r[key]||{}, r[key][`value${i+1}`] = value))
,r),{}),
result = Object
.entries(resultMap)
.map(([name,{...values}]) => ({name,...values}))
console.log(result)
.as-console-wrapper{min-height:100%;}
If only unique items in each category are required, you may somewhat modify above solution, making use of Set():
const src = {"1":{"Average":32.31,"Count":19,"Sum":32.6,"Color":"red"},"2":{"Average":32.72,"Count":18,"Sum":32.96,"Color":"blue"},"3":{"Average":31.4,"Count":18,"Sum":31.48,"Color":"green"}},
resultMap = Object
.values(src)
.reduce((r,o,i) => (
Object
.entries(o)
.forEach(([key,value]) =>
(r[key]=r[key]||(new Set()), r[key].add(value)))
,r),{}),
result = Object
.entries(resultMap)
.map(([name,values]) => ({
name,
...([...values].reduce((r,v,i) => (r[`value${i+1}`]=v, r),{}))
}))
console.log(result)
.as-console-wrapper{min-height:100%;}
You may see your data as a matrix: each row is a feature and each column a dimension
What you have is the data expressed a rows
And what you would like is the data expressed as columns
So what you want is the transpose of your matrix
Let's recall that taking the transpose can be done as: T[j][i] = M[i][j] forall i,j
In your case
for each index of row i in M
for each index of column j in M
// T[j] is your aggregated record
// i being the index of the row has to be renamed 'val'+i
// and you add the property: title: columnOfJ to record T[j]
T[j]['val' + i] = M[i][j]
T[j].title = col corresponding to index j
const M = {"1":{"Average":32.31,"Count":19,"Sum":32.6,"Color":"red"},"2":{"Average":32.72,"Count":18,"Sum":32.96,"Color":"blue"},"3":{"Average":31.4,"Count":18,"Sum":31.48,"Color":"green"}}
const T = []
Object.entries(M).forEach(([i, Mi]) => {
Object.keys(Mi).forEach((col, j) => {
T[j] = T[j] || {}
T[j].title = col // we just put the title before so it is the first entry in your record...
T[j]['val' + i] = Mi[col]
})
})
console.log(T)

Issue with mapping Object of Arrays

I am trying to set up a block of code to prepare to setState, however, I'm running into an issue mapping a list in the render section as reactjs is telling me map is not a function. I don't think I'm setting this up correctly initially and it should be an array of objects instead of object arrays.
My goal is to set up a list. The names on the left side. The sum total of ondinResult and cmfResult on the right side. Below is the result I should expect:
This is how the data from the API is after calling the GET request:
"fileResults": {
"incFiles": [
{
"assetManagerId": 5,
"name": "BlackRock",
"odinResult": {
"total": 5,
"success": 2,
"error": 3
},
"cmfResult": {
"total": 0,
"success": 0,
"error": 0
}
},
{
"assetManagerId": 8,
"name": "Barings",
"odinResult": {
"total": 0,
"success": 0,
"error": 0
},
"cmfResult": {
"total": 10,
"success": 8,
"error": 2
}
},
{
"assetManagerId": 11,
"name": "AIM Derivatives",
"odinResult": {
"total": 6,
"success": 4,
"error": 2
},
"cmfResult": {
"total": 0,
"success": 0,
"error": 0
}
},
{
"assetManagerId": 11,
"name": "AIM Derivatives",
"odinResult": {
"total": 0,
"success": 0,
"error": 0
},
"cmfResult": {
"total": 8,
"success": 2,
"error": 6
}
}
],
"odinTotal": 11,
"cmfTotal": 18
},
My code block I'm currently setting up before setState:
//mapping odin and cmf results then adding the totals together
let odinTotal = response.data.fileResults.incFiles.map(item => item.odinResult.total)
let cmfTotal = response.data.fileResults.incFiles.map(item => item.cmfResult.total)
const legendData = {
labels: response.data.fileResults.incFiles.map(item => item.name),
totals: odinTotal.map(function (num, idx) {
return num + cmfTotal[idx]
})
}
My result is this from the above:
After deconstructing my state I tried to map it out in under render but get an error of: "Cannot read property 'map' of undefined."
<ul>
{legendData.labels.map(item => (
<li key={item}>{item}</li>
))}
</ul>
It sounds like you are fetching some data when the component mounts, so you need to likely provide some initial empty array value to legendData's labels array.
state = {
legendData: {
labels: [],
totals: [],
},
}
Then as long as your data loading logic also returns and updates state with an array your render logic will work.
Another option is to use a guard pattern on the mapping function to ensure the labels property exists and has a length property.
<ul>
{legendData && legendData.labels.length && legendData.labels.map(item => (
<li key={item}>{item}</li>
))}
</ul>
A react component in which you use map should always have a Intial state to empty array or empty object based on requirement.
check for the condition:
{legendData && legendData.labels.length ?
legendData.labels.map(item =>(
<li key={item}>{item}</li>
)) : null}

How to sort just a specific part of an already sorted 2D Array depending on new values. But only if the first sorted values match in Javascript

I have the following problem:
I have an array with objects in it.
Every object has a score and a rank, like this:
[
{ "score": 20, "rank": 12 },
{ "score": 20, "rank": 7 },
{ "score": 34, "rank": 4 }
]
First of all, I sort this descending by the score and store it into a 2-dimensional array.
[34, 4]
[20, 12]
[20, 7]
But now, if there is the same score twice or more often I want those to be sorted by the rank. So whatever has the lowest rank will have a smaller index number. Resulting in:
[34, 4]
[20, 7]
[20, 12]
I really don't know how to do this, I made some approaches, but they are a way to bad to mention them.
You can check if the difference of score of two objects is 0 then return the difference of rank otherwise return difference of score
const arr = [
{ "score": 20, "rank": 12 },
{ "score": 20, "rank": 7 },
{ "score": 34, "rank": 4 }
]
let res = [...arr]
.sort((a,b) => (b.score - a.score) || (a.rank - b.rank))
.map(x => [x.score,x.rank]);
console.log(res)
Just use lodash and orderby 2 fields.
You could sort the array first and then just map over for the Object.values:
const arr = [
{ "score": 20, "rank": 12 },
{ "score": 20, "rank": 7 },
{ "score": 34, "rank": 4 }
]
let result = arr.sort((a,b) => (b.score - a.score) || (a.rank - b.rank))
.map(x => Object.values(x))
console.log(result)

Organising object data by timestep, so as to chart multiple sensor readings against time using Chart JS

I am creating a line chart using the Chart JS library. The data I am using is coming from two different temperature sensors (sensor 1 and sensor 2), that may not necessarily have the same timestamps. I want to split the data into 3 arrays:
1. Sensor 1 readings
2. Sensor 2 readings
3. Timestamps
If there is no value at a particular timestep then for one of the sensors for the other the array should have a blank value like at index 1 of this array: [0, ,1,2]
Here is an example of the data:
zdata =
{ "data": [
{
"timestamp": 10,
"sensor_id": 1,
"temp": 14.5,
},
{
"timestamp": 20,
"sensor_id": 1,
"temp": 18,
},
{
"timestamp": 30,
"sensor_id": 1,
"temp": 25.5,
},
{
"timestamp": 5,
"sensor_id": 2,
"temp": 24.5,
},
{
"timestamp": 20,
"sensor_id": 2,
"temp": 29.5,
}
]
};
And how I want the arrays to turn out:
Timestamp: [5,10,20,30]
Sensor 1: [,14.5,18,25.5]
Sensor 2: [24.5,,29.5,]
I also need the number of sensors to change dynamically, so, for example, the data could come in with 3 sensor readings and I would need an extra array to be generated.
So far I attempted the following, however, the 'result.temp' operation is returning an undefined value so the code does works.
var unique_timestamps = [...new Set(zdata.data.map(item => item.timestamp))];
console.log(unique_timestamps);
var sensors = [...new Set(zdata.data.map(item => item.sensor_id))];
console.log(sensors);
// Will hold final arrays of sensor readings
var temperature_datasets = [];
for (i = 0; i < sensors.length; i++) {
// Holds the array of temperatures for one sensor
readings_arr =[];
for (j = 0; j < unique_timestamps.length; j++) {
var result = zdata.data.filter(obj => {
return (obj.timestamp === unique_timestamps[j] && obj.sensor_id === sensors[i])
})
readings_arr[j]=result.temp;
}
temperature_datasets[i] = readings_arr
}
I have two questions:
Is there a more efficient way of doing this that will take fewer operations?
If not, why am I getting an undefined result for 'result.temp' and how can I get the temperature value.
Here is a modern ES6 version that gets you the same result and is hopefully more readable and is more performant.
Code is explained as comments.
Initially, we pivot an object with time stamp as key and sensor as values.
We then use this to get the timestamp array and an array of sensorValues as shown.
zdata =
{ "data": [
{
"timestamp": 10,
"sensor_id": 1,
"temp": 14.5,
},
{
"timestamp": 20,
"sensor_id": 1,
"temp": 18,
},
{
"timestamp": 30,
"sensor_id": 1,
"temp": 25.5,
},
{
"timestamp": 5,
"sensor_id": 2,
"temp": 24.5,
},
{
"timestamp": 20,
"sensor_id": 2,
"temp": 29.5,
}
]
};
let {sensors ,...sensorTimeMap} = zdata.data.reduce((acc,val) => {
if(!acc[val.timestamp])
acc[val.timestamp] = {};
acc[val.timestamp][val.sensor_id] = val.temp; // pivots the data against timestamp so as to give us more performant lookups later
if(!acc.sensors.includes(val.sensor_id))
acc.sensors.push(val.sensor_id)
return acc;
},{ sensors:[] }); // Since the number of sensors is dynamic and should be extracted from the data object, we collect available sensors in an array and also create a timeStampMap for future reference
let timestamp = Object.keys(sensorTimeMap); // All timestamps of the pivot generated
let sensorValArray = sensors.map(sensor => (Object.values(sensorTimeMap).map(obj => obj[sensor] || ''))); // gives us an array of arrays since we cannot know for sure how many sensors are there beforehand and cannot store in different variables. But this is the only way to handle dynamic length since we cannot know how many variables to declare beforehand!
console.log(timestamp,sensorValArray);

Categories