0

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"
            }
        },

    ]
}
4
  • 1
    I am working on something that is similar to this. I will try to write an answer for you soon. Can you please update the question to give the exact output you are looking for? Commented May 22, 2020 at 15:09
  • i have edited my question. pls take a look at it Commented May 22, 2020 at 15:21
  • By ApiData2 value null u meant entire ApiData2 array will be null ? Commented May 22, 2020 at 15:37
  • No. If u see the fourth object in ApiData2 , the latestMetric is null. In this case the ApiData1 value i.e fourth object will remain same as it is. Commented May 22, 2020 at 15:42

1 Answer 1

1

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 -

  1. safely accessing deeply nested state
  2. 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.

Sign up to request clarification or add additional context in comments.

3 Comments

There's a surprising amount of technical complexity in your program. My updated answer attempts to distill it, but there's still some areas of improvement. If you have specific questions, lmk and I'll try to answer them when I make time for another update :D
Hi ,thank u very much for ur time and effort that u put on. could u please share with me which solution i should go for. Here u have imported range ,utils from somewhere, that makes me hard to get it :(
range and utils are defined in the answer. it just makes sense to show you that these parts are separate and shouldn't go in the same file with your component.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.