"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(/^