tooltip = (Plot) => {
class Tooltip extends Plot.Mark {
constructor(
data,
{
x,
y,
z,
stroke = "black",
fill = "none",
r = 4,
content = (d) => d,
direction = "down",
tx,
ty,
dx = 0,
dy = 0,
onclick,
onmouseover,
annotate,
formatter = defaultFormatter,
...options
} = {}
) {
super(
data,
[
{ name: "x", value: x, scale: "x", optional: true },
{ name: "y", value: y, scale: "y", optional: true },
{ name: "z", value: z, optional: true },
{ name: "content", value: content }
],
options
);
this.r = r;
this.fill = fill;
this.stroke = stroke;
this.annotate = annotate;
this.direction = direction;
this.tx = tx;
this.ty = ty;
this.dx = dx;
this.dy = dy;
this.onclick = onclick;
this.onmouseover = onmouseover;
this.formatter = formatter;
}
render(
index,
scales,
{ x: X, y: Y, z: Z, content: T },
{ width, height, marginTop, marginRight, marginBottom, marginLeft }
) {
const {
r,
stroke,
fill,
annotate,
direction,
tx,
ty,
dx,
dy,
onclick,
onmouseover,
formatter
} = this;
const x = X
? (i) => X[i]
: constant((marginLeft + width - marginRight) / 2);
const y = Y
? (i) => Y[i]
: constant((marginTop + height - marginBottom) / 2);
const quadtree = d3
.quadtree()
.x(x)
.y(y)
.addAll(index.filter((i) => x(i) !== undefined && y(i) !== undefined));
const g = d3.create("svg:g");
const highlights = g.append("g");
let frozen = -1; // freeze the tooltip on click
const catcher = g
.append("rect")
.attr("height", height)
.attr("width", width)
.style("fill", "none")
.attr("pointer-events", "all")
.on("pointerenter", () => {})
.on("pointerout", (event) => frozen === -1 && hide())
.on("pointermove", move);
catcher.on("click", (event) => {
const i = find(event);
if (frozen > -1 && i > -1 && i !== frozen) {
show((frozen = i));
} else {
frozen = frozen === -1 ? i : -1;
}
if (typeof onclick === "function" && i >= 0)
onclick(event, i, g.node());
});
function find(event) {
const p = d3.pointers(event)[0],
i = quadtree.find(...p);
if (Math.hypot(p[0] - x(i), p[1] - y(i)) < 30) return i;
return -1;
}
function move(event) {
if (frozen > -1) return;
const i = find(event);
if (i > -1) {
show(i);
if (typeof onmouseover === "function") {
onmouseover(event, i, g.node());
}
} else hide();
}
let tooltip;
let xy;
hide();
setTimeout(() => {
// in case the user uses onclick / onmouseover for dataflow
const owner = ownerFigure(g.node());
owner.value = "";
owner.dispatchEvent(new CustomEvent("input"));
tooltip =
this.tooltip ||
(this.tooltip = d3
.select(owner.parentElement)
.insert("div", ":first-child")
.style("position", "relative")
.style("height", 0)
.style("pointer-events", "none")
.style("font", "10px sans-serif")
.style("z-index", 2));
xy = g
.select(function () {
return this.parentElement;
})
.attr("transform");
if (xy) xy = xy.replace(/(\d+)/g, "$1px"); // html wants px
}, 1);
return g.node();
function show(i) {
highlights
.selectAll("circle")
.data(index.filter((j) => i === j || (Z && Z[i] === Z[j])))
.join("circle")
.attr("r", r)
.style("fill", fill)
.style("stroke", stroke)
.attr("cx", x)
.attr("cy", y);
tooltip &&
tooltip.call(callout, {
formatter,
direction,
text: T[i],
x: tx === undefined ? x(i) : tx,
y: ty === undefined ? y(i) : ty,
transform: xy,
dx,
dy
});
}
function hide() {
tooltip && tooltip.call(callout);
highlights.html("");
if (annotate !== undefined && index.includes(annotate)) {
setTimeout(() => show(annotate), 200);
}
}
}
}
return function tooltip(data, options) {
return new Tooltip(data, options);
};
function constant(x) {
return () => x;
}
}