dag2Svg = {
const width = 680, height = 520;
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", width)
.attr("height", height)
.attr("class", "dag-svg")
.style("touch-action", "none")
.style("max-width", "100%")
.style("display", "block")
.style("margin", "0 auto");
svg.append("defs").append("marker")
.attr("id", "dag2-arrow")
.attr("markerWidth", 14).attr("markerHeight", 14)
.attr("refX", 11).attr("refY", 4.5)
.attr("orient", "auto").attr("markerUnits", "strokeWidth")
.append("path")
.attr("d", "M0,0 L0,9 L13,4.5 z")
.attr("fill", "currentColor");
const lineEls = dag2Lines.map(ln => {
const sel = svg.append("line")
.attr("x1", ln.x1).attr("y1", ln.y1)
.attr("x2", ln.x2).attr("y2", ln.y2)
.attr("stroke", "#333").attr("stroke-width", 3)
.attr("marker-end", "url(#dag2-arrow)");
return { ...ln, sel };
});
svg.append("text").attr("x", 75).attr("y", 500).attr("font-size", 34).text("c");
svg.append("text").attr("x", 75).attr("y", 55).attr("font-size", 34).text("X");
svg.append("text").attr("x", 600).attr("y", 195).attr("font-size", 34).text("Y");
const tipG = svg.append("g").attr("display", "none");
const tipRect = tipG.append("rect")
.attr("rx", 8).attr("fill", "white").attr("fill-opacity", 0.97)
.attr("stroke", "#2563eb").attr("stroke-width", 2);
const tipTextG = tipG.append("g");
// Appended *after* the tooltip group, so the +/- signs always paint
// on top of it (SVG stacks later siblings above earlier ones) --
// otherwise a sign sitting near where the tooltip box happens to pop
// up would get hidden behind it.
const signsG = svg.append("g");
dag2Lines.forEach(ln => {
signsG.append("text")
.attr("x", ln.signX).attr("y", ln.signY)
.attr("text-anchor", "middle")
.attr("font-size", 30).attr("font-weight", "bold")
.attr("fill", ln.sign === "+" ? "#15803d" : "#b91c1c")
.text(ln.sign);
});
const lineHeight = 22;
const padX = 14, padY = 14;
const maxCharsPerLine = 30;
function showTip(mx, my, ln) {
lineEls.forEach(o => o.sel
.attr("stroke", o.key === ln.key ? "#2563eb" : "#333")
.attr("stroke-width", o.key === ln.key ? 5 : 3));
const lines = dagWrapText(ln.text, maxCharsPerLine);
tipTextG.selectAll("*").remove();
const boxW = Math.max(...lines.map(l => l.length)) * 9 + padX * 2;
const boxH = lines.length * lineHeight + padY * 2 - 6;
let bx = mx + 14, by = my - boxH - 14;
if (bx + boxW > width - 2) bx = mx - boxW - 14;
if (bx < 2) bx = 2;
if (by < 2) by = my + 14;
if (by + boxH > height - 2) by = height - boxH - 2;
tipRect.attr("x", bx).attr("y", by).attr("width", boxW).attr("height", boxH);
lines.forEach((line, i) => {
tipTextG.append("text")
.attr("x", bx + padX).attr("y", by + padY + 6 + i * lineHeight)
.attr("font-size", 17).attr("fill", "#1e3a8a")
.text(line);
});
tipG.attr("display", null);
}
function hideTip() {
tipG.attr("display", "none");
lineEls.forEach(o => o.sel.attr("stroke", "#333").attr("stroke-width", 3));
}
const hitRadius = 22;
function findLine(mx, my) {
for (const ln of lineEls) {
if (dagDistToSegment(mx, my, ln.x1, ln.y1, ln.x2, ln.y2) <= hitRadius) return ln;
}
return null;
}
const overlay = svg.append("rect")
.attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
.attr("fill", "transparent")
.style("cursor", "pointer");
overlay.on("pointermove", (event) => {
if (event.pointerType === "touch") return;
const [mx, my] = d3.pointer(event);
const ln = findLine(mx, my);
if (ln) showTip(mx, my, ln); else hideTip();
});
overlay.on("pointerleave", () => hideTip());
overlay.on("click", (event) => {
const [mx, my] = d3.pointer(event);
const ln = findLine(mx, my);
if (ln) showTip(mx, my, ln); else hideTip();
});
return svg.node();
}