People ask for a convenient pan/zoom/search viewer

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)

1 Like

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&#45;&gt;a1 -->
            <g id="edge1" class="edge">
                <title>a0-&gt;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&#45;&gt;a2 -->
            <g id="edge2" class="edge">
                <title>a1-&gt;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&#45;&gt;b3 -->
            <g id="edge9" class="edge">
                <title>a1-&gt;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&#45;&gt;a3 -->
            <g id="edge3" class="edge">
                <title>a2-&gt;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&#45;&gt;a0 -->
            <g id="edge11" class="edge">
                <title>a3-&gt;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&#45;&gt;end -->
            <g id="edge12" class="edge">
                <title>a3-&gt;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&#45;&gt;b1 -->
            <g id="edge4" class="edge">
                <title>b0-&gt;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&#45;&gt;b2 -->
            <g id="edge5" class="edge">
                <title>b1-&gt;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&#45;&gt;a3 -->
            <g id="edge10" class="edge">
                <title>b2-&gt;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&#45;&gt;b3 -->
            <g id="edge6" class="edge">
                <title>b2-&gt;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&#45;&gt;end -->
            <g id="edge13" class="edge">
                <title>b3-&gt;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&#45;&gt;a0 -->
            <g id="edge7" class="edge">
                <title>start-&gt;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&#45;&gt;b0 -->
            <g id="edge8" class="edge">
                <title>start-&gt;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:

  1. 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.
  2. 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.

1 Like

pprof also has a scrollable Graphviz UI: https://twitter.com/rakyll/status/899799394907594752 https://github.com/google/pprof

1 Like