324 lines
8.4 KiB
JavaScript
324 lines
8.4 KiB
JavaScript
"use strict";
|
|
//node ids are in order in which nodes come in existence
|
|
|
|
var univSvgHeight;
|
|
var univSvgWidth;
|
|
|
|
const nodes = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
|
|
const links = [
|
|
{ source: 0, target: 1 },
|
|
{ source: 0, target: 2 },
|
|
{ source: 1, target: 2 }
|
|
];
|
|
|
|
//universal width and height let index.htm control svg dimensions when needed
|
|
let lastNodeId = nodes.length;
|
|
var w = univSvgWidth ? univSvgWidth : 616,
|
|
h = univSvgHeight ? univSvgHeight : 400,
|
|
rad = 10,
|
|
wid = 10;
|
|
|
|
const svg = d3
|
|
.select("#svg-wrap")
|
|
.append("svg")
|
|
.attr("width", w)
|
|
.attr("height", h);
|
|
|
|
const dragLine = svg
|
|
.append("path")
|
|
.attr("class", "dragLine hidden")
|
|
.attr("d", "M0,0L0,0");
|
|
|
|
let edges = svg.append("g").selectAll(".edge");
|
|
|
|
let vertices = svg.append("g").selectAll(".vertex");
|
|
|
|
const force = d3
|
|
.forceSimulation()
|
|
.force(
|
|
"charge",
|
|
d3
|
|
.forceManyBody()
|
|
.strength(-300)
|
|
.distanceMax(w / 2)
|
|
)
|
|
.force("link", d3.forceLink().distance(60))
|
|
.force("x", d3.forceX(w / 2))
|
|
.force("y", d3.forceY(h / 2))
|
|
.on("tick", tick);
|
|
|
|
force.nodes(nodes);
|
|
force.force("link").links(links);
|
|
|
|
const fgColor = '#ffffff'
|
|
const colors = d3.schemeCategory10;
|
|
|
|
let mousedownNode = null;
|
|
|
|
const clrBtn = d3.select("#clear-graph");
|
|
clrBtn.on("click", clearGraph);
|
|
|
|
const nodename = (id) =>
|
|
id <= 26 ?
|
|
String.fromCharCode(id + 96) :
|
|
String.fromCharCode(parseInt(id / 26) + 96) + String.fromCharCode((id % 26) + 96);
|
|
|
|
//empties the graph
|
|
function clearGraph() {
|
|
nodes.splice(0);
|
|
links.splice(0);
|
|
lastNodeId = 0;
|
|
restart();
|
|
showGraphLatex();
|
|
}
|
|
|
|
//update the simulation
|
|
function tick() {
|
|
edges
|
|
.attr("x1", (d) => d.source.x)
|
|
.attr("y1", (d) => d.source.y)
|
|
.attr("x2", (d) => d.target.x)
|
|
.attr("y2", (d) => d.target.y);
|
|
|
|
vertices
|
|
.attr("cx", (d) => d.x)
|
|
.attr("cy", (d) => d.y);
|
|
}
|
|
|
|
function addNode() {
|
|
const e = d3.event;
|
|
if (e.button == 0) {
|
|
var coords = d3.mouse(e.currentTarget);
|
|
var newNode = { x: coords[0], y: coords[1], id: ++lastNodeId };
|
|
nodes.push(newNode);
|
|
restart();
|
|
showGraphLatex();
|
|
}
|
|
}
|
|
|
|
function removeNode(d, i) {
|
|
//to make ctrl-drag works for mac/osx users
|
|
if (d3.event.ctrlKey) return;
|
|
|
|
nodes.splice(nodes.indexOf(d), 1);
|
|
links
|
|
.filter((l) => l.source === d || l.target === d)
|
|
.foreach((l) => links.splice(links.indexOf(l), 1));
|
|
|
|
d3.event.preventDefault();
|
|
restart();
|
|
showGraphLatex();
|
|
}
|
|
|
|
function removeEdge(d, i) {
|
|
links.splice(links.indexOf(d), 1);
|
|
d3.event.preventDefault();
|
|
restart();
|
|
showGraphLatex();
|
|
}
|
|
|
|
function beginDragLine(d) {
|
|
//to prevent call of addNode through svg
|
|
d3.event.stopPropagation();
|
|
//to prevent dragging of svg in firefox
|
|
d3.event.preventDefault();
|
|
if (d3.event.ctrlKey || d3.event.button != 0) return;
|
|
|
|
mousedownNode = d;
|
|
dragLine
|
|
.classed("hidden", false)
|
|
.attr("d", `M${mousedownNode.x},${mousedownNode.y}L${mousedownNode.x},${mousedownNode.y}`);
|
|
}
|
|
|
|
function updateDragLine() {
|
|
var coords = d3.mouse(d3.event.currentTarget);
|
|
if (!mousedownNode) return;
|
|
dragLine.attr("d", `M${mousedownNode.x},${mousedownNode.y}L${coords[0]},${coords[1]}`);
|
|
}
|
|
|
|
function hideDragLine() {
|
|
dragLine.classed("hidden", true);
|
|
mousedownNode = null;
|
|
restart();
|
|
}
|
|
|
|
//no need to call hideDragLine() and restart() in endDragLine
|
|
//mouseup on vertices propagates to svg which calls hideDragLine
|
|
function endDragLine(d) {
|
|
if (!mousedownNode || mousedownNode === d) return;
|
|
//return if link already exists
|
|
if (links.some(l =>
|
|
(l.source === mousedownNode && l.target === d) ||
|
|
(l.source === d && l.target === mousedownNode)
|
|
)) return;
|
|
|
|
const newLink = { source: mousedownNode, target: d };
|
|
links.push(newLink);
|
|
showGraphLatex();
|
|
}
|
|
|
|
//one response per ctrl keydown
|
|
var lastKeyDown = -1;
|
|
|
|
function keydown() {
|
|
d3.event.preventDefault();
|
|
if (lastKeyDown !== -1) return;
|
|
lastKeyDown = d3.event.key;
|
|
|
|
if (lastKeyDown === "Control") {
|
|
vertices.call(
|
|
d3
|
|
.drag()
|
|
.on("start", (d) => {
|
|
if (!d3.event.active) force.alphaTarget(1).restart();
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
})
|
|
.on("drag", (d) => {
|
|
d.fx = d3.event.x;
|
|
d.fy = d3.event.y;
|
|
})
|
|
.on("end", (d) => {
|
|
if (!d3.event.active) force.alphaTarget(0);
|
|
d.fx = null;
|
|
d.fy = null;
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function keyup() {
|
|
lastKeyDown = -1;
|
|
if (d3.event.key === "Control")
|
|
vertices.on("mousedown.drag", null);
|
|
}
|
|
|
|
//updates the graph by updating links, nodes and binding them with DOM
|
|
//interface is defined through several events
|
|
function restart() {
|
|
edges = edges.data(links, (d) => "v" + d.source.id + "-v" + d.target.id);
|
|
edges.exit().remove();
|
|
|
|
var ed = edges
|
|
.enter()
|
|
.append("line")
|
|
.attr("class", "edge")
|
|
// .style("stroke", (d, _) => '#888')
|
|
.on("mousedown", () => d3.event.stopPropagation())
|
|
.on("contextmenu", removeEdge);
|
|
|
|
ed.append("title").text(d => "v" + d.source.id + "-v" + d.target.id);
|
|
|
|
edges = ed.merge(edges);
|
|
|
|
//vertices are known by id
|
|
vertices = vertices.data(nodes, (d) => d.id);
|
|
vertices.exit().remove();
|
|
|
|
var ve = vertices
|
|
.enter()
|
|
.append("circle")
|
|
.attr("r", rad)
|
|
.attr("class", "vertex")
|
|
.style("fill", (d, _) => colors[d.id % 10])
|
|
.on("mousedown", beginDragLine)
|
|
.on("mouseup", endDragLine)
|
|
.on("contextmenu", removeNode);
|
|
|
|
ve.append("title").text(d => "v" + d.id);
|
|
|
|
vertices = ve.merge(vertices);
|
|
|
|
|
|
// edges.sort((a, b) => a.source !== b.source ? a.source > b.source : a.target > b.target)
|
|
links.sort((a, b) => a.source !== b.source ? a.source > b.source : a.target > b.target)
|
|
|
|
force.nodes(nodes);
|
|
force.force("link").links(links);
|
|
force.alpha(0.8).restart();
|
|
}
|
|
|
|
//further interface
|
|
svg
|
|
.on("mousedown", addNode)
|
|
.on("mousemove", updateDragLine)
|
|
.on("mouseup", hideDragLine)
|
|
.on("contextmenu", () => d3.event.preventDefault())
|
|
.on("mouseleave", hideDragLine);
|
|
|
|
d3.select(window)
|
|
.on("keydown", keydown)
|
|
.on("keyup", keyup);
|
|
|
|
restart();
|
|
showGraphLatex();
|
|
|
|
//handling output area
|
|
function showGraphLatex() {
|
|
const v = "\\[ V = \\{ " +
|
|
nodes
|
|
.map(({ id }, i) => {
|
|
return (i == 0) ?
|
|
`${nodename(id)}` :
|
|
`, ${nodename(id)}`;
|
|
//add line break
|
|
// if ((i + 1) % 15 == 0) v += "\\\\";
|
|
})
|
|
.join('') +
|
|
" \\} \\]\n\n";
|
|
|
|
const e = "\\[ E = \\{" +
|
|
links.map(({ source, target }, i) => {
|
|
if (i == links.length - 1)
|
|
return `${nodename(source.id)}${nodename(target.id)}`;
|
|
else
|
|
return `${nodename(source.id)}${nodename(target.id)}, `;
|
|
// //add line break
|
|
// if ((i + 1) % 10 == 0) e += "\\\\";
|
|
}).join('') +
|
|
"\\} \\]";
|
|
|
|
document.getElementById("output").textContent = v + e;
|
|
//recall mathjax
|
|
document.getElementById("latex-output").textContent = v + e;
|
|
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
|
}
|
|
|
|
function generateSVG() {
|
|
const svgClone =
|
|
document
|
|
.getElementsByTagName("svg")[0]
|
|
.cloneNode({ deep: true });
|
|
|
|
svgClone
|
|
.firstChild
|
|
.childNodes
|
|
.foreach(e => e.style.stroke = "#000000");
|
|
|
|
var serializer = new XMLSerializer();
|
|
var source = serializer.serializeToString(svgClone);
|
|
|
|
if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
|
|
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
}
|
|
if (!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) {
|
|
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
|
}
|
|
|
|
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
|
|
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
|
|
}
|
|
|
|
function downloadSVG() {
|
|
var downloadLink = document.createElement("a");
|
|
downloadLink.href = generateSVG();
|
|
downloadLink.download = 'graph.svg';
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
document.body.removeChild(downloadLink);
|
|
}
|
|
|
|
d3
|
|
.select('#export-svg')
|
|
.on('click', downloadSVG) |