These things already exist, but with external tools. I wonder if it would be reasonable to enhance the SVG code generator possibly based on d3-graphviz in graphviz to would emit a standalone viewable graph?
Fwiw I pan/zoom/search SVGs by opening them in a browser and using scrollbars, ctrl/+, and ctrl/f. It’s admittedly not a smooth google-maps-style experience but it works today.
I think something that output some JS to make the experience smoother (into the svg or into an html output) would be a reasonable feature request.
As some other prior art, pprof has a zoomable pannable graphviz viewer, see some images of it here: https://www.goodwith.tech/blog/go-pprof (I can’t find a live example to share)
I am curious to see what this would look like, but I’m pessimistic that we could do anything better than what browsers do. Zooming SVGs in a modern browser is surprisingly pleasant these days.
Is this what you mean? Real-time multiuser editing is supported. As we speak, I’m working on dot import/export.
Not shown is the “Prettify” function, which is graphviz layout. And the video-in-a-node feature is no longer supported.
Here is my attempt at a minimal html page:
<!DOCTYPE html>
<html>
<head>
<title>SVG Zoom</title>
<script src="https://cdn.jsdelivr.net/npm/d3-selection@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-dispatch@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-drag@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-zoom@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-transition@3"></script>
<script>
window.onload = function () {
var svg = d3.select("svg");
var g = svg.select("g");
var [x, y, width, height] = svg.attr("viewBox").split(" ");
var zoom = d3.zoom();
svg.call(zoom
.extent([[0, 0], [width, height]])
.scaleExtent([0.1, 8])
.on("zoom", ({ transform }) => g.attr("transform", transform))
);
svg.call(zoom.translateTo, width / 2, -height / 2);
};
</script>
</head>
<body>
<svg width="224pt" height="409pt" viewBox="0.00 0.00 224.00 409.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 405.01)">
<title>G</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-405.01 220,-405.01 220,4 -4,4"></polygon>
<g id="clust1" class="cluster">
<title>cluster_0</title>
<polygon fill="lightgrey" stroke="lightgrey" points="8,-64.21 8,-357.01 98,-357.01 98,-64.21 8,-64.21"></polygon>
<text text-anchor="middle" x="53" y="-340.41" font-family="Times,serif" font-size="14.00">process #1</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_1</title>
<polygon fill="none" stroke="blue" points="133,-64.21 133,-357.01 208,-357.01 208,-64.21 133,-64.21"></polygon>
<text text-anchor="middle" x="170.5" y="-340.41" font-family="Times,serif" font-size="14.00">process #2</text>
</g>
<!-- a0 -->
<g id="node1" class="node">
<title>a0</title>
<ellipse fill="white" stroke="white" cx="63" cy="-306.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="63" y="-302.01" font-family="Times,serif" font-size="14.00">a0</text>
</g>
<!-- a1 -->
<g id="node2" class="node">
<title>a1</title>
<ellipse fill="white" stroke="white" cx="63" cy="-234.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="63" y="-230.01" font-family="Times,serif" font-size="14.00">a1</text>
</g>
<!-- a0->a1 -->
<g id="edge1" class="edge">
<title>a0->a1</title>
<path fill="none" stroke="black" d="M63,-287.91C63,-280.2 63,-270.93 63,-262.33"></path>
<polygon fill="black" stroke="black" points="66.5,-262.32 63,-252.32 59.5,-262.32 66.5,-262.32"></polygon>
</g>
<!-- a2 -->
<g id="node3" class="node">
<title>a2</title>
<ellipse fill="white" stroke="white" cx="63" cy="-162.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="63" y="-158.01" font-family="Times,serif" font-size="14.00">a2</text>
</g>
<!-- a1->a2 -->
<g id="edge2" class="edge">
<title>a1->a2</title>
<path fill="none" stroke="black" d="M63,-215.91C63,-208.2 63,-198.93 63,-190.33"></path>
<polygon fill="black" stroke="black" points="66.5,-190.32 63,-180.32 59.5,-190.32 66.5,-190.32"></polygon>
</g>
<!-- b3 -->
<g id="node8" class="node">
<title>b3</title>
<ellipse fill="lightgrey" stroke="black" cx="168" cy="-90.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="168" y="-86.01" font-family="Times,serif" font-size="14.00">b3</text>
</g>
<!-- a1->b3 -->
<g id="edge9" class="edge">
<title>a1->b3</title>
<path fill="none" stroke="black" d="M74.44,-217.75C92.74,-192.99 128.75,-144.3 150.37,-115.06"></path>
<polygon fill="black" stroke="black" points="153.45,-116.78 156.58,-106.66 147.82,-112.62 153.45,-116.78"></polygon>
</g>
<!-- a3 -->
<g id="node4" class="node">
<title>a3</title>
<ellipse fill="white" stroke="white" cx="63" cy="-90.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="63" y="-86.01" font-family="Times,serif" font-size="14.00">a3</text>
</g>
<!-- a2->a3 -->
<g id="edge3" class="edge">
<title>a2->a3</title>
<path fill="none" stroke="black" d="M63,-143.91C63,-136.2 63,-126.93 63,-118.33"></path>
<polygon fill="black" stroke="black" points="66.5,-118.32 63,-108.32 59.5,-118.32 66.5,-118.32"></polygon>
</g>
<!-- a3->a0 -->
<g id="edge11" class="edge">
<title>a3->a0</title>
<path fill="none" stroke="black" d="M49.25,-106.15C41.04,-116.11 31.38,-129.97 27,-144.21 12.89,-190.09 12.89,-206.33 27,-252.21 30.29,-262.9 36.54,-273.36 42.93,-282.13"></path>
<polygon fill="black" stroke="black" points="40.35,-284.53 49.25,-290.28 45.88,-280.24 40.35,-284.53"></polygon>
</g>
<!-- end -->
<g id="node10" class="node">
<title>end</title>
<polygon fill="none" stroke="black" points="133.22,-36.32 96.78,-36.32 96.78,0.11 133.22,0.11 133.22,-36.32"></polygon>
<polyline fill="none" stroke="black" points="108.78,-36.32 96.78,-24.32 "></polyline>
<polyline fill="none" stroke="black" points="96.78,-11.89 108.78,0.11 "></polyline>
<polyline fill="none" stroke="black" points="121.22,0.11 133.22,-11.89 "></polyline>
<polyline fill="none" stroke="black" points="133.22,-24.32 121.22,-36.32 "></polyline>
<text text-anchor="middle" x="115" y="-13.91" font-family="Times,serif" font-size="14.00">end</text>
</g>
<!-- a3->end -->
<g id="edge12" class="edge">
<title>a3->end</title>
<path fill="none" stroke="black" d="M74.54,-73.66C80.84,-65.17 88.79,-54.45 95.97,-44.76"></path>
<polygon fill="black" stroke="black" points="98.88,-46.71 102.03,-36.59 93.26,-42.54 98.88,-46.71"></polygon>
</g>
<!-- b0 -->
<g id="node5" class="node">
<title>b0</title>
<ellipse fill="lightgrey" stroke="black" cx="168" cy="-306.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="168" y="-302.01" font-family="Times,serif" font-size="14.00">b0</text>
</g>
<!-- b1 -->
<g id="node6" class="node">
<title>b1</title>
<ellipse fill="lightgrey" stroke="black" cx="170" cy="-234.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="170" y="-230.01" font-family="Times,serif" font-size="14.00">b1</text>
</g>
<!-- b0->b1 -->
<g id="edge4" class="edge">
<title>b0->b1</title>
<path fill="none" stroke="black" d="M168.49,-287.91C168.71,-280.2 168.98,-270.93 169.23,-262.33"></path>
<polygon fill="black" stroke="black" points="172.72,-262.41 169.51,-252.32 165.73,-262.21 172.72,-262.41"></polygon>
</g>
<!-- b2 -->
<g id="node7" class="node">
<title>b2</title>
<ellipse fill="lightgrey" stroke="black" cx="173" cy="-162.21" rx="27" ry="18"></ellipse>
<text text-anchor="middle" x="173" y="-158.01" font-family="Times,serif" font-size="14.00">b2</text>
</g>
<!-- b1->b2 -->
<g id="edge5" class="edge">
<title>b1->b2</title>
<path fill="none" stroke="black" d="M170.74,-215.91C171.07,-208.2 171.47,-198.93 171.84,-190.33"></path>
<polygon fill="black" stroke="black" points="175.34,-190.46 172.27,-180.32 168.34,-190.16 175.34,-190.46"></polygon>
</g>
<!-- b2->a3 -->
<g id="edge10" class="edge">
<title>b2->a3</title>
<path fill="none" stroke="black" d="M153.84,-149.02C136.33,-137.88 110.24,-121.28 90.51,-108.72"></path>
<polygon fill="black" stroke="black" points="92.26,-105.68 81.94,-103.27 88.5,-111.59 92.26,-105.68"></polygon>
</g>
<!-- b2->b3 -->
<g id="edge6" class="edge">
<title>b2->b3</title>
<path fill="none" stroke="black" d="M171.76,-143.91C171.21,-136.2 170.55,-126.93 169.94,-118.33"></path>
<polygon fill="black" stroke="black" points="173.43,-118.04 169.22,-108.32 166.44,-118.54 173.43,-118.04"></polygon>
</g>
<!-- b3->end -->
<g id="edge13" class="edge">
<title>b3->end</title>
<path fill="none" stroke="black" d="M156.24,-73.66C149.82,-65.17 141.72,-54.45 134.39,-44.76"></path>
<polygon fill="black" stroke="black" points="137.04,-42.46 128.22,-36.59 131.46,-46.68 137.04,-42.46"></polygon>
</g>
<!-- start -->
<g id="node9" class="node">
<title>start</title>
<polygon fill="none" stroke="black" points="115,-401.01 75.76,-383.01 115,-365.01 154.24,-383.01 115,-401.01"></polygon>
<polyline fill="none" stroke="black" points="86.67,-388.02 86.67,-378.01 "></polyline>
<polyline fill="none" stroke="black" points="104.09,-370.02 125.91,-370.02 "></polyline>
<polyline fill="none" stroke="black" points="143.33,-378.01 143.33,-388.02 "></polyline>
<polyline fill="none" stroke="black" points="125.91,-396.01 104.09,-396.01 "></polyline>
<text text-anchor="middle" x="115" y="-378.81" font-family="Times,serif" font-size="14.00">start</text>
</g>
<!-- start->a0 -->
<g id="edge7" class="edge">
<title>start->a0</title>
<path fill="none" stroke="black" d="M105.94,-368.99C98.71,-358.58 88.37,-343.71 79.66,-331.17"></path>
<polygon fill="black" stroke="black" points="82.52,-329.16 73.94,-322.95 76.77,-333.16 82.52,-329.16"></polygon>
</g>
<!-- start->b0 -->
<g id="edge8" class="edge">
<title>start->b0</title>
<path fill="none" stroke="black" d="M124.23,-368.99C131.65,-358.51 142.28,-343.52 151.2,-330.93"></path>
<polygon fill="black" stroke="black" points="154.12,-332.86 157.04,-322.68 148.41,-328.82 154.12,-332.86"></polygon>
</g>
</g>
</svg>
</body>
</html>
Of course if you are going down that road, then you could go one step further:
<!DOCTYPE html>
<html>
<head>
<title>SVG Zoom</title>
<script src="https://cdn.jsdelivr.net/npm/d3-selection@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-dispatch@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-drag@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-zoom@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-transition@3"></script>
<script src="https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.min.js"></script>
<script>
var hpccWasm = window["@hpcc-js/wasm"];
hpccWasm.graphviz.dot(`
digraph G {
subgraph cluster_0 {
style=filled;
color=lightgrey;
node [style=filled,color=white];
a0 -> a1 -> a2 -> a3;
label = "process #${1}";
}
subgraph cluster_1 {
node [style=filled];
b0 -> b1 -> b2 -> b3;
label = "process #${2}";
color=blue
}
start -> a0;
start -> b0;
a1 -> b3;
b2 -> a3;
a3 -> a0;
a3 -> end;
b3 -> end;
start [shape=Mdiamond];
end [shape=Msquare];
}`
).then(svg => {
d3.select("body").html(svg);
var svg = d3.select("svg");
var g = svg.select("g");
var [x, y, width, height] = svg.attr("viewBox").split(" ");
var zoom = d3.zoom();
svg.call(zoom
.extent([[0, 0], [width, height]])
.scaleExtent([0.1, 8])
.on("zoom", ({ transform }) => g.attr("transform", transform))
);
svg.call(zoom.translateTo, width / 2, -height / 2);
});
</script>
</head>
<body>
</body>
</html>
I think that would be an overkill for two reasons:
- d3-graphviz contains Graphviz through @hpcc-js/wasm and renders the SVG. What we need here is just to pan and zoom an existing SVG outputted by Graphviz.
- d3-graphviz’s main purpose is to do animated transitions between graphs. If you just need pan and zoom, it’s better to use something more lightweight such as d3-zoom as in Gordon’s proposals above.
I agree. At least with Firefox you can do all you need with various combinations of Ctrl, Shift and mouse wheel, although I’d prefer to do panning by dragging as d3-zoom (and d3-graphviz) do.
FYI I learned from this post that PyPy have their own Graphviz viewer for zooming, panning, etc.
pprof also has a scrollable Graphviz UI: https://twitter.com/rakyll/status/899799394907594752 https://github.com/google/pprof