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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more