0

I have some nested Json in a file called data.json. I am using fetch to read the file in and then would like to do some filtering based on if a user has a specific option selected in a dropdown on the website.

var jsonData = [{"type": "FeatureCollection",
       "features": [
          {"type": 'Feature', "properties": {"id": 1}, "geometry": {"type": "Point", "coordinates": [-80.71, 28.34]}},
          {"type": 'Feature', "properties": {"id": 2}, "geometry": {"type": "Point", "coordinates": [-79.89, 28.45]}},
          {"type": 'Feature', "properties": {"id": 2}, "geometry": {"type": "Point", "coordinates": [-60.79, 28.32]}}
       ]}
]

I would like to perform some filtering on this array of features based on the "properties": {"id": #} field. E.g. if a user selects a value that matches that id, keep that in my result display data, else remove it.

I am trying to do something via a Promise like below. I'm new to JavaScript but the usage of .filter in the below code is my attempt to get this to work.

Ideal solution I want to achieve:

I am displaying a map of data on the U.S. Map with points relative to some locations that belong to the id field I mentioned. I would like the user to be able to click one of the IDs via a drop-down facility in Javascript, then via their selection, filter the JSON data to only features that belong to that Id. E.g. a traditional filter you'd use on data.

function filterIds(data) {
    let idFilter= document.getElementById("id-filter");
    let selection = idFilter.addEventListener('change', function(event) {
        return this.value;
    });

    data.features.map((element) => {
        // spread out our array of data and filter on specific property (namely, the id key)
        return {...element, properties: element.filter((property) => property.id=== selection)};
    });

async function getData(url) {
    let response = await fetch(url);
    let data = await response.json();
    // console.log(data);
    return data;
};
getData("../data/processed/data.json") // fetch raw data
    .then(data => filterIds(data));
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Coldchain9
  • 1,373
  • 11
  • 31
  • Does this answer your question? [How to filter object array based on attributes?](https://stackoverflow.com/questions/2722159/how-to-filter-object-array-based-on-attributes) – Abraham Mar 07 '22 at 18:58
  • If you are calling filterIds more than once, please remove the assignment of the event listener, or you will get an extra event listener each time you call filterIds. – James Mar 07 '22 at 18:59
  • @James Ok. I suppose I have to understand where to put the event listener if I want the filter of my JSON to directly relate to what the user is selecting. I suppose a global event listener is more sensible? – Coldchain9 Mar 07 '22 at 19:00
  • 1/2 ... What the OP is doing right now ... after having fetched the data, an event handler gets subscribed to the `'change'` event of a queried dropdown. But the handler's implementation does not fit the OP's scenario. Actually the OP wants the event handler to take care of the `data.features` mapping, which right now has no effect, since it never gets executed at `'change'`, and the only time it is processed, the `property.id === selection` comparison runs against a `selection` value of `undefined`. – Peter Seliger Mar 07 '22 at 19:31
  • 2/2 ... What the OP might try to do instead ... implement the `data.features` mapping part into the `change` handler. But then again, the question remains ... _"What is going to happen with the result of the `map` task?"_ – Peter Seliger Mar 07 '22 at 19:31
  • @Coldchain9 ... please consider editing your questions. In addition to the already posted code, maybe precisely describe in words and by bullet-points what actually should happen right after the data got fetched and also what should be done with the mapped data after each `change` event. – Peter Seliger Mar 07 '22 at 19:38
  • @PeterSeliger added some text for clarity. – Coldchain9 Mar 07 '22 at 21:02
  • @Coldchain9 ... Another question which just popped up ... where does the dropdown initially get its own data from? The fetched data and the dropdown data somehow need to be related, don't they? Consider in addition to the javascript blob to also provide the relevant markup. – Peter Seliger Mar 07 '22 at 21:22
  • 1
    @PeterSeliger I have a function that populates the dropdown via getting distinct IDs from the raw json. its an intermediary step. 1. Load data, 2. populate dropdown 3. (work in progress) user selects id, and data is populated. I can add that function to the code above as well if that helps. – Coldchain9 Mar 07 '22 at 21:24

4 Answers4

1

Assuming that the OP initially needs to keep the fetched data in sync with the dropdown ... the dropdown-option's id-values after all have to reflect the feature-items of the fetched data's features array ... the proposed steps for a main initializing process are as follows ...

  1. Start with fetching the data.
  2. Create a map/index of id-specific feature-items from the resolved data's features array. Thus one later even avoids the filter task due to just looking up the id-specific feature-list based on the selected id.
  3. Render the dropdown-options based on a list of ids's which are the keys of the just created map.
  4. Implement the 'change' handling. (The following example code favors an explicit data-binding approach for the handler).

// ... mocked API call ...
async function fetchData() {
  return new Promise(resolve =>
    setTimeout(() => resolve({
      data: [{
        "type": "FeatureCollection",
        "features": [{
          "type": 'Feature',
          "properties": {
            "id": 1
          },
          "geometry": {
            "type": "Point",
            "coordinates": [-80.71, 28.34]
          }
        }, {
          "type": 'Feature',
          "properties": {
            "id": 2
          },
          "geometry": {
            "type": "Point",
            "coordinates": [-79.89, 28.45]
          }
        }, {
          "type": 'Feature',
          "properties": {
            "id": 2
          },
          "geometry": {
            "type": "Point",
            "coordinates": [-60.79, 28.32]
          }
        }]
      }]
    }), 2000)
  );
}

// one time data transformation in order to avoid filtering later.
function getFeaturesMapGroupedByPropertiesId(featureList) {
  return featureList.reduce((map, featureItem) => {

    const { properties: { id } } = featureItem;

    (map[id] ??= []).push(featureItem);

    return map;

  }, {});
}

function renderDropdownOptions(node, idList) {
  const { options } = node;

  // delete/reset the `options` collection.
  options.length = 0;
  // put/add initial default selected option item.
  options.add(new Option('select an id', '', true, true));

  idList.forEach(id =>
    options.add(new Option(`feature ${ id }`, id))
  );
}

function handleFeatureChangeWithBoundFeaturesMap({ currentTarget }) {
  const idBasedFeaturesMap = this;
  const featureId = currentTarget.value;

  console.log(
    `id specific feature list for id "${ featureId }" ...`,
    idBasedFeaturesMap[featureId],
  );
}

async function main() {
  console.log('... trigger fetching data ...');

  const { data } = await fetchData();
  console.log('... fetching data ... done ...', { data });

  const idBasedFeaturesMap =
    getFeaturesMapGroupedByPropertiesId(data[0].features);

  // console.log({ idBasedFeaturesMap });
  // //console.log(Object.keys(idBasedFeaturesMap));

  const dropdownNode = document.querySelector('select#feature');
  if (dropdownNode) {
  
    console.log('... synchronize dropdown data ...');

    renderDropdownOptions(
      dropdownNode,
      Object.keys(idBasedFeaturesMap),
    );
    dropdownNode
      .addEventListener(
        'change',
        handleFeatureChangeWithBoundFeaturesMap.bind(idBasedFeaturesMap)
      );  
    console.log('... synchronize dropdown data ... done!');
  }
}
main();
.as-console-wrapper {
  min-height: 100%;
  width: 80%;
  left: auto!important;
}
body { margin: 0; }
<select id="feature">
  <option value="">... initializing ...</option>
  <!--
  <option value="1">feature 1</option>
  <option value="2">feature 2</option>
  //-->
</select>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • Very well written. I've been using JS for about a week, could you explain your ```fetchData``` function if I am using it on a raw ```.json``` file that I am directing to via a path? Currently I'm getting undefined when using yours with a hard coded path in the ```data:``` section. Thanks. – Coldchain9 Mar 08 '22 at 13:28
  • 1
    It was written just in order to simulate an asynchronous API call. It creates and returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which after 2 seconds (`setTimeout` fakes the API call's delay) invokes the `resolve` callback of the promise. The latter switches the returned promise into the settled state of `status: 'fulfilled'` which leads to (a) further function invocation(s) down the promise chain where the `resolve`'s return value, here the very data, gets passed to as single payload parameter. – Peter Seliger Mar 08 '22 at 13:47
  • I am very close to adapting your logic to get a working solution in my project. I'll accept the answer as soon as I understand one thing. When I ```addEventListener``` to the ```dropdownNode``` and send in the ```idBasedFeaturesMap```, I want to extract the value from the event. However when using ```.value``` to extract that value from ```currentTarget``` I receive undefined. However, if I ```console.log(currentTarget)``` I do receive the event in my console. I'm just not sure how to extract the value. Thanks again. – Coldchain9 Mar 09 '22 at 19:14
  • Actually, disregard my above question. I have it working. Thanks a great deal for your help. – Coldchain9 Mar 09 '22 at 19:36
0

You need to make some changes in your filter function -

function filterIds(data) {
  let idFilter= document.getElementById("id-filter");
  let value = null;
  let selection = idFilter.addEventListener('change', function(event) {
    value = this.value;
});
data.features.map((element) => {
      const properties = element.filter((property) => 
      property.id=== value);
      return {...element, properties};
});
}

To fix Uncaught (in promise) TypeError: element.filter is not a function you need to make following changes to your getData function -

async function getData(url) {return await fetch(url).then((res) => res.json())};

Also, you must set event listener outside your filter function, at a global level.

Mir
  • 50
  • 5
  • Got it. I made the change to the eventListener. was my first time using one so pardon my ignorance. I still am getting an error with your map solution. ```Uncaught (in promise) TypeError: element.filter is not a function``` Does this have something to do with the fact that I am calling the data.features.map in a ```Promise``` way? – Coldchain9 Mar 07 '22 at 19:11
  • `async function getData(url) { return await fetch(url).then((res) => res.json()) };` – Mir Mar 07 '22 at 19:20
  • 1
    @Mir ... don't comment with code, change/edit your answer instead, which by now does not lead to any improvement. – Peter Seliger Mar 07 '22 at 19:35
  • @Mir ... Why does the edited code not reflect (especially the event subscription part) what was just suggested to the OP? – Peter Seliger Mar 07 '22 at 19:42
0

Here's a technique to try, it changes the flow slightly to:

  • user loads page
  • page assigns an event listener to the dropdown
  • page loads data.json into a global variable

Only when the user changes the dropdown does it check to make sure data.json got loaded and then perform the needed filtering.

// global var to store "data" from getData
let processedData;

// assign the listener once, on page load
document.getElementById("id-filter").addEventListener('change', function(event) {
  if (!processedData) return; // still no processedData available
  
  let selection = this.value;
  
  let newData = processedData.features.map((element) => {
    // spread out our array of data and filter on specific property (namely, the id key)
    return {...element, properties: element.filter((property) => property.id === selection)};
  });

  // do something with newData
});


const setData = (data) => {
  // this just stores data in the global processedData variable
  processedData = data;
};

async function getData(url) {
    let response = await fetch(url);
    let data = await response.json();
    return data;
};

// call getData once, on page load
getData("../data/processed/data.json") // fetch raw data
    .then(data => setData(data)); 
James
  • 20,957
  • 5
  • 26
  • 41
0

Maybe it's not necessary to filter, since you are indexing prior to it. Depending of what you are doing, it can be way faster.

Base example for a table, or a chart.

var jsonData = [{"type": "FeatureCollection",
       "features": [
          {"type": 'Feature', "properties": {"id": 1}, "geometry": {"type": "Point", "coordinates": [-80.71, 28.34]}},
          {"type": 'Feature', "properties": {"id": 2}, "geometry": {"type": "Point", "coordinates": [-79.89, 28.45]}},
          {"type": 'Feature', "properties": {"id": 2}, "geometry": {"type": "Point", "coordinates": [-60.79, 28.32]}}
       ]}
]

// Create a select element
document.body.append(Object.assign(document.createElement("select"), {id: "select" }))

// Count features and create options
for (let i = 0; i < jsonData[0].features.length; i++){
  document.getElementById("select").append(
    Object.assign(document.createElement("option"), {textContent: i })
  )
}

// Read by index at selection change
select.addEventListener("change", (e) => {
  let selected = +e.target.selectedOptions[0].textContent // Cast to integer
  console.log(jsonData[0].features[selected])
})
NVRM
  • 11,480
  • 1
  • 88
  • 87