|
<!DOCTYPE html> |
|
<html style="height: calc(100% - 16px);"> |
|
<meta charset="utf-8"> |
|
<style type="text/css" media="screen"> |
|
#editor { |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
bottom: 0; |
|
left: 0; |
|
border: 1px solid lightgray; |
|
} |
|
</style> |
|
<body style="height: 100%"> |
|
<script src="https://d3js.org/d3.v5.js"></script> |
|
<script src="https://unpkg.com/viz.js@1.8.0/viz.js" type="javascript/worker"></script> |
|
<script src="https://unpkg.com/d3-graphviz@1.5.1/build/d3-graphviz.min.js"></script> |
|
<script src="https://unpkg.com/ace-builds@1.3.3/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script> |
|
<div id="editor" style="width: 50%; float: left"></div> |
|
<div style="width: 50%; height: 0%"> |
|
<div id="help-text" style="position: absolute; top: 3px; display: none; font: 16px/1.5em 'Helvetica Neue', Helvetica, sans-serif;"> |
|
<b>Draw an edge</b>:<br> |
|
Right-click on the node to draw the edge from<br> |
|
Double-click on the node to draw the edge to<br> |
|
Press the ESC key to abort<br> |
|
|
|
<br> |
|
<b>Draw a node</b>:<br> |
|
Middle-click the background<br> |
|
<br> |
|
<b>Delete a node or an edge</b>:<br> |
|
Select a node or an edge by clicking it<br> |
|
Press the DEL key |
|
<br> |
|
</div> |
|
</div> |
|
<div id="graph" style="width: 50%; height: 100%; margin-left: 50%; text-align: center"> |
|
<div id="error-message" style="position: absolute; top: 1%; left: 51%; color: red"></div> |
|
</div> |
|
<div id="graph2" style="width: 50%; height: 0%; margin-left: 50%"> |
|
<div id="help" title="Show help" style="position: absolute; top: 3px; left: calc(100% - 28px)"> |
|
<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> |
|
<path d="M11 3A8 8 0 0 0 3 11 8 8 0 0 0 11 19 8 8 0 0 0 19 11 8 8 0 0 0 11 3M10.994 6.5C11.758 6.5 12.379 6.719 12.857 7.158 13.336 7.589 13.576 8.142 13.576 8.816 13.576 9.109 13.495 9.406 13.336 9.707 13.176 10.01 13.03 10.223 12.908 10.354 12.791 10.475 12.623 10.635 12.404 10.83L12.342 10.891C11.83 11.338 11.572 11.785 11.572 12.232V12.719H10.389V12.146C10.389 11.781 10.469 11.467 10.629 11.207 10.788 10.939 11.07 10.625 11.473 10.268 11.699 10.06 11.859 9.914 11.951 9.816 12.05 9.711 12.148 9.569 12.24 9.391 12.341 9.204 12.393 9.01 12.393 8.816 12.393 8.442 12.266 8.142 12.01 7.914 11.77 7.686 11.431 7.572 10.994 7.572 10.272 7.572 9.776 7.964 9.508 8.744L8.424 8.305C8.6 7.841 8.904 7.426 9.332 7.06 9.769 6.687 10.322 6.5 10.994 6.5M10.98 13.842C11.224 13.842 11.426 13.923 11.586 14.09 11.754 14.249 11.838 14.442 11.838 14.67 11.838 14.898 11.754 15.09 11.586 15.256 11.426 15.418 11.224 15.5 10.98 15.5 10.737 15.5 10.531 15.418 10.363 15.256 10.204 15.09 10.125 14.898 10.125 14.67 10.125 14.442 10.204 14.249 10.363 14.09 10.531 13.923 10.737 13.842 10.98 13.842" transform="translate(0-.002)" fill="#4d4d4d" fill-rule="evenodd"/> |
|
</svg> |
|
</div> |
|
</div> |
|
<script> |
|
|
|
'use strict'; |
|
|
|
var isDrawingEdge = false; |
|
var isDrawingNode = false; |
|
var startNode; |
|
var selectedEdge = d3.select(null); |
|
var selectedEdgeFill; |
|
var selectedEdgeStroke; |
|
var selectedNode = d3.select(null); |
|
var selectedNodeStroke; |
|
var selectedNodeFill; |
|
|
|
var nodeIndex; |
|
var edgeIndex; |
|
|
|
var editor = ace.edit("editor"); |
|
|
|
var pendingUpdate = false |
|
var rendering = false; |
|
|
|
var showHelp = false; |
|
|
|
var dotSrc = `strict digraph { |
|
node [style="filled"] |
|
a [shape="ellipse" fillcolor="` + d3.schemeCategory10[0] + `"] |
|
b [shape="polygon" fillcolor="` + d3.schemeCategory10[1] + `"] |
|
a -> b [fillcolor="` + d3.schemePaired[0] + `" color="` + d3.schemePaired[1] + `"] |
|
}`; |
|
|
|
var graphviz = d3.select("#graph").graphviz() |
|
.attributer(attributer) |
|
.transition(function() { |
|
return d3.transition().duration(1000); |
|
}); |
|
|
|
editor.getSession().on('change', render); |
|
editor.setValue(dotSrc); |
|
|
|
function attributer(datum, index, nodes) { |
|
var selection = d3.select(this); |
|
if (datum.tag == "svg") { |
|
var width = d3.select('#graph').node().clientWidth; |
|
var height = d3.select('#graph').node().clientHeight; |
|
var x = "10"; |
|
var y = "10"; |
|
var unit = 'px'; |
|
selection |
|
.attr("width", width + unit) |
|
.attr("height", height + unit); |
|
datum.attributes.width = width + unit; |
|
datum.attributes.height = height + unit; |
|
} |
|
} |
|
|
|
function render() { |
|
dotSrc = editor.getValue(); |
|
if (!dotSrc) { |
|
return; |
|
} |
|
if (rendering) { |
|
pendingUpdate = true; |
|
return; |
|
} |
|
|
|
rendering = true; |
|
d3.select("#error-message").text(""); |
|
graphviz |
|
.onerror(handleError) |
|
.renderDot(dotSrc, startApp); |
|
} |
|
|
|
function handleError(errorMessage) { |
|
d3.select("#error-message").text(errorMessage); |
|
rendering = false; |
|
if (pendingUpdate) { |
|
pendingUpdate = false; |
|
render(); |
|
} |
|
} |
|
|
|
function startApp() { |
|
var nodes = d3.selectAll(".node"); |
|
var edges = d3.selectAll(".edge"); |
|
|
|
// click outside of nodes |
|
d3.select(document).on("click", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
unSelectEdge(); |
|
unSelectNode(); |
|
if (event.which == 2) { |
|
var graph0 = d3.select("svg").selectWithoutDataPropagation("g"); |
|
var width = 54; |
|
var height = 36; |
|
var [x0, y0] = d3.mouse(graph0.node()); |
|
var x1 = x0 - width / 2; |
|
var x2 = x0 + width / 2; |
|
var y1 = y0 - height / 2; |
|
var y2 = y0 + height / 2; |
|
if (nodeIndex == null) { |
|
nodeIndex = d3.selectAll('.node').size(); |
|
} else { |
|
nodeIndex += 1; |
|
} |
|
var shapes = ['ellipse', 'polygon']; |
|
var shape = shapes[nodeIndex % shapes.length]; |
|
var numLetters = 'z'.charCodeAt(0) - 'a'.charCodeAt(0) + 1; |
|
var letter = String.fromCharCode('a'.charCodeAt(0) + (nodeIndex % numLetters)); |
|
var digit = Math.floor(nodeIndex / numLetters) || ''; |
|
var nodeName = letter + digit; |
|
var fillcolor = d3.schemeCategory10[nodeIndex % 10]; |
|
var color = 'black'; |
|
graphviz |
|
.drawNode(x1, y1, width, height, shape, nodeName, {text: nodeName, fillcolor: fillcolor, color: color, id: 'drawn-node', labeljust: 'l'}) |
|
.insertDrawnNode(nodeName); |
|
var dotSrcLines = dotSrc.split('\n'); |
|
var newNodeString = ' ' + nodeName + ' [shape="' + shape + '" color="' + color + '"; fillcolor="' + fillcolor + '"; penwidth="1"]'; |
|
dotSrcLines.splice(-1, 0, newNodeString); |
|
dotSrc = dotSrcLines.join('\n'); |
|
editor.setValue(dotSrc); |
|
} |
|
}); |
|
|
|
// keyup outside of nodes |
|
d3.select(document).on("keyup", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
if (event.keyCode == 27) { |
|
graphviz.removeDrawnEdge(); |
|
unSelectEdge(); |
|
unSelectNode(); |
|
} |
|
if (event.keyCode == 46) { |
|
deleteSelectedEdge(); |
|
deleteSelectedNode(); |
|
graphviz.removeDrawnEdge(); |
|
editor.setValue(dotSrc); |
|
} |
|
isDrawingEdge = false; |
|
}); |
|
|
|
// move |
|
d3.select(document).on("mousemove", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
var graph0 = d3.select("svg").selectWithoutDataPropagation("g"); |
|
var [x0, y0] = d3.mouse(graph0.node()); |
|
var shortening = 2; // avoid mouse pointing on edge |
|
if (isDrawingEdge) { |
|
graphviz |
|
.moveDrawnEdgeEndPoint(x0, y0, {shortening: shortening}) |
|
} |
|
}); |
|
|
|
// click and mousedown on nodes |
|
nodes.on("click mousedown", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
if (!isDrawingEdge && event.which == 1) { |
|
unSelectEdge(); |
|
selectNode(d3.select(this)); |
|
} |
|
}); |
|
|
|
// double-click on nodes |
|
nodes.on("dblclick", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
unSelectEdge(); |
|
if (isDrawingEdge) { |
|
var endNode = d3.select(this); |
|
var startNodeName = startNode.selectWithoutDataPropagation("title").text(); |
|
var endNodeName = endNode.selectWithoutDataPropagation("title").text(); |
|
graphviz |
|
.insertDrawnEdge(startNodeName + '->' + endNodeName); |
|
var fillcolor = d3.schemePaired[(edgeIndex * 2) % 12]; |
|
var color = d3.schemePaired[(edgeIndex * 2 + 1) % 12]; |
|
var dotSrcLines = dotSrc.split('\n'); |
|
var newEdgeString = ' ' + startNodeName + ' -> ' + endNodeName + ' [color="' + color + '"; fillcolor="' + fillcolor + '"; penwidth="1"]'; |
|
dotSrcLines.splice(-1, 0, newEdgeString); |
|
dotSrc = dotSrcLines.join('\n'); |
|
editor.setValue(dotSrc); |
|
} |
|
isDrawingEdge = false; |
|
}); |
|
|
|
// right-click on nodes |
|
nodes.on("contextmenu", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
unSelectEdge(); |
|
unSelectNode(); |
|
graphviz.removeDrawnEdge(); |
|
startNode = d3.select(this); |
|
var graph0 = d3.select("svg").selectWithoutDataPropagation("g"); |
|
var [x0, y0] = d3.mouse(graph0.node()); |
|
if (edgeIndex == null) { |
|
edgeIndex = d3.selectAll('.edge').size(); |
|
} else { |
|
edgeIndex += 1; |
|
} |
|
var fillcolor = d3.schemePaired[(edgeIndex * 2) % 12]; |
|
var color = d3.schemePaired[(edgeIndex * 2 + 1) % 12]; |
|
|
|
graphviz |
|
.drawEdge(x0, y0, x0, y0, {fillcolor: fillcolor, color: color}); |
|
isDrawingEdge = true; |
|
}); |
|
|
|
// click and mousedown on edges |
|
edges.on("click mousedown", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
unSelectNode(); |
|
selectEdge(d3.select(this)); |
|
}); |
|
|
|
// right-click outside of nodes |
|
d3.select(document).on("contextmenu", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
unSelectEdge(); |
|
unSelectNode(); |
|
}); |
|
|
|
// click on help |
|
d3.select("#help").on("click", function() { |
|
var event = d3.event; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
toggleShowHelp(); |
|
}); |
|
|
|
rendering = false; |
|
if (pendingUpdate) { |
|
pendingUpdate = false; |
|
render(); |
|
} |
|
} |
|
|
|
function selectEdge(edge) { |
|
unSelectEdge(); |
|
selectedEdge = edge; |
|
selectedEdgeFill = selectedEdge.selectAll('polygon').attr("fill"); |
|
selectedEdgeStroke = selectedEdge.selectAll('polygon').attr("stroke"); |
|
selectedEdge.selectAll('path, polygon').attr("stroke", "red"); |
|
selectedEdge.selectAll('polygon').attr("fill", "red"); |
|
} |
|
|
|
function unSelectEdge() { |
|
selectedEdge.selectAll('path, polygon').attr("stroke", selectedEdgeStroke); |
|
selectedEdge.selectAll('polygon').attr("fill", selectedEdgeFill); |
|
selectedEdge = d3.select(null); |
|
} |
|
|
|
function deleteSelectedEdge() { |
|
selectedEdge.style("display", "none"); |
|
if (selectedEdge.size() != 0) { |
|
var edgeName = selectedEdge.selectWithoutDataPropagation("title").text(); |
|
edgeName = edgeName.replace('->', ' -> '); |
|
var dotSrcLines = dotSrc.split('\n'); |
|
while (true) { |
|
var i = dotSrcLines.findIndex(function (element, index) { |
|
return element.indexOf(edgeName) >= 0; |
|
}); |
|
if (i < 0) |
|
break; |
|
dotSrcLines.splice(i, 1); |
|
} |
|
dotSrc = dotSrcLines.join('\n'); |
|
} |
|
} |
|
|
|
function selectNode(node) { |
|
unSelectNode(); |
|
selectedNode = node; |
|
selectedNodeFill = selectedNode.selectAll('polygon, ellipse').attr("fill"); |
|
selectedNodeStroke = selectedNode.selectAll('polygon, ellipse').attr("stroke"); |
|
selectedNode.selectAll('polygon, ellipse').attr("stroke", "red"); |
|
selectedNode.selectAll('polygon, ellipse').attr("fill", "red"); |
|
} |
|
|
|
function unSelectNode() { |
|
selectedNode.selectAll('polygon, ellipse').attr("stroke", selectedNodeStroke); |
|
selectedNode.selectAll('polygon, ellipse').attr("fill", selectedNodeFill); |
|
selectedNode = d3.select(null); |
|
} |
|
|
|
function deleteSelectedNode() { |
|
selectedNode.style("display", "none"); |
|
if (selectedNode.size() != 0) { |
|
var nodeName = selectedNode.selectWithoutDataPropagation("title").text(); |
|
var dotSrcLines = dotSrc.split('\n'); |
|
while (true) { |
|
var i = dotSrcLines.findIndex(function (element, index) { |
|
var trimmedElement = element.trim(); |
|
if (trimmedElement == nodeName) { |
|
return true; |
|
} |
|
if (trimmedElement.indexOf(nodeName + ' ') == 0) { |
|
return true; |
|
} |
|
if (trimmedElement.indexOf(' ' + nodeName + ' ') >= 0) { |
|
return true; |
|
} |
|
return false; |
|
}); |
|
if (i < 0) |
|
break; |
|
dotSrcLines.splice(i, 1); |
|
} |
|
dotSrc = dotSrcLines.join('\n'); |
|
} |
|
} |
|
|
|
function toggleShowHelp() { |
|
showHelp = !showHelp; |
|
if (showHelp) { |
|
d3.select("#editor").style("display", "none"); |
|
d3.select("#help-text").style("display", "block"); |
|
} else { |
|
d3.select("#editor").style("display", "block"); |
|
d3.select("#help-text").style("display", "none"); |
|
} |
|
} |
|
|
|
d3.select(window).on("resize", resizeSVG); |
|
|
|
function resizeSVG() { |
|
var width = d3.select('#graph').node().clientWidth; |
|
var height = d3.select('#graph').node().clientHeight; |
|
d3.select("#graph").selectWithoutDataPropagation("svg") |
|
.transition() |
|
.duration(700) |
|
.attr("width", width) |
|
.attr("height", height); |
|
}; |
|
|
|
</script> |
|
</html> |