🏠 back to Observable

Forcing re-evaluation of downstream cells


#1

I’m working on this notebook which is just a widget to let folks I work with make simple maps with certain counties highlighted.


I want to let them download the maps when they are done (using https://beta.observablehq.com/@mbostock/saving-svg). The thing that I can’t quite wrap my head around is how to force the download cells to re-evaluate in response to a click on the map (when the user clicks on a county to highlight). The click doesn’t cause the cell to reevaluate, so it doesn’t notify any downstream cells (like the download cell) of the change.

Any ideas? Maybe I’m totally off-base with my approach here?

Thanks,
Evan


#2

Hi Evan,

There are (of course), a few different ways you could go about doing this. But assuming that you don’t want to change the value of the map cell … I’d suggest adding a new mutable counties cell, whose contents are the array of all of the currently highlighted county names.

When the folks you work with click a county, add or remove it from the array, assigning mutable counties = ... again. Then, you can have your download cell depend on the value of counties, and it will re-evaluate every time the county selection changes.

I hope that’s clear enough. Let me know if it isn’t…


#3

In addition to mutable, there’s two other strategies you could apply here.

One is to change how the download buttons are implemented. Rather than greedily capturing the state of the map when generating the buttons, defer it: lazily capture the state of the map when the user clicks the button. This requires two clicks because browsers don’t let you trigger a download asynchronously, but that’s hopefully acceptable.

{
  const a = document.createElement("a");
  const b = a.appendChild(document.createElement("button"));
  b.textContent = "Generate PNG";
  a.onclick = async () => {
    b.disabled = true;
    b.textContent = "Generating PNG…";
    const url = a.href = URL.createObjectURL(await rasterize(map));
    b.textContent = "Download PNG";
    b.disabled = false;
    a.download = "nc_map.png";
    a.onclick = () => setTimeout(() => URL.revokeObjectURL(url), 50);
  };
  return a;
}

The second approach is to define your map cell reactively, so that any other cell referencing it will recompute automatically whenever the map changes. To do that, change your map cell to be a view:

viewof map = …

Change the value of the view to be the SVG element itself (a bit self-referential, but it works):

newSvg.property("value", newSvg.node());

And then inside your click event handler, dispatch an input event to tell Observable that the view’s value changed:

.on("click", d => {
  newSvg.dispatch("input");
  this.classList.toggle("selected");
})

A bit more on viewof here:


#4

Thanks a lot to both of you! Mike’s second strategy was the one that I was trying to get to, but couldn’t quite figure out the details of, though I also like the mutable idea.

Best,
Evan


#5

Generators.observe would probably be a simpler solution than viewof, since you can call the change callback (passing the SVG element) inside your click event listener.


#6

Not sure I completely follow - do you mean something like this?