Initial commit
This commit is contained in:
commit
02151694a8
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
# Connect Arrows D3.js Example
|
||||||
|
|
||||||
|
This is an example of how you can create a drag and drop system for arrows in D3.js
|
||||||
|
|
||||||
|
![Screenshot](./.github/images/screenshot.png)
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Connect Arrows</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"
|
||||||
|
integrity="sha512-M7nHCiNUOwFt6Us3r8alutZLm9qMt4s9951uo8jqO4UwJ1hziseL6O3ndFyigx6+LREfZqnhHxYjKRJ8ZQ69DQ=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script defer src="./script.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Minimal example of arrow connection in d3</h1>
|
||||||
|
<svg id="graph" height="900", width="900"></svg>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,212 @@
|
||||||
|
const data = {
|
||||||
|
nodes: [
|
||||||
|
{ id: 'a', name: 'A' },
|
||||||
|
{ id: 'b', name: 'B' },
|
||||||
|
{ id: 'c', name: 'C' },
|
||||||
|
{ id: 'd', name: 'D' },
|
||||||
|
{ id: 'e', name: 'E' },
|
||||||
|
{ id: 'f', name: 'F' },
|
||||||
|
{ id: 'g', name: 'G' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Format: { source: nodeId, target: nodeId, type: String }
|
||||||
|
links: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINK_TYPES = {
|
||||||
|
'Useful': '#b03d17',
|
||||||
|
'Important': '#b8f27a',
|
||||||
|
'Necessary': '#32a852',
|
||||||
|
'Logically Connected': '#110dde',
|
||||||
|
'Generalization': '#00f4fc',
|
||||||
|
'Synonym': '#bd00fc',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_HEIGHT = 100;
|
||||||
|
const NODE_WIDTH = 100;
|
||||||
|
const LINK_WIDTH = 8;
|
||||||
|
const COLOR_CIRCLE_RADIUS = 20;
|
||||||
|
|
||||||
|
const svg = d3.select('#graph');
|
||||||
|
|
||||||
|
let selectedLinkType = 'Useful';
|
||||||
|
|
||||||
|
svg.append('g').attr('id', 'links');
|
||||||
|
svg.append('g').attr('id', 'startNodes');
|
||||||
|
svg.append('g').attr('id', 'endNodes');
|
||||||
|
svg.append('g').attr('id', 'linkTypes');
|
||||||
|
|
||||||
|
const eventPositionToNode = (event) => {
|
||||||
|
const { x, y } = event;
|
||||||
|
const nodes = svg.selectAll('.node').nodes();
|
||||||
|
const node = nodes.find((node) => {
|
||||||
|
let { x: nodeX, y: nodeY } = node.getBoundingClientRect();
|
||||||
|
nodeX -= svg.node().getBoundingClientRect().x;
|
||||||
|
nodeY -= svg.node().getBoundingClientRect().y;
|
||||||
|
return x >= nodeX && x <= nodeX + NODE_WIDTH && y >= nodeY && y <= nodeY + NODE_HEIGHT;
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLinks = () => {
|
||||||
|
svg
|
||||||
|
.select('#links')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(data.links)
|
||||||
|
.join('line')
|
||||||
|
.attr('x1', (d) => {
|
||||||
|
const node = d3.select(`#startNode-${d.source}`);
|
||||||
|
let { x, y } = node.node().getBoundingClientRect();
|
||||||
|
x -= svg.node().getBoundingClientRect().x;
|
||||||
|
return x + NODE_WIDTH / 2;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr('y1', (d) => {
|
||||||
|
const node = d3.select(`#startNode-${d.source}`);
|
||||||
|
let { x, y } = node.node().getBoundingClientRect();
|
||||||
|
y -= svg.node().getBoundingClientRect().y;
|
||||||
|
return y + NODE_HEIGHT / 2;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr('x2', (d) => {
|
||||||
|
const node = d3.select(`#endNode-${d.target}`);
|
||||||
|
let { x, y } = node.node().getBoundingClientRect();
|
||||||
|
x -= svg.node().getBoundingClientRect().x;
|
||||||
|
return x + NODE_WIDTH / 2;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr('y2', (d) => {
|
||||||
|
const node = d3.select(`#endNode-${d.target}`);
|
||||||
|
let { x, y } = node.node().getBoundingClientRect();
|
||||||
|
y -= svg.node().getBoundingClientRect().y;
|
||||||
|
return y + NODE_HEIGHT / 2;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr("stroke-width", LINK_WIDTH)
|
||||||
|
.attr('stroke', (d) => LINK_TYPES[d.type])
|
||||||
|
.on('click', (_, d) => {
|
||||||
|
// Remove the link
|
||||||
|
const index = data.links.findIndex((link) => link.source === d.source && link.target === d.target);
|
||||||
|
data.links.splice(index, 1);
|
||||||
|
updateLinks();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const drag_handler = d3.drag()
|
||||||
|
.on("start", (event) => {
|
||||||
|
// Add a temporary arrow and let it follow the mouse
|
||||||
|
svg.append("line")
|
||||||
|
.attr("x1", event.x)
|
||||||
|
.attr("y1", event.y)
|
||||||
|
.attr("x2", event.x)
|
||||||
|
.attr("y2", event.y)
|
||||||
|
.attr("stroke-width", LINK_WIDTH)
|
||||||
|
.attr('stroke', LINK_TYPES[selectedLinkType])
|
||||||
|
.attr("marker-end", "url(#arrowhead)")
|
||||||
|
.attr("id", "temp-arrow");
|
||||||
|
|
||||||
|
})
|
||||||
|
.on("drag", (event) => {
|
||||||
|
svg.select("#temp-arrow")
|
||||||
|
.attr("x2", event.x)
|
||||||
|
.attr("y2", event.y);
|
||||||
|
})
|
||||||
|
.on("end", (event) => {
|
||||||
|
// 1. Track the node that the arrow is pointing to, if any, and add it to the links
|
||||||
|
// 2. Remove the temporary arrow
|
||||||
|
// 3. Redraw the links
|
||||||
|
const target = eventPositionToNode(event);
|
||||||
|
if (target) {
|
||||||
|
const d3Target = d3.select(target);
|
||||||
|
|
||||||
|
// Remove existing link between the two nodes if any.
|
||||||
|
const existingLink = data.links.findIndex((link) => {
|
||||||
|
link.source === event.subject.id && link.target === d3Target.datum().id;
|
||||||
|
})
|
||||||
|
if (existingLink !== -1) {
|
||||||
|
data.links.splice(existingLink, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.links.push({
|
||||||
|
source: event.subject.id,
|
||||||
|
target: d3Target.datum().id,
|
||||||
|
type: selectedLinkType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
svg.select("#temp-arrow").remove();
|
||||||
|
updateLinks();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const startNodes = svg
|
||||||
|
.select('#startNodes')
|
||||||
|
.selectAll('g')
|
||||||
|
.data(data.nodes, d => d.id)
|
||||||
|
.join("g")
|
||||||
|
.attr('id', d => `startNode-${d.id}`)
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('transform', (_d, i) => `translate(100, ${i * (NODE_HEIGHT + 10)})`)
|
||||||
|
.attr('style', 'cursor: pointer;')
|
||||||
|
.call((node) => node
|
||||||
|
.append('rect')
|
||||||
|
.attr('width', NODE_WIDTH)
|
||||||
|
.attr('height', NODE_HEIGHT)
|
||||||
|
.attr('fill', 'red')
|
||||||
|
)
|
||||||
|
.call((node) => node
|
||||||
|
.append('text')
|
||||||
|
.attr('style', 'fill: white; font-size: 30px;')
|
||||||
|
.attr('dy', 60)
|
||||||
|
.attr('dx', 40)
|
||||||
|
.text(d => d.name)
|
||||||
|
)
|
||||||
|
.call(drag_handler);
|
||||||
|
|
||||||
|
|
||||||
|
const endNodes = svg
|
||||||
|
.select('#endNodes')
|
||||||
|
.selectAll('g')
|
||||||
|
.data(data.nodes, d => d.id)
|
||||||
|
.join("g")
|
||||||
|
.attr('id', d => `endNode-${d.id}`)
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('transform', (_d, i) => `translate(500, ${i * (NODE_HEIGHT + 10)})`)
|
||||||
|
.call((node) => node
|
||||||
|
.append('rect')
|
||||||
|
.attr('width', NODE_WIDTH)
|
||||||
|
.attr('height', NODE_HEIGHT)
|
||||||
|
.attr('fill', 'blue')
|
||||||
|
)
|
||||||
|
.call((node) => node
|
||||||
|
.append('text')
|
||||||
|
.attr('style', 'fill: white; font-size: 30px;')
|
||||||
|
.attr('dy', 60)
|
||||||
|
.attr('dx', 40)
|
||||||
|
.text(d => d.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const linkTypes = svg
|
||||||
|
.select('#linkTypes')
|
||||||
|
.selectAll('g')
|
||||||
|
.data(Object.keys(LINK_TYPES))
|
||||||
|
.join("g")
|
||||||
|
.attr('id', d => d)
|
||||||
|
.call((node) => node
|
||||||
|
.append('circle')
|
||||||
|
.attr('cx', (_d, i) => 250 + (i * (COLOR_CIRCLE_RADIUS * 2 + 10)))
|
||||||
|
.attr('cy', 800)
|
||||||
|
.attr('r', COLOR_CIRCLE_RADIUS)
|
||||||
|
.attr('fill', (d) => LINK_TYPES[d])
|
||||||
|
.attr('stroke-width', 5)
|
||||||
|
)
|
||||||
|
.on('click', (event, d) => {
|
||||||
|
console.log('clicked', d);
|
||||||
|
d3.select('#linkTypes').selectAll('circle').attr('stroke', 'none');
|
||||||
|
selectedLinkType = d;
|
||||||
|
d3.select(event.target).attr('stroke', 'red');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
d3.select('#' + selectedLinkType).attr('stroke', 'red');
|
Reference in New Issue