Observable Framework : Resolve Promise with resize in a div

I have a buffer() function based on npm:geotoolbox. The 1st parameter is the size of the returned svg. The 2nd parameter is the buffer distance.

In work in a div element :

This works.
${buffer(200, k)}

But this returns [object Promise]
${resize((width) => buffer(width, k))}

How do I resolve this Promise?

Hi,

Is the buffer() function you mentioned a wrapper around https://github.com/riatelab/geotoolbox? Could you post the full code snippet including the div?

Thanks!

The component (js file)

// Buffer
export async function buffer(width, k) {
  const aus_simpl = await geo.buffer(aus, { dist: k });
  let svg = viz.create({
    projection: d3.geoEquirectangular(),
    domain: aus_simpl,
    margin: 1,
    width,
  });

  svg.path({
    data: aus,
    fill: "#D75C48",
    fillOpacity: 0.3,
    stroke: "none",
  });
  svg.path({
    data: aus_simpl,
    fill: "none",
    stroke: "#D75C48",
    strokeWidth: 2,
    strokeDasharray: [4, 2],
  });

  return Promise.resolve(svg.render());
}

and the page (md file)

 <div class="card">
   <h2><b>Buffer</b></h2>
       ${buffer_k}
       <br/>
       ${buffer(200, bufferk)}
       

 </div>

Hi,

The function passed into resize needs to return an element. ${buffer(200, k)} works because it is automatically resolved. You can modify this file locally: framework/src/client/stdlib/resize.ts at 2945151b5a6abacfadf338bac7d10544ae576843 · observablehq/framework · GitHub to

  const observer = new ResizeObserver(async ([entry]) => {
    const {width, height} = entry.contentRect;
    while (div.lastChild) div.lastChild.remove();
    if (width > 0) {
      const child = await run(width, height);
      // prevent feedback loop if height is used
      if (run.length !== 1 && isElement(child)) child.style.position = "absolute";
      div.append(child);
    }
  });

Hope this helps!

You don’t want to recompute the buffer every time the window resizes, I expect. So probably better to move the async buffer calculation outside of the render call, and instead pass it in. That way your render function can be sync.

1 Like

To give a complete example of moving the buffer calculation outside of resize:

```js
import * as geo from "npm:geotoolbox";
```

```js
const world = fetch("https://static.observableusercontent.com/files/f00996f6cc84c5676a169075acba6d90cabb161fd36c4c91e5106cb8316028c36183a1e58c5d4260dae229421d91eb8cc15555eea39c4edc1f2a4e897082f2d8?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27countries.json").then((_) => _.json());
```

```js
const dist = view(Inputs.range([-1000, 5000], {label: "Distance", value: 300, step: 100}));
const quadsegs = view(Inputs.range([1, 8], {label: "quadsegs", value: 8, step: 1}));
```

```js
const buffered = geo.buffer(world, {dist, quadsegs});
```

<div class="card">
  ${resize((width) =>
    Plot.plot({
      width,
      projection: "equal-earth",
      marks: [
        Plot.geo(world, {stroke: "blue"}),
        Plot.geo(buffered, {stroke: "red"})
      ]
    })
  )}
</div>
1 Like

If you really want to have an async component (like, you don’t want to declare buffered as a top-level variable as above), another option is to return an element synchronously, and then add the async content later. That looks like this:

function buffermap(data, {width, dist, quadsegs}) {
  const div = document.createElement("div");
  geo.buffer(world, {dist, quadsegs}).then((buffered) => {
    div.append(Plot.plot({
      width,
      projection: "equal-earth",
      marks: [
        Plot.geo(world, {stroke: "blue"}),
        Plot.geo(buffered, {stroke: "red"})
      ]
    }));
  });
  return div;
}

(You could even use a WeakMap to cache the buffered calculation so that you don’t have to recompute it on resize.)

3 Likes

Thanks a lot