Add graph-maker site
This commit is contained in:
parent
dcf5cc3c45
commit
5a0cd8d1ca
33
tools/graph-maker/graph.css
Normal file
33
tools/graph-maker/graph.css
Normal file
@ -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;
|
||||
}
|
324
tools/graph-maker/graph.js
Normal file
324
tools/graph-maker/graph.js
Normal file
@ -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(/^<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)
|
48
tools/graph-maker/index.html
Normal file
48
tools/graph-maker/index.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Graph maker</title>
|
||||
|
||||
<!-- <script src="https://d3js.org/d3.v6.min.js" defer></script> -->
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" defer></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS_HTML-full"></script>
|
||||
<script src="./graph.js" defer></script>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="graph.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="app-area" class="d-flex-column justify-content-center">
|
||||
<div id="svg-wrap" class="bg-dark flex-column"></div>
|
||||
<div id="svg-buttons" class="text-center my-5 flex-column">
|
||||
<button class="btn btn-primary" id="clear-graph">Clear All</button>
|
||||
<button class="btn btn-primary" id="export-svg">Export SVG</button>
|
||||
<div class="flex-column">
|
||||
<input type="radio" class="btn-check" id="btn-check-outlined" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary" for="btn-check-outlined">Undirected</label><br>
|
||||
|
||||
<input type="radio" class="btn-check" id="btn-check-outlined" autocomplete="off" disabled>
|
||||
<label class="btn btn-outline-secondary" for="btn-check-outlined">Directed</label><br>
|
||||
|
||||
<input type="radio" class="btn-check" id="btn-check-outlined" autocomplete="off" disabled>
|
||||
<label class="btn btn-outline-secondary" for="btn-check-outlined">Tree</label><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5 mx-5">
|
||||
<textarea name="output" id="output" cols="30" rows="10" class="col-sm"></textarea>
|
||||
<div id="latex-output" class="col-sm"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user