I’m planning on writing about diffing soon, a problem space that is well-visualized by graphs. So this site needs to support graphs.

I hear a lot of nice things about Mermaid (which has Jekyll support via jekyll-mermaid), but I’ve been using Graphviz for the past two decades and I’m not about to leave an old friend for dead.

Not able to find an existing plugin (and not ready to publish my own gem1), I’ve hacked together a custom Liquid block to support rendering a graph from DOT source and I’m sharing it here in case you’d like to do the same.

Code

  1. Add gem "ruby-graphviz" to your Gemfile
  2. Create a new file _plugins/graphviz.md:
require 'digest/md5'
require 'ruby-graphviz'

module Jekyll
  class GraphvizBlock < Liquid::Block
    def initialize(tag_name, markup, options)
      super
      @tag = markup
    end

    def render(context)
      graph = GraphViz.parse_string(super)
      raise "\"error in #{super.trim.lines.first.chomp}\"" unless graph

      graph_id = @tag unless @tag.empty?
      graph_id ||= graph.id if graph.id.match?(/^[a-zA-Z0-9]+$/)
      graph_id ||= Digest::MD5.hexdigest(super)
      
      svg = graph.output(:svg => String).
        sub(/^.*<svg/m, "<svg class=\"graphviz\" id=\"#{ graph_id }\"")

      "<!--#{super}-->\n#{svg}"
    end
  end
end

Liquid::Template.register_tag('graphviz', Jekyll::GraphvizBlock)

Demo

Assuming your host has the graphviz suite of tools installed2, the following markup:

{% graphviz %}
digraph {
    { rank=same; b, c }
    a -> b[label="0.2",weight="0.2"];
    a -> c[label="0.4",weight="0.4",color=red,penwidth=3.0];
    c -> b[label="0.6",weight="0.6",constraint=false];
    c -> e[label="0.6",weight="0.6",color=red,penwidth=3.0];
    e -> e[label="0.1",weight="0.1"];
    e -> b[label="0.7",weight="0.7",color=red,penwidth=3.0,constraint=false];
}
{% endgraphviz %}

Will render to:

<!--
digraph {
    { rank=same; b, c }
    a -> b[label="0.2",weight="0.2"];
    a -> c[label="0.4",weight="0.4",color=red,penwidth=3.0];
    c -> b[label="0.6",weight="0.6",constraint=false];
    c -> e[label="0.6",weight="0.6",color=red,penwidth=3.0];
    e -> e[label="0.1",weight="0.1"];
    e -> b[label="0.7",weight="0.7",color=red,penwidth=3.0,constraint=false];
}
-->
<svg class="graphviz" id="0aa95122822414653a03fddb28d0382d" width="152pt" height="218pt"
 viewBox="0.00 0.00 152.00 218.00" 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 214)">
<polygon fill="white" stroke="none" points="-4,4 -4,-214 148,-214 148,4 -4,4"/>
<!-- b -->
<g id="node1" class="node">
<title>b</title>
<ellipse fill="none" stroke="black" cx="27" cy="-105" rx="27" ry="18"/>
<text text-anchor="middle" x="27" y="-101.3" font-family="Times,serif" font-size="14.00">b</text>
</g>
<!-- c -->
<g id="node2" class="node">
<title>c</title>
<ellipse fill="none" stroke="black" cx="117" cy="-105" rx="27" ry="18"/>
<text text-anchor="middle" x="117" y="-101.3" font-family="Times,serif" font-size="14.00">c</text>
</g>
<!-- c&#45;&gt;b -->
<g id="edge1" class="edge">
<title>c&#45;&gt;b</title>
<path fill="none" stroke="black" d="M89.68,-105C82.19,-105 73.92,-105 65.91,-105"/>
<polygon fill="black" stroke="black" points="66.01,-101.5 56.01,-105 66.01,-108.5 66.01,-101.5"/>
<text text-anchor="middle" x="72" y="-111.8" font-family="Times,serif" font-size="14.00">0.6</text>
</g>
<!-- e -->
<g id="node3" class="node">
<title>e</title>
<ellipse fill="none" stroke="black" cx="71" cy="-18" rx="27" ry="18"/>
<text text-anchor="middle" x="71" y="-14.3" font-family="Times,serif" font-size="14.00">e</text>
</g>
<!-- c&#45;&gt;e -->
<g id="edge2" class="edge">
<title>c&#45;&gt;e</title>
<path fill="none" stroke="red" stroke-width="3" d="M108.13,-87.61C102.02,-76.32 93.74,-61.02 86.55,-47.73"/>
<polygon fill="red" stroke="red" stroke-width="3" points="89.84,-46.46 82,-39.33 83.68,-49.79 89.84,-46.46"/>
<text text-anchor="middle" x="105" y="-57.8" font-family="Times,serif" font-size="14.00">0.6</text>
</g>
<!-- e&#45;&gt;b -->
<g id="edge5" class="edge">
<title>e&#45;&gt;b</title>
<path fill="none" stroke="red" stroke-width="3" d="M62.54,-35.34C56.77,-46.5 48.95,-61.6 42.12,-74.79"/>
<polygon fill="red" stroke="red" stroke-width="3" points="39.06,-73.09 37.57,-83.58 45.28,-76.31 39.06,-73.09"/>
<text text-anchor="middle" x="61" y="-57.8" font-family="Times,serif" font-size="14.00">0.7</text>
</g>
<!-- e&#45;&gt;e -->
<g id="edge6" class="edge">
<title>e&#45;&gt;e</title>
<path fill="none" stroke="black" d="M95.53,-26.03C106.51,-26.79 116,-24.12 116,-18 116,-14.18 112.29,-11.7 106.83,-10.56"/>
<polygon fill="black" stroke="black" points="107.21,-7.08 97.04,-10.05 106.85,-14.07 107.21,-7.08"/>
<text text-anchor="middle" x="125" y="-14.3" font-family="Times,serif" font-size="14.00">0.1</text>
</g>
<!-- a -->
<g id="node4" class="node">
<title>a</title>
<ellipse fill="none" stroke="black" cx="71" cy="-192" rx="27" ry="18"/>
<text text-anchor="middle" x="71" y="-188.3" font-family="Times,serif" font-size="14.00">a</text>
</g>
<!-- a&#45;&gt;b -->
<g id="edge3" class="edge">
<title>a&#45;&gt;b</title>
<path fill="none" stroke="black" d="M62.52,-174.61C56.31,-162.62 47.75,-146.09 40.6,-132.27"/>
<polygon fill="black" stroke="black" points="43.86,-130.95 36.15,-123.68 37.64,-134.17 43.86,-130.95"/>
<text text-anchor="middle" x="61" y="-144.8" font-family="Times,serif" font-size="14.00">0.2</text>
</g>
<!-- a&#45;&gt;c -->
<g id="edge4" class="edge">
<title>a&#45;&gt;c</title>
<path fill="none" stroke="red" stroke-width="3" d="M79.87,-174.61C85.98,-163.32 94.26,-148.02 101.45,-134.73"/>
<polygon fill="red" stroke="red" stroke-width="3" points="104.32,-136.79 106,-126.33 98.16,-133.46 104.32,-136.79"/>
<text text-anchor="middle" x="105" y="-144.8" font-family="Times,serif" font-size="14.00">0.4</text>
</g>
</g>
</svg>

Which looks like:

b b c c c->b 0.6 e e c->e 0.6 e->b 0.7 e->e 0.1 a a a->b 0.2 a->c 0.4

Controlling the <svg>’s id

The generated <svg> tag includes an id attribute determined by, in order:

  1. A parameter passed to the Liquid tag (e.g. {% graphviz Id %})
  2. The name of the graph (e.g. graph Id { … })
  3. A hash of the graph’s source code
  1. Though this seems like the perfect opportunity for me to learn someday 

  2. Which Netlify does!