MA0301/tools/graph-maker/graph.js

324 lines
8.4 KiB
JavaScript
Raw Normal View History

2021-05-17 19:04:42 +02:00
"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)