0

Suppose you've drawn a diagram in some GUI illustration app. You export the diagram to SVG and insert it as a left-aligned block image in an HTML page.

You deliberately scale the diagram so that the size of the text in the diagram matches the 16px reflowable body text in the surrounding content of the HTML page.

In a maximized browser window on a 23-inch monitor, the reflowable body text column is much wider than the diagram, leaving a wide area of white space to the right of the diagram:

Diagram with white space to right

It would look better if the diagram was responsive; in this case, where there is an abundance of space, if the diagram filled more of the available width. (Just center-align it and be done with it?!)

However, you don't want to just scale up the entire SVG to fit the available width, because the text in the diagram would also scale up. The text in the diagram would start to shout. You want the text in the diagram to remain at the same 16px size.

And you don't want to just scale up everything but the text. Text labels could be "lost", appear disproportionately small, inside relatively massive shapes.

Ideally, you want a method that distributes—lays out—the shapes in a diagram to fit the available space.

What app or tool can you use to draw diagrams that dynamically resize to fit the width of their HTML container, but with fixed-sized text? Responsive diagrams that don't raise their voice above the surrounding body text when they scale up.

My initial use case is diagrams that are, in formal terms, graphs, including directed graphs: nodes connected by edges.

Example renderings of the same diagram at different widths

Note that, in both cases, the text inside the diagram is the same size as text outside the diagram.

Wide example

Narrow example

With this rudimentary example diagram, which has only a few nodes and edges, the differences in layout are minimal: in the wider example, the nodes are slightly further apart and the edges are correspondingly longer. With a more complex diagram, and perhaps also with a greater relative difference in width, I can imagine a "layout engine" making more significantly different decisions about how to arrange the nodes and edges to fit the container.

I acknowledge that, especially for such a simple diagram, a layout that fits the size of the container isn't necessarily optimal for readability or looks. In some cases, external white space might be preferable to a "spidery" diagram with extremely long edges, and nodes that shrinkwrap to fit their labels might not be ideal.

Requirements

To achieve what I want, I think I need:

  1. An abstract definition of a diagram that is independent of the size of the HTML container
  2. A method for redrawing the diagram to fit the size of the HTML container, based on that description, when the HTML container resizes

My starting point: DOT and d3-graphviz

The DOT language is:

[An] abstract grammar for defining Graphviz nodes, edges, graphs, subgraphs, and clusters

Graphviz renders DOT into various output formats, such as SVG.

d3-graphviz is a D3 plugin based on Graphviz.

I already have experience with d3-graphviz.

Here, for example, is DOT source of the diagram shown previously in the example renderings (note: <width> and <height> are placeholders for actual values):

digraph {

    ratio="fill"
    size="<width>,<height>"
    margin=0
    pad=0
    bgcolor="#f0f0f0"
    
    node [shape=box,style="rounded,filled",color="#ccccff", fontname="Arial", fontsize="12pt"]
    edge [color="#6666ff"]

    a -> d
    b -> d
    c -> d

    a [label="Alpha"];
    b [label="Bravo"];
    c [label="Charlie"];
    d [label="Width: <width>"];
}

DOT and d3-graphviz are my starting point; they're what I'm familiar with. I'm very open to answers that involve other abstract grammars, other tools.

Graham Hannington
  • 1,749
  • 16
  • 18
  • calculate the overall transform for the text e.g. via GetCTM or GetScreenCTM. Invert it and apply the inverted matrix to the text element. – Robert Longson Aug 21 '23 at 15:36
  • Thanks for the suggestion. Following comments by @ccprog on my self-answer, I've updated my question to include more detail. In particular, "\[I\] don't want to just scale up everything but the text". I'm sorry I didn't make that clearer initially. I suspect that what I want (see the example renderings in my updated question) requires a precursor to SVG: an abstract grammar, such as DOT. – Graham Hannington Aug 22 '23 at 04:04

1 Answers1

0

Use d3-graphviz.

Dynamically inject the width of the HTML container into the DOT source.

When the container resizes, re-render the DOT to match the container width.

Rudimentary examples

Tip: Run the following snippets "full page", and then resize the browser window.

Example 1

<!DOCTYPE html>
<html>
<head>
<style>
body {
    margin: 1em;
    font-family: Arial;
    font-size: 16px;
}
#dot {
    display: none;
}
#graph-container {
    width: 100%;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js"></script>
<script src="https://unpkg.com/d3-graphviz@3.0.5/build/d3-graphviz.js"></script>
<div id="dot">
digraph {

    ratio="fill"
    size="<span id="graph-width">#</span>,<span id="graph-height">#</span>"
    margin=0
    pad=0
    bgcolor="#f0f0f0"
    
    node [shape=box,style="rounded,filled",color="#ccccff", fontname="Arial", fontsize="12pt"]
    edge [color="#6666ff"]

    a -> d
    b -> d
    c -> d

    a [label="Alpha"];
    b [label="Bravo"];
    c [label="Charlie"];
    d [label="Width: <span id="width">#</span>px"];
}
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<div id="graph-container" style="height: 20em;"></div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<script>

const graphContainer = d3.select("#graph-container");
const width = graphContainer.node().clientWidth;
const height = graphContainer.node().clientHeight;
const dpi = 96

// Redraw
function redrawGraph(container, { width, height }) {
    document.getElementById("width").innerText = width
    document.getElementById("graph-width").innerText = width / dpi
    document.getElementById("graph-height").innerText = height / dpi
    graphContainer.graphviz()
      .renderDot(document.getElementById("dot").textContent);
}

// Detect container resize
const resizeObserver = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    redrawGraph(entry.target, entry.contentRect);
  }
})

const container = document.querySelector('#graph-container');
resizeObserver.observe(container)

// Initial draw
document.getElementById("width").innerText = container.clientWidth
document.getElementById("graph-width").innerText = container.clientWidth / dpi
document.getElementById("graph-height").innerText = container.clientHeight / dpi
graphContainer.graphviz()
  .renderDot(document.getElementById("dot").textContent);

</script>
</body>
</html>

Example 2

<!DOCTYPE html>
<html>
<head>
<style>
body {
    margin: 1em;
    font-family: Arial;
    font-size: 16px;
}
#dot {
    display: none;
}
#graph-container {
    width: 100%;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js"></script>
<script src="https://unpkg.com/d3-graphviz@3.0.5/build/d3-graphviz.js"></script>
<div id="dot">
digraph {

    ratio="fill";
    size="<span id="graph-width">#</span>,<span id="graph-height">#</span>";
    margin=0;
    pad=0;
    bgcolor="#f0f0f0";
    nodesep=0.2;
    ranksep=0.2;
    
    node [shape=box,style="rounded,filled",color="#ccccff", fontname="Arial", fontsize="12pt"]
    edge [color="#6666ff"]

    a -> {b c}
    b -> {d e}
    c -> {f g}
    d -> {h i}
    e -> {j k}
    f -> {l m}
    g -> {n o}
    h -> {p q}
    i -> {r s}
    j -> {t u}
    k -> {v w}
    l -> {x y}

    a [label="Alpha (width: <span id="width">#</span>px)"];
    b [label="Bravo"];
    c [label="Charlie"];
    d [label="Delta"];
    e [label="Echo"];
    f [label="Foxtrot"];
    g [label="Golf"];
    h [label="Hotel"];
    i [label="India"];
    j [label="Juliet"];
    k [label="Kilo"];
    l [label="Lima"];
    m [label="Mike"];
    n [label="November"];
    o [label="Oscar"];
    p [label="Papa"];
    q [label="Quebec"];
    r [label="Romeo"];
    s [label="Sierra"];
    t [label="Tango"];
    u [label="Uniform"];
    v [label="Victor"];
    w [label="Whiskey"];
    x [label="Xray"];
    y [label="Yankee"];
    # z [label="Zulu"];

}
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<div id="graph-container" style="height: 30em;"></div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<script>

const graphContainer = d3.select("#graph-container");
const width = graphContainer.node().clientWidth;
const height = graphContainer.node().clientHeight;
const dpi = 96

// Redraw
function redrawGraph(container, { width, height }) {
    document.getElementById("width").innerText = width
    document.getElementById("graph-width").innerText = width / dpi
    document.getElementById("graph-height").innerText = height / dpi
    graphContainer.graphviz()
      .renderDot(document.getElementById("dot").textContent);
}

// Detect container resize
const resizeObserver = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    redrawGraph(entry.target, entry.contentRect);
  }
})

const container = document.querySelector('#graph-container');
resizeObserver.observe(container)

// Initial draw
document.getElementById("width").innerText = container.clientWidth
document.getElementById("graph-width").innerText = container.clientWidth / dpi
document.getElementById("graph-height").innerText = container.clientHeight / dpi
graphContainer.graphviz()
  .renderDot(document.getElementById("dot").textContent);

</script>
</body>
</html>

Known limitations

  • I don't like having to manually specify the diagram height. I'd like the height to be determined automatically, preferably without massively complicating the code. Perhaps also, in both cases within limits:
    • Dynamically changing the height as the available width changes
    • A draggable bottom border to manually change the height
Graham Hannington
  • 1,749
  • 16
  • 18
  • That is a very specialized answer to a relatively broad question. It won't be much help to users who find it if their way to handle the production of graphs is different from yours. My advice would be to rephrase the question to describe what lead you down the path you went, and not one of the many others available. Hint: You use the tags [tag:graphviz] and [tag:d3-graphviz], but do not mention them once in your question. – ccprog Aug 21 '23 at 14:00
  • Feel free to suggest a d3-centric way of handling the container resizing. – Graham Hannington Aug 21 '23 at 14:43
  • @ccprog, my question was deliberately broad. I added those tags to the question because I don't know how to tag answers. I'd love to see answers that use other apps/tools/methods. – Graham Hannington Aug 21 '23 at 14:49
  • ...don't know -> don't think it's possible. I'd have mixed feelings about removing those tags. Does their presence negate the possibility of other solutions? – Graham Hannington Aug 21 '23 at 15:01
  • @ccprog, with apologies, maybe I'm missing your point. Is the question the problem, not the answer? Is the question itself too broad for Stack Overflow? I confess, one of my aims in asking this question was to find other apps/tools/methods. But maybe Stack Overflow is the wrong place to do that? – Graham Hannington Aug 21 '23 at 15:23
  • 1
    Yes, that is what i was trying to say. I am pretty sure that without your own answer the question would have got maybe some close votes, or at least some downvotes and commentary to be more specific. Things work better when you describe the complete path from problem to solution. You did not start out with just any graphing app, but with Graphviz. You did not look for solutions that rewrite the HTML+CSS code to be responsive on itself, but for a JS solution. You did not look at the basic DOM APIs, but were open to include a library, even such a heavy technical debt as D3. – ccprog Aug 21 '23 at 16:16
  • All this is part of your question in the sense that only then you would come up with this answer. Include them, and just the same someone might tell you "Hey, things can be solved easier if you divert from your solution at point X." – ccprog Aug 21 '23 at 16:19
  • Adding the DOT source and the SVG code exported from Graphviz to your question will go a long way. – ccprog Aug 21 '23 at 16:25
  • @ccprog, thanks for your comments. I've added the DOT source to my question, but not the SVG rendered by Graphviz: instead, I've inserted screenshots of rendered diagrams in situ, in a web page, to show (a) the original issue ("too much" white space) and (b) renderings occupying the available space at different widths. I've also gone into more detail about my starting point, and what I see as the requirements, including an abstract grammar (such as DOT) for defining diagrams. I don't see how the basic DOM APIs by themselves, without a precursor abstraction such as DOT, can provide a solution. – Graham Hannington Aug 22 '23 at 03:46
  • You ask a question and within minutes answer your own question. Thus your question won't show on the 'unanswered' SO list. Your "answer" is so detailed no one else dare to answer. Isn't Medium or Dev.to a better place to write blogs? – Danny '365CSI' Engelman Aug 22 '23 at 07:07
  • Not just _within minutes_: I followed the prompt offered by the Stack Overflow UI to answer my own question _immediately_. I'm sincerely interested in other, better answers. I'm keen to know if there are other, perhaps better tools for this purpose. Or better answers using Graphviz. – Graham Hannington Aug 22 '23 at 07:45