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!!
1 Like
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 await
ing 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:
1 Like
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
1 Like