Skip to content
flowchart TD

Mermaid Diagrams

Mermaid is a powerful tool for creating diagrams and visualizations using a simple markdown-like syntax. It allows you to create flowcharts, sequence diagrams, class diagrams, and more. Below are some examples of Mermaid diagrams which use the Hand Drawn style with the Excalifont(s) for a more informal and visually appealing look.

Sequence Diagram

%% Example Sequence Diagram
sequenceDiagram
    actor Alice
    actor Bob
    Alice->>+Bob: Hello Bob, how are you?
    Bob->>Bob: Thinking...
    Bob-->>-Alice: Hi Alice, I'm good thanks!

Flowchart

%% Example Flowchart
flowchart TD
    classDef fillBlack fill:#000
    classDef fillBrown fill:#c96
    classDef fillRed fill:#fcc
    classDef fillOrange fill:#fc9
    classDef fillYellow fill:#ffc
    classDef fillGreen fill:#cfc
    classDef fillAqua fill:#cff
    classDef fillBlue fill:#69f
    classDef fillViolet fill:#ccf
    classDef fillGray fill:#ccc
    classDef fillWhite fill:#fff

    A[Start]:::fillGreen
    B{Is it sunny?}:::fillViolet
    C[Go for a walk]:::fillYellow
    D[Stay indoors]:::fillBlue
    E[/Enjoy the sunshine!\]:::fillOrange
    F[\Find something fun to do inside!/]:::fillAqua
    G((End)):::fillRed
    A --> B
    B --> C
    B --> D
    C --> E
    D --> F
    E --> G
    F --> G  

Class Diagram

%% Example Class Diagram
classDiagram
    class Animal {
        +String name
        +int age
        +void eat()
    }
    class Dog {
        +String breed
        +void bark()
    }
    Animal <|-- Dog

Entity Relationship Diagram

%% Example ER Diagram
erDiagram
    CUSTOMER ||--o{ ORDER : places
    ORDER ||--|{ LINE_ITEM : contains
    CUSTOMER }|..|{ DELIVERY_ADDRESS : uses

MkDocs Integrations

In order to force the first diagram on each page to render properly, I had to add a section to the mkdocs_hooks.py and a mermaid-init.js. These files force a fake blank diagram on the page and to wait for the fonts to fully load before any rendering or sizing is done on the first real diagram.

  • mkdocs_hooks.py:

    from __future__ import annotations
    
    from pathlib import Path
    from typing import Any
    
    _MERMAID_WARMUP_HTML = (
        '<pre class="mermaid mermaid-warmup"'
        ' style="height: 0; overflow: hidden; margin: 0; padding: 0; border: none; opacity: 0; pointer-events: none;">'
        '<code>flowchart TD</code>'
        '</pre>\n'
    )
    
    def on_page_content(html, page, config, files, **kwargs):
        """Inject a warmup <pre class="mermaid"> element at the beginning of the page content to ensure that Mermaid is loaded before any diagrams are rendered."""
        if "mermaid" not in html.lower():
            return html
        return _MERMAID_WARMUP_HTML + html
    

  • mermaid-init.js:

    document$.subscribe(() => {
      if (typeof mermaid === "undefined") {
        return;
      }
      document.querySelectorAll(".mermaid-warmup").forEach((el) => el.remove());
    
      mermaid.initialize({
        startOnLoad: false,
        securityLevel: 'loose',
        fontFamily: 'Excalifont, cursive', // Use Excalifont
        layout: "elk",
        look: "handDrawn",
        theme: "base", // You can also try 'neutral', 'dark', 'forest', 'default'
        themeVariables: {
          'fontFamily': 'Excalifont, cursive',
          'fontSize': '16px',
          'primaryColor': '#ecccff',
          'primaryTextColor': '#000000',
          'primaryBorderColor': '#000000',
          'lineColor': '#000000',
          'textColor': '#000000',
          'handDrawnCurve': true // Enable hand-drawn style
        },
        flowchart: {
          curve: 'linear',
          htmlLabels: true,
          useMaxWidth: true,
        },
        sequence: {
          actorFontFamily: 'Excalifont, cursive',
          messageFrontFontFamily: 'Excalifont, cursive',
          noteFontFamily: 'Excalifont, cursive',
        },
      });
    
      const blocks = document.querySelectorAll("pre.mermaid:not(.mermaid-warmup)");
    
      blocks.forEach((block, index) => {
        const code = block.querySelector("code");
        const text = code ? code.textContent : block.textContent;
        const div = document.createElement("div");
        div.classList.add("mermaid");
        div.id = "mermaid-" + Date.now() + "-" + index;
        div.textContent = text || "";
        block.replaceWith(div);
      });
    
      Promise.allSettled([
          document.fonts.load("16px 'Excalifont'"),
          document.fonts.load("16px 'Cascadia Code'"),
          ]).then(() => {
            mermaid.run({
              querySelector: ".mermaid:not([data-processed])",
            }).then(() => {
              requestAnimationFrame(() => fixActorBoxPadding(12));
            }).catch((err) => {
              console.warn("Mermaid rendering error:", err);
              mermaid.run({
                querySelector: ".mermaid:not([data-processed])",
              }).then(() => {
                requestAnimationFrame(() => fixActorBoxPadding(12));
              }).catch((err) => {
              console.warn("Mermaid retry error:", err);
              });
            });
          });
    });
    
    /**
     * After Mermaid renders a sequence diagram, the actor/participant boxes may be sized using fallback-font metrics.
     * This function uses the live getBBox() of each rendered element to compute the true width, expands the surrounding
     * <rect>, and re-centers the label so every box has at least `padding` pixels of clear space.
     */
    function fixActorBoxPadding(padding) {
      document.querySelectorAll("div.mermaid svg").forEach((svg) => {
        svg.querySelectorAll("rect.actor").forEach((rect) => {
          const g = rect.parentElement;
          if (!g) return;
          const text = g.querySelector("text");
          if (!text) return;
    
          let tBBox, rBBox;
          try {
            tBBox = text.getBBox();
            rBBox = rect.getBBox();
          } catch (e) {
            return;
          }
    
          if (tBBox.width === 0) return;
    
          const needed = tBBox.width + padding *2;
          if (needed <= rBBox.width) return; // already fits
    
          const extra = needed - rBBox.width;
          const newX = rBBox.x - extra / 2;
          rect.setAttribute("x", newX);
          rect.setAttribute("width", needed);
    
          // Re-center the text label inside the expanded box
          const centerX = newX + (needed / 2);
          text.setAttribute("x", centerX);
          text.querySelectorAll("tspan[x]").forEach((ts) => {
            ts.setAttribute("x", centerX);
          });
        });
      });
    }