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;
}
}