Tooltip Image

Hi all,

I’m trying to follow and combine the tutorials on image plotting and tooltips:
Work in progress observable: https://observablehq.com/d/cc4d71aba075ebfd

Currenlty it seems to only handle text such as in the tutorial on extracting image data there seems to be a workaround by adding a: const div = html<div></div> to the tooltip. But I have difficulties understanding the code block, and can’t seem to figure out where the image actually gets added in?

How would it be possible to add an image to a tooltip? To something like this:

Many thanks for your help in advance!
Ramon

1 Like

I feel like I answered almost this exact some question the other day, but here’s how to do it with TippyJS:

1 Like

Thanks so much for the super quick and clear explanation!!

A follow up. I am trying to add custom images via a .zip file.
Loading via sampleHeads = (FileAttachment(“sample-Heads.zip”)).zip()

This gives this error and I am unsure how to then access the file via the

?

1 Like

Here’s a notebook explaining how to use ZIP files in attachements:


In that notebook, you’ll see code like

dogZip = FileAttachment("Dog_Photos@3.zip").zip()

in one cell and then

dogZip.file('n02085936_2905.jpg').image()

in another cell.

Given the error you describe, I would guess that you’re combining these types of things into one cell, without awaiting the result, like so:

img =  {
  let dogZip = await FileAttachment("Dog_Photos@3.zip").zip();
  return dogZip.file('n02085936_2905.jpg').image()
}

The explicit await is not necessary in the example notebook since the runtime automatically forces cells to resolve before passing the result.

Yeah I had looked at the file but don’t understand how to use it inside a loop.

I just have this code as one line, it will sucessfully show the correct image

dogZip.file(dogImageNames[2]).image()

If I iterate through all the image names with [idx] and the correct image “path” e.g. ‘n02085936_2905.jpg’ is shown as a title, why wouldn’t the code below inside the scatter plot loop show an image?

let content = `<div><h2>${dogImageNames[idx]} </h2><img src="${dogZip.file(dogImageNames[idx]).image()}"></div>`;

Yeah, dealing with Promises can be tricky. I would say that MDN’s official documentation is the best you’re going to find. When dealing with iterables, it’s often the case that Promise.all, which takes an iterable of promises and resolves to an array, is the way to go.

If your objective is to generate a grid of images, I’d consider doing something like so:

pic_grid = {
  let dogZip = await FileAttachment("Dog_Photos@3.zip").zip();
  let names = d3
    .sort(
      (await dogZip.file("Readme.txt").text())
        .split("\r")
        .slice(3, -1)
        .map((s) => s.split("\t")),
      (a) => a[0]
    )
    .map((a) => a[1]);
  let imgs = await Promise.all(
    dogZip.filenames
      .filter((f) => f.slice(-4) == ".jpg")
      .map(async function (name, i) {
        let url = await dogZip.file(name).url();
        return await `<div style="display:inline-block"><h2>${
          names[i]
        }</h2><img width=${width / 4} src=${url} /></div>`;
      })
  );
  return html`${imgs}`;
}

Have a look:

I see, many thanks for the thorough example!! I had thought this would be super straight forward somehow… I’m trying to replace the images of the heads of the presidents with dogs from a zip.

So how do I wrap the existing d3.select(plot) into the Promise.all ? and why does it need the await then? Since displaying a single image with html works:

but when I have it inside the loop the image doesn’t show.

scatter_plot_with_image_tooltips2 = {
  let dogZipInside = await FileAttachment("54_Dogs.zip").zip();
  let dogImageNamesInside = await dogZipInside.filenames.filter(d => d.match(/\.jpg$/))
  let plot = Plot.plot({
    marks: [
      Plot.dot(data2, {
        fill: "black",
        x: "First Inauguration Date",
        y: (d) => metric.value(d) / 100,
        title: (d, i) => i
      })
    ]
  });
  
  d3.select(plot)
    .selectAll("circle")
    .nodes()
    .forEach(function (c) {
      let idx = parseInt(d3.select(c).select("title").text());
      let p = data2[idx];
      let dogURL = dogZip.file(dogImageNamesInside[idx]).url()
      let content = `<div><h2>${dogImageNamesInside[idx]} </h2><img src="${dogURL}"></div>`;
      console.log(content);
      tippy(c, {
        content: content,
        theme: "light",
        allowHTML: true
      });
      d3.select(c).select("title").remove();
    });
  
  return plot;
}

also if I try and place the await (also if I try and place an await in the code block below (to let) it will throw an error ‘Unexpected token’)

let url5 = await dogZip.file(dogImageNames[4]).url()

Without looking at your notebook, it’s hard to say but, again, Observable runs in a reactive manner which means, in part, that if Cell 2 depends on Cell 1, then Cell 2 will wait for Cell 1 to resolve before Cell 2 executes. Thus, the await is taken care of automatically by the runtime.

Well then, in your Scatter Plot notebook, your visualization is built on data stored in the favorability variable, which is a list of objects containing Name and Portrait URL keys. Let’s just format your dog data to match exactly that:

dog_data = {
  let dogZip = await FileAttachment("Dog_Photos@3.zip").zip();
  let breeds = d3
    .sort(
      (await dogZip.file("Readme.txt").text())
        .split("\r")
        .slice(3, -1)
        .map((s) => s.split("\t")),
      (a) => a[0]
    )
    .map((a) => a[1]);
  let urls = await Promise.all(
    dogZip.filenames
      .filter((f) => f.slice(-4) == ".jpg")
      .map(async function (name, i) {
        return dogZip.file(name).url();
      })
  );
  return d3.zip(breeds, urls).map((a) => ({ breed: a[0], url: a[1] }));
}

That’s not quite a drop in replacement for your favorability variable but it’s kinda close.

Here it is after a little fiddling with the Plot code. I assume you’d want some other data for the x or y channels.

1 Like

Thanks so much for all your help!! I got it to work now and created this small sample:
creating a plot that displays images associated with a csv dataset