Skip to content

Instantly share code, notes, and snippets.

@u110
Forked from magjac/README.md
Last active May 16, 2021 11:57
Show Gist options
  • Save u110/b6fe1ce54acd80beb7b6e3bd9149fdad to your computer and use it in GitHub Desktop.
Save u110/b6fe1ce54acd80beb7b6e3bd9149fdad to your computer and use it in GitHub Desktop.
Graph Editor sample
<!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"]
症状をえらんでください -> 次の質問1 [label=あたま]
症状をえらんでください -> 次の質問2 [label=かお]
症状をえらんでください -> 次の質問3 [label=あし]
症状をえらんでください -> 症状をえらんでください [label=やりなおす]
}
`;
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment