addTooltips = (chart, styles) => {
const stroke_styles = { stroke: "blue", "stroke-width": 2 };
const fill_styles = { fill: "blue", opacity: 0.5 };
const type = d3.select(chart).node().tagName;
let wrapper =
type === "FIGURE" ? d3.select(chart).select("svg") : d3.select(chart);
const svgs = d3.select(chart).selectAll("svg");
if (svgs.size() > 1) wrapper = d3.select([...svgs].pop());
wrapper.style("overflow", "visible");
const bbox = {
width: chart.clientWidth,
height: chart.clientHeight
};
wrapper.selectAll("path").each(function (data, index, nodes) {
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", "left");
// 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
})`
);
})
// click selection
.on("pointerdown", function (event) {
// First, find all elements with selected class, and remove class
let selectedElements = d3.selectAll(".selected")
selectedElements.classed("selected", false) // use .classed() because we used d3.selectAll
// Now get the target of the event, and add "selected" class
let selected = event.target;
selected.classList.toggle("selected"); // use .toggle() because we used event.target
console.log(selected, "This element is selected");
})
// when pointer leaves
.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()
// and remove the selected styling
d3.selectAll(".selected").classed("selected", false); // use .classed() because we used d3.selectAll
})
.on("pointerdown", () => {
// and remove the selected styling
// d3.selectAll(".selected").classed("selected", false); // use .classed() because we used d3.selectAll
// console.log("clear!")
});
// Define the styles
chart.appendChild(html`<style>
.${id} .has-title { cursor: pointer; pointer-events: all; }
.${id} .has-title.selected { stroke: orchid; strokeWidth: 10px }
.${id} .has-title:hover { ${Object.entries(styles).map(([key, value]) => `${key}: ${value};`).join(" ")} }`);
return chart;
}