Published
Edited
Oct 25, 2021
3 stars
Insert cell
md`# Connecting nodes`
Insert cell
svg = {
const origin = [
xScale(nodes[0].x) + rectWidth,
yScale(nodes[0].y) + rectHeight / 2
];

const curve = shape.line().curve(shape.curveBumpX);

let connectionMode = false;

let onArea = false;

const opacityOff = 0.2;
const opacityOn = 1;

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 400])
.on("click", function () {
if (!connectionMode) return;

if (!onArea) {
connectionMode = false;
hideConnectionLine();
setDefaultAllNodes();
}
})
.on("mousemove", function (event) {
if (!connectionMode) return;

const pos = d3.mouse(this);

const newPoints = [origin, pos];

moveConnection(newPoints);
});

svg
.selectAll("rect")
.data(nodes)
.enter()
.append("rect")
.attr("class", "node")
.attr("id", (d, i) => "node_" + i)
.attr("stroke", "black")
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("stroke-opacity", opacityOn)
.attr("x", function (d) {
return xScale(d.x);
})
.attr("y", function (d) {
return yScale(d.y);
})

.attr("width", rectWidth)
.attr("height", rectHeight);

drawAddButton();

const points = [origin, [300, 150]];

svg
.append("path")
.attr("d", curve(points))
.attr("id", "connection")
.attr("stroke", "transparent")
.attr("fill", "none");

svg
.append("path")
.attr("d", drawArrow([300, 150]))
.attr("id", "arrow")
.attr("stroke", "transparent")
.attr("fill", "transparent");

svg
.selectAll("ellipse")
.data(nodes)
.enter()
.append("ellipse")
.attr("class", "area")
.attr("id", (d, i) => "area_" + i)
.attr("stroke", "transparent")
.attr("fill", "transparent")
.attr("cx", (d) => getNodeCenter(d).x)
.attr("cy", (d) => getNodeCenter(d).y)
.attr("rx", rectWidth * 1)
.attr("ry", rectHeight * 1.5)
.on("mouseenter", function (d, i) {
if (connectionMode) {
if (!isAvailable(i)) return;

onArea = true;

highlightNode(i);
}

if (!connectionMode && i === 0) {
onArea = true;
showAddButton();
}
})
.on("mouseout", (d, i) => {
onArea = false;

if (!connectionMode) {
setDefaultAllNodes();

if (i === 0) {
hideAddButton();
}

return;
}

if (i === 0) return;

svg
.select("#node_" + i)
.attr("stroke", "black")
.attr("stroke-width", 1);
})
.on("click", (d, i) => {
if (connectionMode) {
if (!isAvailable(i)) return;

connectNode(d);

setDefaultAllNodes();

stopConnectionMode();
} else {
if (i === 0) {
startConnectionMode();
showConnectionLine();
hideAddButton();
("");
}
}
});

return svg.node();

function drawAddButton() {
const posX = xScale(nodes[0].x) + rectWidth + 15;
const posY = yScale(nodes[0].y) + rectHeight / 2;
const lineWidth = 7;

const gAdd = svg
.append("g")
.attr("id", "addButton")
.attr("display", "none")
.style("cursor", "pointer");

gAdd
.append("circle")
.attr("fill", "white")
.attr("stroke-width", 1)
.attr("stroke", "white")
.attr("stroke-opacity", 0.5)
.attr("cx", posX)
.attr("cy", posY)
.attr("r", 8);

gAdd
.append("path")
.attr("stroke", "black")
.attr("d", `M ${posX - lineWidth / 2},${posY} h ${lineWidth} `);

gAdd
.append("path")
.attr("stroke", "black")
.attr("d", `M ${posX},${posY - lineWidth / 2} v ${lineWidth} `);
}

function drawArrow([x, y]) {
const arrowWidth = 6;
const arrowHeight = 4;

const x0 = x + arrowWidth;
const y0 = y;

const x1 = x;
const y1 = y + arrowHeight / 2;

const x2 = x;
const y2 = y - arrowHeight / 2;

return `M ${x0},${y0} ${x1},${y1} ${x2},${y2} z`;
}

function getNodeMiddleLeft({ x, y }) {
return [xScale(x), yScale(y) + rectHeight / 2];
}

function getOpacity(d, i) {
if (i === 0) return opacityOn;

return isAvailable(i) ? opacityOn : opacityOff;
}

function showAddButton() {
svg.select("#addButton").attr("display", "unset");
}

function hideAddButton() {
svg.select("#addButton").attr("display", "none");
}

function highlightNode(id) {
svg
.select("#node_" + id)
.attr("stroke", "orange")
.attr("stroke-opacity", opacityOn)
.attr("stroke-width", 2);

svg.select("#area_" + id).style("cursor", "pointer");
}

function setDefaultAllNodes() {
svg
.selectAll(".node")
.attr("stroke-opacity", opacityOn)
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("cursor", "default");

svg.selectAll(".area").style("cursor", "default");
}

function connectNode(node) {
const newPoints = [origin, getNodeMiddleLeft(node)];
svg.select("#connection").attr("d", curve(newPoints));
svg
.select("#arrow")
.attr("stroke", "transparent")
.attr("fill", "transparent");
}

function showConnectionLine() {
svg.select("#connection").attr("stroke", "black");
svg.select("#arrow").attr("stroke", "black").attr("fill", "black");
}

function hideConnectionLine() {
svg.select("#connection").attr("stroke", "transparent");
svg
.select("#arrow")
.attr("stroke", "transparent")
.attr("fill", "transparent");
}

function startConnectionMode() {
highlightNode(0);
blurUnavailableNodes();
connectionMode = true;
}

function stopConnectionMode() {
setDefaultAllNodes();
connectionMode = false;
}

function blurUnavailableNodes() {
svg.selectAll(".node").attr("stroke-opacity", getOpacity);
}

function moveConnection(thePoints) {
svg.select("#connection").attr("d", curve(thePoints));
svg.select("#arrow").attr("d", drawArrow(thePoints[1]));
}

function isAvailable(id) {
return id > 10 && id !== 0;
}
}
Insert cell
rectHeight = 15
Insert cell
rectWidth = 50
Insert cell
getNodeCenter = function ({x, y}) {
return {
x: xScale(x) + rectWidth / 2,
y: yScale(y) + rectHeight / 2
};
}
Insert cell
xScale = d3
.scaleLinear()
.domain([
0,
d3.max(nodes, function (d) {
return d.x;
})
])
.range([0, width]) // Set margins for x specific
Insert cell
yScale = d3
.scaleLinear()
.domain([
0,
d3.max(nodes, function (d) {
return d.y;
})
])
.range([0, 400]) // Set
Insert cell
nodes = [
{ id: 0, x: 1, y: 1 },
{ id: 1, x: 100, y: 110 },
{ id: 2, x: 83, y: 43 },
{ id: 3, x: 92, y: 28 },
{ id: 4, x: 49, y: 74 },
{ id: 5, x: 51, y: 10 },
{ id: 6, x: 25, y: 98 },
{ id: 7, x: 77, y: 30 },
{ id: 8, x: 20, y: 83 },
{ id: 9, x: 11, y: 63 },
{ id: 10, x: 4, y: 55 },
{ id: 11, x: 85, y: 100 },
{ id: 12, x: 60, y: 40 },
{ id: 13, x: 70, y: 80 },
{ id: 14, x: 10, y: 20 },
{ id: 15, x: 40, y: 50 },
{ id: 16, x: 25, y: 31 }
]
Insert cell
d3 = require("d3@v5")
Insert cell
shape = require("d3-shape")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more