Public
Edited
Jun 9, 2023
23 forks
Importers
190 stars
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
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 its a line)
wrapper.selectAll("path").each(function (data, index, nodes) {
// For line charts, set the pointer events to be visible stroke
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;

const tip = wrapper
.selectAll(".hover")
.data([1])
.join("g")
.attr("class", "hover")
.style("pointer-events", "none")
.style("text-anchor", "middle");

// 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, wrapper.node());
if (text) tip.call(hover, pointer, text.split("\n"));
else tip.selectAll("*").remove();

// Raise it
d3.select(this).raise();
// Keep within the parent horizontally
const tipSize = tip.node().getBBox();
if (pointer[0] + tipSize.x < 0)
tip.attr(
"transform",
`translate(${tipSize.width / 2}, ${pointer[1] + 7})`
);
else if (pointer[0] + tipSize.width / 2 > wrapper.attr("width"))
tip.attr(
"transform",
`translate(${wrapper.attr("width") - tipSize.width / 2}, ${
pointer[1] + 7
})`
);
})
.on("pointerout", function (event) {
tip.selectAll("*").remove();
// Lower it!
d3.select(this).lower();
});
});

// Remove the tip if you tap on the wrapper (for mobile)
wrapper.on("touchstart", () => tip.selectAll("*").remove());

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

return chart;
}
Insert cell
// Function to position the tooltip
hover = (tip, pos, text) => {
const side_padding = 10;
const vertical_padding = 5;
const vertical_offset = 15;

// Empty it out
tip.selectAll("*").remove();

// Append the text
tip
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("transform", `translate(${pos[0]}, ${pos[1] + 7})`)
.selectAll("text")
.data(text)
.join("text")
.style("dominant-baseline", "ideographic")
.text((d) => d)
.attr("y", (d, i) => (i - (text.length - 1)) * 15 - vertical_offset)
.style("font-weight", (d, i) => (i === 0 ? "bold" : "normal"));

const bbox = tip.node().getBBox();

// Add a rectangle (as background)
tip
.append("rect")
.attr("y", bbox.y - vertical_padding)
.attr("x", bbox.x - side_padding)
.attr("width", bbox.width + side_padding * 2)
.attr("height", bbox.height + vertical_padding * 2)
.style("fill", "white")
.style("stroke", "#d3d3d3")
.lower();
}
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 "@mkfreeman/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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more