Javascript Map objects

Are there any examples of creating a visualization using javascript’s Map objects?

I like some of Mike’s data analysis notebooks which use them, but I’m not sure how or if I can input Map objects into a viz. Do I need to convert back to a traditional array of objects first? I’m working with nested objects. Any pointers or examples would be appreciated! Cheers

If you’re talking about making visualizations in d3, note that you can’t directly bind data to a Map object with .data() since that function really expects an Array object there. You can convert a Map m to an array using Array.from(m) or by doing something like this:

const a = [];
m.forEach([val, key] => {
  a.push(val);
});

If you’re making graphics “by hand” (e.g. by drawing in a canvas with context2d), then you can iterate over the key-value pairs in a Map using for ... of.

Maps are basically just “key-value stores” and they’re primarily used when there’s a need to associate data or functions to specific keys for later retrieval. For instance, they’re frequently used to cache results of a computation / fetch operation for later use.

Could you say a bit more about what kinds of examples you’re looking for? Most of the time that I’ve used Map objects it’s for general programming needs (e.g. a the data generation or preprocessing step), rather than something I turn to when it comes time to make visualizations.

I see. Well I was going through some of Mike’s data analysis examples, particularly the ‘group’ examples which involve using map objects. For what I need I can use d3.nest. I was just curious why for these grouping examples seemed to be promoting map objects. I’ve moved over from R recently and would like to do all my wrangling and analysis in javascript :). thanks for your response!

I’ll add support for iterables (including Map) in the next major version of d3-selection. Until then I recommend Array.from.

4 Likes

Cool! I hadn’t seen these notebooks before. Map objects are well-suited for use there because the “grouping” operation is exactly the operation of associating data to keys; the MDN page you linked explains some of the advantages of using them over ordinary JS objects.

Browsing a little bit, it seems like those notebooks were in fact written to explore a possible future addition to d3 that uses the Map objects, see this tweet which links an earlier version. That’s probably the most direct answer to the question of why the notebooks are “promoting map objects”.

1 Like

Here are some more examples of the new d3-array (which will be part of d3 v6):

2 Likes

I’m trying using the array.from method selected, basically iterating through the levels of the nested map, after using d3.rollup, but that seems tedious (requiring several levels of iteration) compared to using d3.nest… so I don’t see why this is the route I should go :slight_smile:.

Here’s a notebook where I’m experimenting with these techniques.

Surely there must be a better way to convert the nested map object. Any pointers would be greatly appreciated.

It’s possible to convert the nested map to a nested plain object with a recursive function (code at the end of this post), but as you say, there’s no point in doing this if you just want the output of d3.nest.

Here’s a question: is it possible that the code that requires a nested object would somehow benefit from using nested maps instead?

function mapToObj(map) {
  if (map instanceof Map) {
    return Array.from(map).map(([key, val]) => ({name:key, children:mapToObj(val)}));
  }
  return map;
}

Edit: that doesn’t wrap the outermost layer, so use it like this, I guess:

dat = ({name:"moluscs", children:mapToObj(mapped)})
2 Likes

Here are some ways to make your code less verbose and possibly allow you to spot/extract reusable parts (edit: removed some leftovers in longData, be sure to reload):

3 Likes

Oops, had “class” twice in hierarchy - fixed. Let me know if you have any questions!

Wow, that’s very helpful! There’s obviously a few things I need to learn here. I’ll comb through this carefully. thanks!

Thanks so much for this! Likewise, I need to go through this carefully and learn from it. I will let you know if I have any questions :wink: Thanks again.

Hey Mootar, bgchen, Mike and others! A few weeks ago you helped me streamline some of my code (thanks again) and I have a similar but different bit of code that I’m trying to slim down… I’ve fiddled with some of the techniques you used but to no avail so I thought I would post it here in case you or someone else would be generous enough to take a look. Here’s the notebook with example code.

P.S. I was debating to post non-observable specific questions here. Let me know if it’s not appropriate and I can post on stackoverflow. Thanks.

What you coded seems mostly reasonable to me (though I’m by no means a JS efficiency expert!). Your code does have one probably undesired side effect though: in the version you shared, note these lines:

      // these aren't zooplanton so return as they are
      d.zoo_type = "";
      incZoo.push(d);

d points to an element of testdata, so in the line d.zoo_type = ""; you’re also changing the entries of the input object testdata as well (check the testdata object in your notebook and you’ll see that some of the entries have a zoo_type field!). By itself, this may not be a big deal, but the next line is more problematic. When you push d to incZoo (and hence flatdata), this creates a dependence between flatdata and testdata since some of the entries of flatdata are references to elements of testdata. This sort of thing can end up infuriatingly hard to debug later if you’re not careful, since you might make changes to one array and find that the other one has mysteriously changed as well!

In the following code, I use some destructuring assignment to work with the fields of d rather than d itself:

// this does the job but I'm thinking there's a better wat
flatdata = {
  let incZoo = [];
  testdata.forEach(({data_id, date, site_id, survey, depth, variable}) => {
    if (variable === "Zooplankton") {
      return Object.keys(data_id).forEach(z => {
        // if the key is zooTypes, un-nest it
        if (zooTypes.includes(z)) {
          data_id[z].forEach(({type, data_id}) => {
            incZoo.push({
              date,
              site_id,
              survey,
              depth,
              variable,
              zoo_type:type,
              data_id
            });
          });
        }
      });
    } else {
      // these aren't zooplanton so return as they are
      incZoo.push({data_id, date, site_id, survey, depth, variable, zoo_type:""});
    }
  });
  return incZoo;
}

I feel like my explanation could be better. Let me know if there’s something confusing and I can try to expand…

1 Like

Ah yes, that has burned me a few times so I always end up doing a deep clone. This avoids all that. Thanks, I’ll use destructuring assignment more going forward.

Destructuring with array.flatMap is pretty great for this:

flatdata = testdata.flatMap(
  d => d.variable === "Zooplankton"
    ? zooTypes
        .filter(t => t in d.data_id)
        .flatMap(t => d.data_id[t])
        .map(t => ({...d, ...t}))
    : d
)
3 Likes

Ahh… yeah. That’s better. Cheers!