212 lines
5.6 KiB
JavaScript
212 lines
5.6 KiB
JavaScript
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'); |