Public
Edited
Aug 5, 2024
Fork of Plot Tooltip
Insert cell
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.dot(data, {
x: "bill_length",
y: "bill_depth",
title: (d) =>
`${d.island} \n bill depth: ${d.bill_depth} \n bill length: ${d.bill_length}` // \n makes a new line
})
]
})
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.dot(data, {
x: "bill_length",
y: "bill_depth",
title: (d) =>
`${d.island} \n bill depth: ${d.bill_depth} \n bill length: ${d.bill_length}` // \n makes a new line
})
],
tooltip: {
fill: "red",
stroke: "blue",
r: 8
}
})
Insert cell
Insert cell
Insert cell
Insert cell
// Pass any plot to addTooltips -- just set the `title` attribute for your marks!
addTooltips(
Plot.dot(data, {
x: "bill_length",
y: "bill_depth",
title: (d) =>
`${d.island} \n bill depth: ${d.bill_depth} \n bill length: ${d.bill_length}` // \n makes a new line
}).plot()
)
Insert cell
Insert cell
addTooltips(
Plot.rectY(
data,
Plot.binX(
{ y: "count", title: (elems) => `${elems.length} rows` },
{ x: "body_mass", thresholds: 20 }
)
).plot(),
// Set styles for the hovered element
{ fill: "gray", opacity: 0.5, "stroke-width": "3px", stroke: "red" }
)
Insert cell
addTooltips(
Plot.dot(
data,
Plot.binX(
{ y: "count", title: (elems) => `${elems.length} rows` },
{ x: "body_mass", thresholds: 20 }
)
).plot(),
// Set styles for the hovered element
{ fill: "gray", opacity: 0.5, "stroke-width": "3px", stroke: "red" }
)
Insert cell
Insert cell
addTooltips(
Plot.barX(data, {
x: "body_mass",
fill: "island",
title: (d) => d.island + "\n" + d.bill_length
}).plot({
// caption: "test",
style: { paddingTop: 30 },
width
})
)
Insert cell
addTooltips(
Plot.plot({
y: {
label: "↑ Unemployed (thousands)"
},
marks: [
Plot.areaY(
unemployment,
Plot.stackY({
x: "date",
y: "unemployed",
fill: "industry",
z: "industry",
title: "industry",
order: "max",
reverse: true,
stroke: "#ddd"
})
),
Plot.ruleY([0])
],
style: {
pointerEvents: "all"
},
color: {
legend: true,
columns: "110px",
width: 640
}
})
)
Insert cell
addTooltips(
Plot.plot({
y: {
label: "↑ Unemployed (thousands)"
},
marks: [
Plot.line(unemployment, {
x: "date",
y: "unemployed",
stroke: "lightgrey",
z: "industry",
title: "industry"
}),
Plot.ruleY([0])
]
})
)
Insert cell
Insert cell
import { data as brandData } from "@observablehq/plot-cheatsheets-marks"
Insert cell
addTooltips(
Plot.plot({
marks: [
Plot.cell(brandData, {
x: "date",
y: "brand",
fill: "value",
title: "value"
})
],
color: { scheme: "blues", legend: true, reverse: false },
marginLeft: 100,
x: { tickFormat: null },
width: 578
})
)
Insert cell
Insert cell
Insert cell
addTooltips = (chart, styles) => {
const stroke_styles = { stroke: "blue", "stroke-width": 3 };
const fill_styles = { fill: "blue", opacity: 0.5 };

// Workaround if it's in a figure
const type = d3.select(chart).node().tagName;
let wrapper =
type === "FIGURE" ? d3.select(chart).select("svg") : d3.select(chart);

// Workaround if there's a legend....
const svgs = d3.select(chart).selectAll("svg");
if (svgs.size() > 1) wrapper = d3.select([...svgs].pop());
wrapper.style("overflow", "visible"); // to avoid clipping at the edges

// Set pointer events to visibleStroke if the fill is none (e.g., if it's a line)
wrapper.selectAll("path").each(function () {
if (
d3.select(this).attr("fill") === null ||
d3.select(this).attr("fill") === "none"
) {
d3.select(this).style("pointer-events", "visibleStroke");
if (styles === undefined) styles = stroke_styles;
}
});

if (styles === undefined) styles = fill_styles;

// Add a unique id to the chart for styling
const id = id_generator();

// Add the event listeners
d3.select(chart).classed(id, true); // using a class selector so that it doesn't overwrite the ID

wrapper.selectAll("title").each(function () {
// Get the text out of the title, set it as an attribute on the parent, and remove it
const title = d3.select(this); // title element that we want to remove
const parent = d3.select(this.parentNode); // visual mark on the screen
const t = title.text();

if (t) {
parent.attr("__title", t).classed("has-title", true);
title.remove();
}

// Mouse events
parent
.on("pointerenter pointermove", function (event) {
const text = d3.select(this).attr("__title");
const pointer = d3.pointer(event, d3.select("body").node());

// Create tooltip div and append it to the body if it doesn't already exist
let tip = d3.select("body").select(".tooltip");

if (tip.empty()) {
tip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
.style("min-width", "30px")
.style("height", "auto")
.style("display", "flex")
.style("flex-direction", "column")
.style("align-items", "center")
.style("color", "black")
.style(
"background-image",
"linear-gradient(to bottom, rgba(255, 255, 255, 0.688), rgba(240, 240, 240, 0.688))"
)
.style("padding", "4px 8px")
.style("border-radius", "6px")
.style("border", "1px solid #565656")
.style("box-shadow", "0 1px 4px 0 rgba(0, 0, 0, 0.2)")
.style("pointer-events", "none")
.style("backdrop-filter", "blur(5px)")
.style("font", "0.85rem calibri")
.style(
"transition",
"0.1s opacity ease-out, 0.1s border-color ease-out, 0.2s left ease-out, 0.2s top ease-out"
)
.style("z-index", 9999999);
}
if (text) {
hover(tip, pointer, text.split("\n"));
}
// Raise it
d3.select(this).raise();
})
.on("pointerout", function () {
// Remove the tooltip div from the DOM
d3.select("body").select(".tooltip").remove();

// Lower it!
d3.select(this).lower();
});
});

// Remove the tip if you tap on the wrapper (for mobile)
wrapper.on("touchstart", () => d3.select("body").select(".tooltip").remove());

// Define the styles
const styleElement = document.createElement("style");
styleElement.innerHTML = `
.${id} .has-title { cursor: pointer; pointer-events: all; }
.${id} .has-title:hover { ${Object.entries(styles)
.map(([key, value]) => `${key}: ${value};`)
.join(" ")} }
`;
chart.appendChild(styleElement);

return chart;
}
Insert cell
// Function to position the tooltip
hover = (tip, pos, text) => {
// const rect = tip.node().getBoundingClientRect();

tip
.style("opacity", 1)
.style("text-anchor", "middle")
.style("pointer-events", "none")
.style("left", `${pos[0] + 30}px`)
.style("top", `${pos[1] + 30}px`)
.selectAll("span")
.data(text)
.join("span") // Create span elements here
.style("dominant-baseline", "ideographic")
.text((d) => d)
.style("font-weight", (d, i) => (i === 0 ? "bold" : "normal"));
}
Insert cell
// To generate a unique ID for each chart so that they styles only apply to that chart
id_generator = () => {
var S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return "a" + S4() + S4();
}
Insert cell
// automatically add tooltips to every Plot in a notebook
// this allows users to do import { Plot } from "@dheerajyss/plot-tooltip"
Plot = tooltipPlugin(await require("@observablehq/plot"))
Insert cell
tooltipPlugin = (Plot) => {
const { plot } = Plot;
Plot.plot = ({ tooltip, ...options }) => addTooltips(plot(options), tooltip);
return Plot;
}
Insert cell
import { data } from "@observablehq/plot-exploration-penguins"
Insert cell
import { unemployment } from "@zanarmstrong/highlight-color-w-dropdown"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more