From 5a0cd8d1ca3804a701c7d18d2b1c2103d6561cc5 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Mon, 17 May 2021 19:04:42 +0200 Subject: [PATCH] Add graph-maker site --- tools/graph-maker/graph.css | 33 ++++ tools/graph-maker/graph.js | 324 +++++++++++++++++++++++++++++++++++ tools/graph-maker/index.html | 48 ++++++ 3 files changed, 405 insertions(+) create mode 100644 tools/graph-maker/graph.css create mode 100644 tools/graph-maker/graph.js create mode 100644 tools/graph-maker/index.html diff --git a/tools/graph-maker/graph.css b/tools/graph-maker/graph.css new file mode 100644 index 0000000..02e6c36 --- /dev/null +++ b/tools/graph-maker/graph.css @@ -0,0 +1,33 @@ +svg { + cursor: crosshair; + display: block; + margin: auto; + background-color: #FFFFFF; +} + +.edge { + stroke: #888; + stroke-width: 2px; + stroke-linecap: round; + stroke-linejoin: round; + cursor: default; +} + +.edge:hover, +.dragLine { + stroke: #333; + stroke-width: 3px; +} + +.vertex { + cursor: pointer; +} + +.vertex:hover { + stroke: #333; + opacity: 0.8; +} + +.dragLine.hidden { + stroke-width: 0; +} \ No newline at end of file diff --git a/tools/graph-maker/graph.js b/tools/graph-maker/graph.js new file mode 100644 index 0000000..e99fe1c --- /dev/null +++ b/tools/graph-maker/graph.js @@ -0,0 +1,324 @@ +"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(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) { + source = source.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) { + source = source.replace(/^ + + + + + + + Graph maker + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+ +
+
+ + + + \ No newline at end of file