d3-graphviz
Version:
Graphviz DOT rendering and animated transitions for D3
365 lines (322 loc) • 10.8 kB
HTML
<meta charset="utf-8">
<body>
<script src="../node_modules/d3/dist/d3.js"></script>
<script src="../node_modules/@hpcc-js/wasm/dist/graphviz.umd.js" type="text/javascript"></script>
<script src="../build/d3-graphviz.js"></script>
<div id="graph" style="text-align: center;"></div>
<script>var margin = 20; // to avoid scrollbars
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 prevNodeName = 'b';
var shapes = [
"box",
"polygon",
"ellipse",
"oval",
"circle",
"point",
"egg",
"triangle",
"plaintext",
"plain",
"diamond",
"trapezium",
"parallelogram",
"house",
"pentagon",
"hexagon",
"septagon",
"octagon",
"doublecircle",
"doubleoctagon",
"tripleoctagon",
"invtriangle",
"invtrapezium",
"invhouse",
"Mdiamond",
"Msquare",
"Mcircle",
"rect",
"rectangle",
"square",
"star",
"none",
"underline",
"cylinder",
"note",
"tab",
"folder",
"box3d",
"component",
"promoter",
"cds",
"terminator",
"utr",
"primersite",
"restrictionsite",
"fivepoverhang",
"threepoverhang",
"noverhang",
"assembly",
"signature",
"insulator",
"ribosite",
"rnastab",
"proteasesite",
"proteinstab",
"rpromoter",
"rarrow",
"larrow",
"lpromoter",
];
var dotSrc = `strict digraph {
node [style="filled"]
a [shape="box" 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);
});
render();
function attributer(datum, index, nodes) {
var selection = d3.select(this);
if (datum.tag == "svg") {
var width = window.innerWidth - margin;
var height = window.innerHeight - margin;
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() {
graphviz
.renderDot(dotSrc, startApp);
}
function startApp() {
var nodes = d3.selectAll(".node");
var edges = d3.selectAll(".edge");
// click outside of nodes
d3.select(document).on("click mousedown", function(event) {
event.preventDefault();
event.stopPropagation();
console.log('document click or mousedown');
unSelectEdge();
if (event.which == 2) {
var graph0 = d3.select("#graph").selectWithoutDataPropagation("svg").selectWithoutDataPropagation("g");
var width = 54;
var height = 36;
var [x0, y0] = d3.pointer(event, 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 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, nodeName, {shape: shape, text: nodeName, fillcolor: fillcolor, color: color})
.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');
render();
}
});
// keyup outside of nodes
d3.select(document).on("keyup", function(event) {
event.preventDefault();
console.log('document keyup', event);
if (event.keyCode == 27) {
graphviz.removeDrawnEdge();
unSelectEdge();
}
if (event.keyCode == 46) {
deleteSelectedEdge();
deleteSelectedNode();
graphviz.removeDrawnEdge();
render();
}
isDrawingEdge = false;
});
// move
d3.select(document).on("mousemove", function(event) {
event.preventDefault();
event.stopPropagation();
console.log('document mousemove');
var graph0 = d3.select("#graph").selectWithoutDataPropagation("svg").selectWithoutDataPropagation("g");
var [x0, y0] = d3.pointer(event, 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(event) {
event.preventDefault();
console.log('node click or mousedown');
if (!isDrawingEdge && event.which == 1) {
unSelectEdge();
selectNode(d3.select(this));
}
});
// double-click on nodes
nodes.on("dblclick", function(event) {
event.preventDefault();
event.stopPropagation();
console.log('node dblclick');
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');
render();
}
isDrawingEdge = false;
});
// right-click on nodes
nodes.on("contextmenu", function(event) {
event.preventDefault();
event.stopPropagation();
console.log('node contextmenu');
unSelectEdge();
unSelectNode();
graphviz.removeDrawnEdge();
startNode = d3.select(this);
var graph0 = d3.select("#graph").selectWithoutDataPropagation("svg").selectWithoutDataPropagation("g");
var [x0, y0] = d3.pointer(event, 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(event) {
event.preventDefault();
event.stopPropagation();
console.log('node click or mousedown');
unSelectNode();
selectEdge(d3.select(this));
});
// right-click outside of nodes
d3.select(document).on("contextmenu", function(event) {
event.preventDefault();
event.stopPropagation();
console.log('document contextmenu');
unSelectEdge();
});
}
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');
}
}
;
</script>