Related
i'm having a bit of a problem here, i have an array that looks like this :
const input = {
name: 'xy',
lastname: 'yx',
history: [
{ "value": 0.02, "date": "2022-08-02T23:03:22.895Z" },
{ "value": 0.04, "date": "2022-08-02T22:03:16.603Z" },
{ "value": 0.08, "date": "2022-08-02T21:03:20.378Z" },
{ "value": 0.02, "date": "2022-08-01T23:03:32.584Z" },
{ "value": 0.04, "date": "2022-08-01T22:03:30.311Z" }]
}
but it have more data and more dates than this exemple, i want to sort it into a new array and put only the newest value of each day
so it will look like this :
const input = {
name: 'xy',
lastname: 'yx',
history: [
{ "value": 0.02, "date": "2022-08-02T23:03:22.895Z" },
{ "value": 0.04, "date": "2022-08-02T22:03:16.603Z" },
{ "value": 0.08, "date": "2022-08-02T21:03:20.378Z" },
{ "value": 0.02, "date": "2022-08-01T23:03:32.584Z" },
{ "value": 0.04, "date": "2022-08-01T22:03:30.311Z" }],
newHistory:
{ "value": 0.02, "date": "2022-08-02T23:03:22.895Z" },
{ "value": 0.02, "date": "2022-08-01T23:03:32.584Z" }],
}
i currently did the sorting of all the dates but i'm having trouble in doing the rest.
Since you've already sorted your data you could use a single reduce to iterate over your array of sorted data while keeping track of a new output array.
For each data point, you compare its date by the last element of the output. If the year-month-day part is not the same, you add the data point to the output array.
Here are these steps written out in code:
const input = [
{ "value": 0.02, "date": "2022-08-02T23:03:22.895Z" },
{ "value": 0.04, "date": "2022-08-02T22:03:16.603Z" },
{ "value": 0.08, "date": "2022-08-02T21:03:20.378Z" },
{ "value": 0.02, "date": "2022-08-01T23:03:32.584Z" },
{ "value": 0.04, "date": "2022-08-01T22:03:30.311Z" } ];
const output = input.reduce(
(out, current) => {
const lastDate = out.at(-1)?.date.slice(0, 10);
const currentDate = current.date.slice(0, 10);
if (lastDate !== currentDate) {
out.push(current);
}
return out;
},
[]
);
console.log(output);
If you like one-liners, you could also write this as:
const input = [
{ "value": 0.02, "date": "2022-08-02T23:03:22.895Z" },
{ "value": 0.04, "date": "2022-08-02T22:03:16.603Z" },
{ "value": 0.08, "date": "2022-08-02T21:03:20.378Z" },
{ "value": 0.02, "date": "2022-08-01T23:03:32.584Z" },
{ "value": 0.04, "date": "2022-08-01T22:03:30.311Z" } ];
const output = input.reduce(
(out, current) => out.at(-1)?.date.slice(0, 10) === current.date.slice(0, 10) ? out : out.concat(current),
[]
);
console.log(output);
I think this is definitely a problem where splitting different tasks into functions helps make the code easier to understand.
You said in a comment that you wanted to group the calendar dates according to UTC time, but (if you change your mind) the code below will also allow you to optionally use the system time zone (as well as some other options).
I've included lots of comments to explain as you read, but feel free to ask for clarity in a comment if something still isn't clear.
'use strict';
/** #returns a stringified number with a minimum length */
function padN (n, maxLength = 2, fillString = '0') {
return String(n).padStart(maxLength, fillString);
}
/**
* #returns a default sorting algorithm function
* for an array of objects each having a specific key
*/
function createDefaultSortByObjKey (key, {reverse = false} = {}) {
return reverse
? (oA, oB) => oA[key] > oB[key] ? -1 : oA[key] < oB[key] ? 1 : 0
: (oA, oB) => oA[key] < oB[key] ? -1 : oA[key] > oB[key] ? 1 : 0;
}
/**
* Set the `utc` option to `true` to use UTC
* #returns a date string format like `"20220703"`
*/
function getSerializedYMD (date, {utc = false} = {}) {
const year = utc ? date.getUTCFullYear() : date.getFullYear();
const month = utc ? date.getUTCMonth() : date.getMonth();
const dayOfMonth = utc ? date.getUTCDate() : date.getDate();
return `${year}${padN(month)}${padN(dayOfMonth)}`;
}
/** #pure */
function transform (array, {
newestFirst = false,
parseDates = false,
utc = false,
} = {}) {
// Create a function for sorting dates, sorting by oldest/newest
const sortFn = createDefaultSortByObjKey('date', {reverse: newestFirst});
const sorted = array
// Create actual date objects for every date property value
.map(o => ({...o, date: new Date(o.date)}))
// Sort them
.sort(sortFn);
// This will be used to compare if the current object's date occurred
// on the same date as the previous
let lastKey = '';
// The objects will be stored in inner arrays, grouped by calendar dates
const grouped = [];
for (const obj of sorted) {
const key = getSerializedYMD(obj.date, {utc});
// Create a new inner array group if a new calendar date is encountered
if (key !== lastKey) grouped.push([]);
// Add the object to the current date group
grouped.at(-1).push(obj);
// Update the last key
lastKey = key;
}
// Now just pick one date from each group
return grouped.map(group => {
// Pick the oldest/newest object in the group
const obj = group.at(newestFirst ? 0 : -1);
return parseDates
// Return it directly with the date value as an actual date object
? obj
// Or convert the date back to an ISO string first
: {...obj, date: obj.date.toISOString()};
});
}
const input = {
name: 'xy',
lastname: 'yx',
history: [
{ value: 0.02, date: '2022-08-02T23:03:22.895Z' },
{ value: 0.04, date: '2022-08-02T22:03:16.603Z' },
{ value: 0.08, date: '2022-08-02T21:03:20.378Z' },
{ value: 0.02, date: '2022-08-01T23:03:32.584Z' },
{ value: 0.04, date: '2022-08-01T22:03:30.311Z' },
],
};
const result = {
// Spread in all of the other properties
...input,
// And add a new transformed array from the `history` property
newHistory: transform(input.history, {newestFirst: true, utc: true}),
// You can even use the sorting function to sort the original history if you want:
// history: [...input.history].sort(createDefaultSortByObjKey('date', {reverse: true})),
};
console.log(result);
// You could have also used the options to get a different result variation:
// transform(input.history, {
// newestFirst: false,
// parseDates: true,
// utc: false,
// });
I'm importing data from my crm Pipedrive into Google sheets using Google Apps Script. This is part of a larger process but I'm at an impasse with this section of the script. I need to return a value by matching two parts of one array to another array.
First I pull all deal fields, which returns custom field keys and their id/label pairs. Here's a simplified output example:
{
"success": true,
"data": [
{
"id": 12500,
"key": "c4ecbe01c34994ede3a50c0f8",
"name": "Lead Type",
"options": [
{
"label": "Expired",
"id": 28
},
{
"label": "Sale",
"id": 29
},
{
"label": "Rent",
"id": 30
},
{
"label": "Other",
"id": 31
}
],
"mandatory_flag": false
}
]
}
Then I have separate info from a specific deal that includes an id. I need to match the below id 28 to the above array and return the label Expired:
var leadType = dealresponse.data["c4ecbe01c34994ede3a50c0f8"];
which returns 28
I don't know what '28' means so that's why I need to match it to the label Expired.
The dealFields array is long, maybe 50 or 100 of the above array objects. And there are around 10 custom deal field keys where I will have to return the label base on matching the key and id. I think I have to loop each key and id to return the label. But not sure of the optimum way to do this and save on processing power.
I tried:
for (var i in dealFieldsresponse) {
if (dealFieldsresponse[i].data.key == "c4ecbe01c34994ede3a50c0f8") {
for (var j in dealFieldsresponse[j]) {
if (dealFieldsresponse[j].id == "28") {
Logger.log(dealFieldsresponse[j].label);
}
}
}
}
It's not working. I'm new at javascript and programming in general so this is my best guess and I appreciate any insights.
Edit: here's a bigger chunk of code that I have to work with:
// Get deal fields data
var dealFieldsurl = URL +'/v1/dealFields?api_token='+ API_TOKEN;
var options = {
"method": "get",
"contentType": "application/json",
};
var dealFieldsresponse = UrlFetchApp.fetch(dealFieldsurl, options);
dealFieldsresponse = JSON.parse(dealFieldsresponse.getContentText());
// Get deal data
var dealurl = URL +'/v1/deals/' + dealId + '?api_token='+ API_TOKEN;
var options = {
"method": "get",
"contentType": "application/json",
};
var dealresponse = UrlFetchApp.fetch(dealurl, options);
dealresponse = JSON.parse(dealresponse.getContentText());
var propertyAddress = dealresponse.data["9bd1d8c4f07f5795fd8bffb16f3b63c6547d7d3a"];
var leadType = dealresponse.data["c4ecbe01c3494d1be52432f4a3194ede3a50c0f8"];
var dealType = dealresponse.data["a4269fb4730cf7fd1787752be94eacbc4b0de24e"];
var dealSource = dealresponse.data["d76fa2d6f8454a51f7d64d981cd9320877bc2ea0"];
var marketingFor = dealresponse.data["58cb55090b55652b7f89a8b44074682d874c548a"];
var dateListedOnMarket = dealresponse.data["aa49c7b95a7d151bec4c2d936f6ab40d0caea43c"];
var dateTakenOffMarket = dealresponse.data["660c1250b0a641a10ff9121c2df124ff89c13052"];
var askingPrice = dealresponse.data["1de94dbf589fda7a3a3248662cd24f03d512a961"];
And the dealFieldsresponse variable stores an array with many objects containing arrays. Here are two primary objects, as you can see each has a key and then options. I need to match the key and then find the id within options for each key
{
"id": 12500,
"key": "c4ecbe01c3494d1be52432f4a3194ede3a50c0f8",
"name": "Lead Type",
"order_nr": 64,
"field_type": "set",
"add_time": "2020-08-20 19:33:22",
"update_time": "2020-08-20 19:33:22",
"last_updated_by_user_id": 11678191,
"active_flag": true,
"edit_flag": true,
"index_visible_flag": true,
"details_visible_flag": true,
"add_visible_flag": true,
"important_flag": true,
"bulk_edit_allowed": true,
"searchable_flag": false,
"filtering_allowed": true,
"sortable_flag": true,
"options": [
{
"label": "Expired",
"id": 28
},
{
"label": "Sale",
"id": 29
},
{
"label": "Rent",
"id": 30
},
{
"label": "Other",
"id": 31
}
],
"mandatory_flag": false
},
{
"id": 12502,
"key": "a4269fb4730cf7fd1787752be94eacbc4b0de24e",
"name": "Deal Type",
"order_nr": 65,
"field_type": "set",
"add_time": "2020-08-20 19:57:12",
"update_time": "2020-08-20 19:57:12",
"last_updated_by_user_id": 11678191,
"active_flag": true,
"edit_flag": true,
"index_visible_flag": true,
"details_visible_flag": true,
"add_visible_flag": true,
"important_flag": true,
"bulk_edit_allowed": true,
"searchable_flag": false,
"filtering_allowed": true,
"sortable_flag": true,
"options": [
{
"label": "Lease",
"id": 37
},
{
"label": "Financing",
"id": 38
},
{
"label": "Assign",
"id": 39
},
{
"label": "ST",
"id": 40
},
{
"label": "Other (see notes)",
"id": 41
}
],
"mandatory_flag": false
},
Edit 2: how do I return the labels for multiple ids?
const obj = {
"a4269fb4730cf7fd1787752be94eacbc4b0de24e": {id: 37,38}, "58cb55090b55652b7f89a8b44074682d874c548a": {id: 44,45},
"2ec54cce0d091b69b1fd1a245c7aad02b57cadb8": {id: 126},
"fab84c732295022ecd7bdf58892a62cb4d8ecf24": {id: 50,52,54},
};
For example, I'd want the first to return red, blue as a string, and the second to return green, orange as a string. Assuming the labels that match the ids are colors. The third one only has one id, but the fourth one has three. How do I account for this? And I'd like my output to be some kind of array where I can then say search key a4269fb4730cf7fd1787752be94eacbc4b0de24e and return value red, blue as a string
I believe your goal as follows.
You want to retrieve the value of label using key and id from the JSON object in your question using Google Apps Script.
As a sample situation, you want to retrieve the value of "label": "Expired" using "key": "c4ecbe01c34994ede3a50c0f8" and "id": 28.
The JSON object has the arrays of data and options. Both arrays have the several elements.
Modification points:
If dealFieldsresponse is the JSON object in your question, dealFieldsresponse.data and dealFieldsresponse.data[].options are 1 dimensional array. When you want to retrieve the value of key and id, it is required to loop those arrays.
When above points are reflected to your script, it becomes as follows.
Modified script:
const searchKey = "c4ecbe01c34994ede3a50c0f8"; // Please set the value of key.
const searchId = 28; // Please set the value of id.
const dealFieldsresponse = {
"success": true,
"data": [
{
"id": 12500,
"key": "c4ecbe01c34994ede3a50c0f8",
"name": "Lead Type",
"options": [
{
"label": "Expired",
"id": 28
},
{
"label": "FSBO",
"id": 29
},
{
"label": "FRBO",
"id": 30
},
{
"label": "Other",
"id": 31
}
],
"mandatory_flag": false
}
]
};
const data = dealFieldsresponse.data;
for (let i = 0; i < data.length; i++) {
if (data[i].key == searchKey) {
const options = data[i].options;
for (let j = 0; j < options.length; j++) {
if (options[j].id.toString() == searchId.toString()) {
// Logger.log(options[j].label);
console.log(options[j].label);
}
}
}
}
Other sample:
As other sample script, how about the following script? In this sample, the result values are put in an array.
const searchKey = "c4ecbe01c34994ede3a50c0f8"; // Please set the value of key.
const searchId = 28; // Please set the value of id.
const dealFieldsresponse = {
"success": true,
"data": [
{
"id": 12500,
"key": "c4ecbe01c34994ede3a50c0f8",
"name": "Lead Type",
"options": [
{
"label": "Expired",
"id": 28
},
{
"label": "FSBO",
"id": 29
},
{
"label": "FRBO",
"id": 30
},
{
"label": "Other",
"id": 31
}
],
"mandatory_flag": false
}
]
};
const res = dealFieldsresponse.data.reduce((ar, {key, options}) => {
if (key == searchKey) {
options.forEach(({id, label}) => {
if (id == searchId) ar.push(label);
});
}
return ar;
}, []);
console.log(res)
Added:
When you want to retrieve the multiple values using the multiple key and id, how about the following sample script? In this sample script, the key c4ecbe01c34994ede3a50c0f8 and id 28 and the key a4269fb4730cf7fd1787752be94eacbc4b0de24e and id 37 are searched and the values of label are retrieved.
const obj = {
"c4ecbe01c34994ede3a50c0f8": {id: 28},
"a4269fb4730cf7fd1787752be94eacbc4b0de24e": {id: 37}
}; // Please set the key and id you want to search.
const dealFieldsresponse = {
"success": true,
"data": [
{
"id": 12500,
"key": "c4ecbe01c34994ede3a50c0f8",
"name": "Lead Type",
"options": [
{
"label": "Expired",
"id": 28
},
{
"label": "FSBO",
"id": 29
},
{
"label": "FRBO",
"id": 30
},
{
"label": "Other",
"id": 31
}
],
"mandatory_flag": false
}
]
};
dealFieldsresponse.data.forEach(({key, options}) => {
if (obj[key]) {
options.forEach(({id, label}) => {
if (id == obj[key].id) obj[key].label = label;
});
}
});
console.log(obj)
Result:
When above script is run, the following result is obtained.
{
"c4ecbe01c34994ede3a50c0f8":{"id":28,"label":"Expired"},
"a4269fb4730cf7fd1787752be94eacbc4b0de24e":{"id":37}
}
At above sample JSON object, the label of the key c4ecbe01c34994ede3a50c0f8 and id 28 is retrieved.
Here i am having a Object i.e ApiData1 . Where it has color key value pair inside properties . I am changing the color values according to ApiData2 value numberOfProjects, and having a range where numberOfProjects values lies between a set of range i am updating the color value. It is working fine.
in some scenarios ApiData2 value will come as null. In this case it has to return the default value that is already ,the value present in ApiData1. But it removes the value , according to my code. I dont know how to fix this. Pls help me with this. Here i am sharing the working demo link JS_FIDDLE
let ApiData1 = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 1,
"id": 10,
"stateId": 10,
"name": "Tamil Nadu",
"code": "TN"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 1,
"id": 11,
"stateId": 11,
"name": "Karnataka",
"code": "KA"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 1,
"id": 12,
"stateId": 12,
"name": "Pondicherry",
"code": "PY"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 6,
"id": 13,
"stateId": 13,
"name": "Maharashtra",
"code": "TT"
}
},
]
}
let ApiData2 = [
{
id: 10,
name: "Tamil Nadu",
code: "TN",
latitude: 29.9964948,
longitude: 81.6855882,
latestMetric: {
stateId: 10,
year: 0,
numberOfProjects: 1433,
}
},
{
id: 11,
name: "Karnataka",
code: "KA",
latitude: 21.9964948,
longitude: 82.6855882,
latestMetric: {
stateId: 11,
year: 0,
numberOfProjects: 3500,
}
},
{
id: 12,
name: "Pondicherry",
code: "PY",
latitude: 22.9964948,
longitude: 87.6855882,
latestMetric: {
stateId: 12,
year: 0,
numberOfProjects: 5500,
}
},
{
id: 13,
name: "Maharashtra",
code: "PY",
latitude: 22.9964948,
longitude: 87.6855882,
latestMetric: null
}
];
function updateColor() {
function updateProperties(colorJsonObject, colorValue) {
let updatedProperties = {
...colorJsonObject.properties,
color: colorValue
};
/* console.log(updatedProperties) */
return updatedProperties;
}
let range = [
{
"Minimum": 1,
"Maximum": 2000,
"color": 1
},
{
"Minimum": 2000,
"Maximum": 4000,
"color": 2
},
{
"Minimum": 4000,
"Maximum": 6000,
"color": 3
}
]
let newData = {
...ApiData1,
features: ApiData1.features.map(colorObject => {
const apiData = ApiData2.find(apiData => {
if (
colorObject.properties.stateId === apiData.latestMetric.stateId
) {
return true;
}
return false;
});
console.log(apiData)
let newValue;
range.forEach(i => {
if (
apiData.latestMetric.numberOfProjects >= i.Minimum &&
apiData.latestMetric.numberOfProjects <= i.Maximum
) {
let value = updateProperties(colorObject, i.color)
newValue = {...colorObject,properties:value}
}
});
return newValue;
})
}
return newData;
}
let colorValue = updateColor();
console.log(colorValue)
Your help or suggestion is much appreciated.
Thanks in advance.
Result:
In ApiData1 the color value in Maharashtra is 4 . In ApiData2 the latestMetric of Maharashtra is null. If i remove this Maharashtra value in both ApiData1 and ApiData2 . the code works fine and updating the color values. But if i run the code with this scenario , it breaks the code.
Here what i am trying to do is, if ApiData2 value has returned null, i just need to pass the default value that is already present in ApiData1 without updating it. The output has to be like this
Expected Output:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 1,
"id": 10,
"stateId": 10,
"name": "Tamil Nadu",
"code": "TN"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 2,
"id": 11,
"stateId": 11,
"name": "Karnataka",
"code": "KA"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 3,
"id": 12,
"stateId": 12,
"name": "Pondicherry",
"code": "PY"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 6,
"id": 13,
"stateId": 13,
"name": "Maharashtra",
"code": "TT"
}
},
]
}
There's a lot to fix up in this code, so I start with a rough pass -
// pass1.js
function updateColor()
{ let range = [
{
"Minimum": 1,
"Maximum": 2000,
"color": 1
},
{
"Minimum": 2000,
"Maximum": 4000,
"color": 2
},
{
"Minimum": 4000,
"Maximum": 6000,
"color": 3
}
]
function updateProperties(feature, colorValue)
{ return {
...feature.properties,
color: colorValue
};
}
let newData = {
...ApiData1,
features: ApiData1.features.map(feature => {
const data2 = ApiData2.find(x =>
feature.properties.stateId === x.latestMetric.stateId
)
let newValue
range.forEach(i => {
let value
if (
data2.latestMetric.numberOfProjects >= i.Minimum &&
data2.latestMetric.numberOfProjects <= i.Maximum
) {
value = updateProperties(feature, i.color)
newValue = {...feature,properties:value}
}
})
return newValue
})
}
return newData
}
Bug 1: unsafe deep property access -
function updateColor()
{ let range = // ...
function updateProperties // ...
let newData = {
...ApiData1,
features: ApiData1.features.map(feature => {
const data2 = ApiData2.find(x =>
//
// Bug!
// TypeError: Cannot read property 'stateId' of null
// ApiData2 = [ ...
// {
// id: 13,
// name: "Maharashtra",
// code: "PY",
// latitude: 22.9964948,
// longitude: 87.6855882,
// latestMetric: null
// }
//
feature.properties.stateId === x.latestMetric.stateId
)
// ...
})
}
return newData
}
You have to null-check before you attempt deep property access -
const data2 = ApiData2.find(x =>
feature.properties && x.latestMetric && // <--
feature.properties.stateId === x.latestMetric.stateId
)
Bug 2, find sometimes returns undefined -
function updateColor()
{ let range = // ...
function updateProperties // ...
let newData = {
...ApiData1,
features: ApiData1.features.map(feature => {
// ...
range.forEach(i => {
let value
if (
//
// Bug!
// TypeError: Cannot read property 'latestMetric' of undefined
// data2 is the result of `ApiData2.find(...)`
// if an element is not found, `.find` returns undefined
//
data2.latestMetric.numberOfProjects >= i.Minimum &&
data2.latestMetric.numberOfProjects <= i.Maximum
) // ...
})
// ...
})
}
return newData
}
Sometimes you have to null-check before you attempt (any) property access!
if (
data2 && data2.latestMetric && // <--
data2.latestMetric.numberOfProjects >= i.Minimum &&
data2.latestMetric.numberOfProjects <= i.Maximum
)
too much pain
Did this whole updateColor function feel tedious and painful to write? Let's see if we can't make this whole process a little nicer. We have two core issues -
safely accessing deeply nested state
safely (and immutably) update deeply nested state
1. a better null
// Util.js
import { Just, Nothing, fromNullable } from "data.maybe"
const safeProp = (o = {}, p = "") =>
o == null
? Nothing()
: fromNullable(o[p])
const safeProps = (o = {}, props = []) =>
props.reduce
( (mr, p) => mr.chain(r => safeProp(r, p))
, fromNullable(o)
)
export { safeProp, safeProps }
safeProp(ApiData1, "type")
// Just {value: "FeatureCollection"}
safeProp(ApiData1, "zzz")
// Nothing {}
safeProps(ApiData, ["features", 0, "type"])
// Just {value: "Feature"}
safeProps(ApiData1, ["features", 0, "properties", "color"])
// Just {value: 1}
safeProps(ApiData1, ["features", 999, "properties", "color"])
// Nothing {}
Maybe allows us to work with nullable values in a safer way -
safeProp(ApiData1, "type")
// Just {value: "FeatureCollection"}
safeProp(ApiData1, "type").getOrElse("not found!")
// FeatureCollection
safeProp(ApiData1, "zzz")
// Nothing {}
safeProp(ApiData1, "zzz").getOrElse("not found!")
// not found!
Things are starting to take shape. Let's make a safer find now -
// Util.js
import // ...
const identity = x => x
const safeFind = (a = [], f = identity) =>
fromNullable(a.find(f))
const safeProp // ...
const safeProps // ...
export { safeFind, //... }
// Main.js
import { safeFind, safeProp, safeProps } from "./Util"
const Api2FindByStateId = (q = null) =>
safeFind
( ApiData2
, x =>
safeProps(x, ["latestMetric", "stateId"])
.map(stateId => stateId === q)
.getOrElse(false)
)
function updateColor ()
{
// ...
return {
...ApiData1,
features: ApiData1.features.map(feature => {
const data2 =
safeProps(feature, ["properties", "stateId"])
.chain(Api2FindByStateId)
// range.forEach()
})
}
}
We don't want to use safeProp every time we use objects. It's only intended for use on objects that have uncertain shape. We want to write concrete objects we can rely upon, when possible. This is the perfect time to define a Range module -
// Range.js
const range = (min = 0, max = 0, data = null) =>
({ min: parse(min), max: parse(max), data })
const inRange = ({ min, max } = range(), x = 0) =>
x >= min && x < max
const parse = (n) =>
Number.isInteger(n) ? n : 0
export { range, inRange } // <-- export only what you plan to use
Now with a well-defined Range module, we can finish our program -
import { safeProp, safeProps } from './Util'
import { range, inRange } from './Range'
const ranges =
[ range(1, 2000, { color: 1 })
, range(2000, 4000, { color: 2 })
, range(4000, 6000, { color: 3 })
]
const findRange = (q = 0) =>
safeFind(ranges, r => inRange(r, q))
function updateColor ()
{ return {
...ApiData1,
features: ApiData1.features.map(feature => {
const defaultColor =
safeProps(feature, ["properties", "color"])
.getOrElse(0)
const newColor =
safeProps(feature, ["properties", "stateId"])
.chain(Api2FindByStateId)
.chain(data2 => safeProps(data2, ["latestMetric", "numberOfProjects"]))
.chain(findRange)
.chain(range => safeProp(range.data, "color"))
.getOrElse(defaultColor)
return { ...feature, properties: { ...feature.properties, color: newColor } }
})
}
}
console.log(JSON.stringify(updateColor(), null, 2)) // <-- run it
Here's the output -
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 1, // <--
"id": 10,
"stateId": 10,
"name": "Tamil Nadu",
"code": "TN"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 2, // <--
"id": 11,
"stateId": 11,
"name": "Karnataka",
"code": "KA"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 3, // <--
"id": 12,
"stateId": 12,
"name": "Pondicherry",
"code": "PY"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon"
},
"properties": {
"color": 6, // <--
"id": 13,
"stateId": 13,
"name": "Maharashtra",
"code": "TT"
}
}
]
}
If you are having trouble visualising how the sequence of chain operations is run, we can add a log utility that shows the execution of our program line-by-line -
const log = (label = "") =>
x => (console.log(label, `->`, JSON.stringify(x)), Just(x))
function updateColor ()
{ return {
...ApiData1,
features: ApiData1.features.map(feature => {
const defaultColor =
safeProps(feature, ["properties", "color"])
.chain(log("feature.properties.color")) // <-- log
.getOrElse(0)
const newColor =
safeProps(feature, ["properties", "stateId"])
.chain(log("feature.properties.stateId")) // <-- log
.chain(Api2FindByStateId)
.chain(log("Api2FindByStateId")) // <-- log
.chain(data2 => safeProps(data2, ["latestMetric", "numberOfProjects"]))
.chain(log("data2.lastMetric.numberOfProjects")) // <-- log
.chain(findRange)
.chain(log("findRange")) // <-- log
.chain(range => safeProp(range.data, "color"))
.chain(log("range.data.color")) // <-- log
.getOrElse(defaultColor)
console.log(`newColor -> ${newColor}\n---`) // <-- log
return { ...feature, properties: { ...feature.properties, color: newColor } }
})
}
}
console.log(JSON.stringify(updateColor(), null, 2)) // <-- run it
feature.properties.color -> 1
feature.properties.stateId -> 10
Api2FindByStateId -> {"id":10,"name":"Tamil Nadu","code":"TN","latitude":29.9964948,"longitude":81.6855882,"latestMetric":{"stateId":10,"year":0,"numberOfProjects":1433}}
data2.lastMetric.numberOfProjects -> 1433
findRange -> {"min":1,"max":2000,"data":{"color":1}}
range.data.color -> 1
newColor -> 1
---
feature.properties.color -> 1
feature.properties.stateId -> 11
Api2FindByStateId -> {"id":11,"name":"Karnataka","code":"KA","latitude":21.9964948,"longitude":82.6855882,"latestMetric":{"stateId":11,"year":0,"numberOfProjects":3500}}
data2.lastMetric.numberOfProjects -> 3500
findRange -> {"min":2000,"max":4000,"data":{"color":2}}
range.data.color -> 2
newColor -> 2
---
feature.properties.color -> 1
feature.properties.stateId -> 12
Api2FindByStateId -> {"id":12,"name":"Pondicherry","code":"PY","latitude":22.9964948,"longitude":87.6855882,"latestMetric":{"stateId":12,"year":0,"numberOfProjects":5500}}
data2.lastMetric.numberOfProjects -> 5500
findRange -> {"min":4000,"max":6000,"data":{"color":3}}
range.data.color -> 3
newColor -> 3
---
feature.properties.color -> 6
feature.properties.stateId -> 13
newColor -> 6
---
{ "type": "FeatureCollection", "features": [ ... ] }
Pay particular attention to the log outputs for the last feature, Maharashtra. There is no output for Api2FindByStateId because no match was found and Nothing was returned. We see newColor -> 6 because all intermediate chain computations are skipped as soon as a Nothing is encountered!
2. a better update
This is a nightmare we want to avoid -
return { ...feature, properties: { ...feature.properties, color: newColor } }
modules like Immutable can help with this immensely -
import { fromJS, setIn } from "immutable"
function updateColor (feature = {})
{ const defaultColor = //...
const newColor = // ...
// immutable update
return setIn(fromJS(feature), ["properties", "color"], newColor).toJS()
}
function updateColors()
{ return {
...ApiData1,
features: ApiData1.features.map(updateColor)
}
}
Even greater gains are achieved when all other data in your program is under the Immutable umbrella. The docs show many useful examples which should help you understand how to use the library effectively.
I have variable data having json data as below:
[
{
"BillingMonth":"11",
"BillingYear":"2016",
"Volume":"72",
"BillingMonthName":"November",
"BillingProduct":"Product1"
},
{
"BillingMonth":"11",
"BillingYear":"2016",
"Volume":"617",
"BillingMonthName":"November",
"BillingProduct":"Product2"
},
{
"BillingMonth":"12",
"BillingYear":"2016",
"Volume":"72",
"BillingMonthName":"December",
"BillingProduct":"Product1"
},
{
"BillingMonth":"12",
"BillingYear":"2016",
"Volume":"72",
"BillingMonthName":"December",
"BillingProduct":"Product2"
}
]
What I want to split above json data using javascript/jquery and get them stored in two variables data1, data2 having json data as below as result:
{
type: "stackedBar",
legendText: "Product1",
showInLegend: "true",
data1: [
{ x: November, y: 72 },
{ x: December, y: 72 },
]
}
and
{
type: "stackedBar",
legendText: "Product2",
showInLegend: "true",
data2: [
{ x: November, y: 617 },
{ x: December, y: 72 },
]
}
The above will bind in canvas js stackedbar chart.
Thanks!
Hey here's a solution I had a lot of fun working on I hope it works well for you. I wasn't sure if you would always have 2 products product1, product2 so I went with a more general approach for n amount of products. The result is in an array format, but you can use es6 destructuring to get the two variables data1 and data2 like I did below:
/*
* helper function to restructure json in the desired format
*/
function format(obj) {
var formatted = {
"type": "stackedBar",
"legendText": obj.BillingProduct,
"showInLegend": "true",
"data": [{
"x": obj.BillingMonthName,
"y": obj.Volume
}]
}
return formatted;
}
/*
* returns an array of unique products with corresponding BillingMonth/Volume data
*/
function getStackedBarData(data) {
// transform each obj in orignal array to desired structure
var formattedData = data.map(format);
// remove duplicate products and aggregate the data fields
var stackedBarData =
formattedData.reduce(function(acc, val){
var getProduct = acc.filter(function(item){
return item.legendText == val.legendText
});
if (getProduct.length != 0) {
getProduct[0].data.push(val.data[0]);
return acc;
}
acc.push(val);
return acc;
}, []);
return stackedBarData;
}
var data = [{
"BillingMonth": "11",
"BillingYear": "2016",
"Volume": "72",
"BillingMonthName": "November",
"BillingProduct": "Product1"
}, {
"BillingMonth": "11",
"BillingYear": "2016",
"Volume": "617",
"BillingMonthName": "November",
"BillingProduct": "Product2"
}, {
"BillingMonth": "12",
"BillingYear": "2016",
"Volume": "72",
"BillingMonthName": "December",
"BillingProduct": "Product1"
}, {
"BillingMonth": "12",
"BillingYear": "2016",
"Volume": "72",
"BillingMonthName": "December",
"BillingProduct": "Product2"
}]
var dataVars = getStackedBarData(data);
var data1 = dataVars[0];
var data2 = dataVars[1];
console.log(data1);
console.log(data2);
Hope this helps you!
I have a data coming from a VM that has several block devices. Every block device is represented with a a line charts that where created using c3.js that read Bytes_Read and Bytes_Written in the dataset and chart it in realtime. But I am struggling with the issue when there are new block devices introduced in dataset it does not create a new chart. What would be the best way to achieve this using JavaScript.
Sample of my dataset
{
"devices": [
{
"Name": "bdev0",
"output": {
"IO_Operations": 0,
"Bytes_Read": 0,
"Bytes_Written": 0
}
},
{
"Name": "bdev0",
"output": {
"IO_Operations": 1,
"Bytes_Read": 2,
"Bytes_Written": 3
}
},
{
"Name": "bdev0",
"output": {
"IO_Operations": 5,
"Bytes_Read": 7,
"Bytes_Written": 8
}
},
{
"Name": "bdev1",
"output": {
"IO_Operations": 10,
"Bytes_Read": 20,
"Bytes_Written": 30
}
}
]
}
Updated dataset with a new device
{
"devices": [
{
"Name": "bdev0",
"output": {
"IO_Operations": 0,
"Bytes_Read": 0,
"Bytes_Written": 0
}
},
{
"Name": "bdev0",
"output": {
"IO_Operations": 1,
"Bytes_Read": 2,
"Bytes_Written": 3
}
},
{
"Name": "bdev0",
"output": {
"IO_Operations": 5,
"Bytes_Read": 7,
"Bytes_Written": 8
}
},
{
"Name": "bdev1",
"output": {
"IO_Operations": 10,
"Bytes_Read": 20,
"Bytes_Written": 30
},
{
"Name": "bdev2",
"output": {
"IO_Operations": 40,
"Bytes_Read": 50,
"Bytes_Written": 90
}
}
]
}
chart code
eon.chart({
pubnub : pubnub,
history : false,
channel : 'orbit5_volume',
flow : true,
debug: true,
generate : {
bindto : '#chart',
size: {
height: 180,
width: 500
},
data : {
x : 'x',
labels : true
},
axis : {
x : {
type : 'timeseries',
tick : {
format : '%H:%M:%S'
},
zoom: {
enabled: true
}
}
}
},
transform : function(m) {
for (var i in m.devices){
return { columns : [
['x', new Date().getTime()],
['Bytes Written', m.devices[i].output.Bytes_Read],
['Bytes Read', m.devices[i].output.Bytes_Written]
]
}
}
}
});
You're chart code transform loop is overwriting every data's key which is why you're only getting a couple values. If you use the i variable to give each piece of data a new key, it'll show up on the chart.
Try this transform function:
eon.chart({
transform : function(m) {
var obj = {columns: [
['x', new Date().getTime()]
]};
for (var i in m.devices) {
obj.columns.push(['Bytes Read ' + i, m.devices[i].output.Bytes_Read]);
obj.columns.push(['Bytes Written ' + i, m.devices[i].output.Bytes_Written]]);
}
}
return obj;
}
});