console
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Canvas Example</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
.node-image {
width: 40px;
height: 40px;
cursor: pointer;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
marker-end: url(#arrowhead);
}
.canvas-popover {
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
padding: 5px 10px;
z-index: 1000;
display: block;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.canvas {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div id="canvas-container" style="width: 100%; height: 100vh;">
<svg id="canvas1" class="canvas" width="100%" height="300" style="background-color: #f0f0f0;"></svg>
<svg id="canvas2" class="canvas" width="100%" height="300" style="background-color: #e0e0e0;"></svg>
<svg id="canvas3" class="canvas" width="100%" height="300" style="background-color: #d0d0d0;"></svg>
</div>
<script>
const width = 100;
const height = 100;
const canvas1 = d3.select("#canvas1");
const canvas2 = d3.select("#canvas2");
const canvas3 = d3.select("#canvas3");
function defineArrowhead(svg) {
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "-0 -5 10 10")
.attr("refX", 20)
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999");
}
defineArrowhead(canvas1);
defineArrowhead(canvas2);
defineArrowhead(canvas3);
function percentToPixels(percent, dimension) {
return (percent / 100) * dimension;
}
const nodesCanvas1 = [
{ id: 0, x: 10, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 1" },
{ id: 1, x: 30, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 2" },
{ id: 2, x: 50, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 3" },
{ id: 3, x: 50, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 4" }
];
const nodeGroup1 = canvas1.selectAll(".node")
.data(nodesCanvas1)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d, i, nodes) => {
const svgWidth = nodes[i].getBoundingClientRect().width;
const svgHeight = nodes[i].getBoundingClientRect().height;
const x = percentToPixels(d.x, svgWidth);
const y = percentToPixels(d.y, svgHeight);
return `translate(${x},${y})`;
});
nodeGroup1.append("image")
.attr("x", -20)
.attr("y", -20)
.attr("width", 40)
.attr("height", 40)
.attr("xlink:href", d => d.imageUrl);
nodeGroup1.each(function(d, i) {
const node = d3.select(this);
const nodeScreenCoords = node.node().getBoundingClientRect();
const popover = d3.select("#canvas-container").append("div")
.attr("class", "canvas-popover")
.style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
.style("top", nodeScreenCoords.top - 50 + "px");
popover.append("p")
.text(`Click to jump from ${d.title}:`);
popover.append("a")
.attr("href", "#")
.text("Go to Link")
.on("click", (event) => {
event.preventDefault();
alert(`Clicked on ${d.title}`);
});
});
nodesCanvas1.forEach((d, i) => {
if (i < nodesCanvas1.length - 1) {
const source = nodesCanvas1[i];
const target = nodesCanvas1[i + 1];
canvas1.append("line")
.attr("x1", percentToPixels(source.x, canvas1.node().getBoundingClientRect().width))
.attr("y1", percentToPixels(source.y, canvas1.node().getBoundingClientRect().height))
.attr("x2", percentToPixels(target.x, canvas1.node().getBoundingClientRect().width))
.attr("y2", percentToPixels(target.y, canvas1.node().getBoundingClientRect().height))
.attr("class", "link");
}
});
canvas1.append("line")
.attr("x1", percentToPixels(nodesCanvas1[0].x, canvas1.node().getBoundingClientRect().width))
.attr("y1", percentToPixels(nodesCanvas1[0].y, canvas1.node().getBoundingClientRect().height))
.attr("x2", percentToPixels(nodesCanvas1[0].x, canvas1.node().getBoundingClientRect().width))
.attr("y2", percentToPixels(nodesCanvas1[3].y, canvas1.node().getBoundingClientRect().height))
.attr("class", "link");
const nodesCanvas2 = [
{ id: 0, x: 25, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 5" },
{ id: 1, x: 25, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 6" },
{ id: 2, x: 25, y: 50, imageUrl: "https://via.placeholder.com/40", title: "Node 7" }
];
const nodeGroup2 = canvas2.selectAll(".node")
.data(nodesCanvas2)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d, i, nodes) => {
const svgWidth = nodes[i].getBoundingClientRect().width;
const svgHeight = nodes[i].getBoundingClientRect().height;
const x = percentToPixels(d.x, svgWidth);
const y = percentToPixels(d.y, svgHeight);
return `translate(${x},${y})`;
});
nodeGroup2.append("image")
.attr("x", -20)
.attr("y", -20)
.attr("width", 40)
.attr("height", 40)
.attr("xlink:href", d => d.imageUrl);
nodeGroup2.each(function(d, i) {
const node = d3.select(this);
const nodeScreenCoords = node.node().getBoundingClientRect();
const popover = d3.select("#canvas-container").append("div")
.attr("class", "canvas-popover")
.style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
.style("top", nodeScreenCoords.top - 50 + "px");
popover.append("p")
.text(`Click to jump from ${d.title}:`);
popover.append("a")
.attr("href", "#")
.text("Go to Link")
.on("click", (event) => {
event.preventDefault();
alert(`Clicked on ${d.title}`);
});
});
nodesCanvas2.forEach((d, i) => {
if (i < nodesCanvas2.length - 1) {
const source = nodesCanvas2[i];
const target = nodesCanvas2[i + 1];
canvas2.append("line")
.attr("x1", percentToPixels(source.x, canvas2.node().getBoundingClientRect().width))
.attr("y1", percentToPixels(source.y, canvas2.node().getBoundingClientRect().height))
.attr("x2", percentToPixels(target.x, canvas2.node().getBoundingClientRect().width))
.attr("y2", percentToPixels(target.y, canvas2.node().getBoundingClientRect().height))
.attr("class", "link");
}
});
const nodesCanvas3 = [
{ id: 0, x: 10, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 8" },
{ id: 1, x: 30, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 9" },
{ id: 2, x: 30, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 10" }
];
const nodeGroup3 = canvas3.selectAll(".node")
.data(nodesCanvas3)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d, i, nodes) => {
const svgWidth = nodes[i].getBoundingClientRect().width;
const svgHeight = nodes[i].getBoundingClientRect().height;
const x = percentToPixels(d.x, svgWidth);
const y = percentToPixels(d.y, svgHeight);
return `translate(${x},${y})`;
});
nodeGroup3.append("image")
.attr("x", -20)
.attr("y", -20)
.attr("width", 40)
.attr("height", 40)
.attr("xlink:href", d => d.imageUrl);
nodeGroup3.each(function(d, i) {
const node = d3.select(this);
const nodeScreenCoords = node.node().getBoundingClientRect();
const popover = d3.select("#canvas-container").append("div")
.attr("class", "canvas-popover")
.style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
.style("top", nodeScreenCoords.top - 50 + "px");
popover.append("p")
.text(`Click to jump from ${d.title}:`);
popover.append("a")
.attr("href", "#")
.text("Go to Link")
.on("click", (event) => {
event.preventDefault();
alert(`Clicked on ${d.title}`);
});
});
nodesCanvas3.forEach((d, i) => {
if (i < nodesCanvas3.length - 1) {
const source = nodesCanvas3[i];
const target = nodesCanvas3[i + 1];
canvas3.append("line")
.attr("x1", percentToPixels(source.x, canvas3.node().getBoundingClientRect().width))
.attr("y1", percentToPixels(source.y, canvas3.node().getBoundingClientRect().height))
.attr("x2", percentToPixels(target.x, canvas3.node().getBoundingClientRect().width))
.attr("y2", percentToPixels(target.y, canvas3.node().getBoundingClientRect().height))
.attr("class", "link");
}
});
const canvasPositions = [
{ x: 50, y: 50 },
{ x: 50, y: 50 },
{ x: 50, y: 50 }
];
d3.select("#canvas-container")
.selectAll(".canvas-line")
.data(canvasPositions.slice(0, 2))
.enter()
.append("line")
.attr("class", "canvas-line")
.attr("x1", (d, i) => percentToPixels(canvasPositions[i].x, canvas1.node().getBoundingClientRect().width) + 50)
.attr("y1", (d, i) => percentToPixels(canvasPositions[i].y, canvas1.node().getBoundingClientRect().height) + 150)
.attr("x2", (d, i) => percentToPixels(canvasPositions[i + 1].x, canvas2.node().getBoundingClientRect().width) + 50)
.attr("y2", (d, i) => percentToPixels(canvasPositions[i + 1].y, canvas2.node().getBoundingClientRect().height) + 150)
.attr("class", "link");
</script>
</body>
</html>