diagram-zoom

Zoom and pan for Mermaid and Graphviz diagrams in Quarto

TipTry it

Every diagram on this page has an Enlarge button in its top-right corner (it appears on hover; on touch screens it is always shown). In the viewer:

  • scroll or pinch to zoom, drag to pan,
  • + / − to step, 0 to fit, Esc to close.

Toggle light/dark mode at the top right: the viewer follows the page theme.

This page is the extension’s own example, rendered.

Install

In a project root (or next to a standalone document):

quarto add Lightbridge-KS/quarto-diagram-zoom

Then enable the filter in the document’s front matter, or once in _quarto.yml for a whole book or website:

filters:
  - diagram-zoom

A diagram too wide for the column

Figure 1 squeezes ten steps into the text column; the labels are hard to read until it is enlarged.

Show the source
flowchart LR
  qmd[".qmd source"] --> engine{"Engine?"}
  engine -->|"code cells"| knitr["knitr / Jupyter"]
  engine -->|"markdown only"| md["Markdown"]
  knitr --> md
  md --> pandoc["Pandoc AST"]
  pandoc --> lua["Lua filters<br/>(diagram-zoom adds its script here)"]
  lua --> writer["HTML writer"]
  writer --> deps["HTML dependencies<br/>JS · CSS"]
  deps --> page["Rendered page"]
  page --> browser["Browser: Mermaid draws,<br/>diagram-zoom attaches buttons"]
  browser --> reader(["Reader zooms in"])
flowchart LR
  qmd[".qmd source"] --> engine{"Engine?"}
  engine -->|"code cells"| knitr["knitr / Jupyter"]
  engine -->|"markdown only"| md["Markdown"]
  knitr --> md
  md --> pandoc["Pandoc AST"]
  pandoc --> lua["Lua filters<br/>(diagram-zoom adds its script here)"]
  lua --> writer["HTML writer"]
  writer --> deps["HTML dependencies<br/>JS · CSS"]
  deps --> page["Rendered page"]
  page --> browser["Browser: Mermaid draws,<br/>diagram-zoom attaches buttons"]
  browser --> reader(["Reader zooms in"])
Figure 1: From a .qmd file to a published page.

A sequence diagram

What happens when the reader presses Enlarge:

Show the source
sequenceDiagram
  actor Reader
  participant Button as Enlarge button
  participant Viewer as diagram-zoom viewer
  participant SVG as Diagram SVG
  Reader->>Button: click / tap / Enter
  Button->>Viewer: enlarge(svg)
  Viewer->>SVG: move into a modal dialog
  Reader->>Viewer: wheel, pinch, drag, + / − / 0
  Viewer->>SVG: resize (vector stays sharp)
  Reader->>Viewer: Esc / Close
  Viewer->>SVG: move back, restore attributes
  Viewer->>Button: return focus

sequenceDiagram
  actor Reader
  participant Button as Enlarge button
  participant Viewer as diagram-zoom viewer
  participant SVG as Diagram SVG
  Reader->>Button: click / tap / Enter
  Button->>Viewer: enlarge(svg)
  Viewer->>SVG: move into a modal dialog
  Reader->>Viewer: wheel, pinch, drag, + / − / 0
  Viewer->>SVG: resize (vector stays sharp)
  Reader->>Viewer: Esc / Close
  Viewer->>SVG: move back, restore attributes
  Viewer->>Button: return focus

In a tabset

Show the source
classDiagram
  class DiagramZoom {
    +enlarge(svg, options)
    +attach(svg, options) button
    +observe(root, options) handle
  }
  class Viewer {
    -zoom
    +fit()
    +step(factor)
    +close()
  }
  class QuartoGlue {
    +match(svg) bool
  }
  DiagramZoom --> Viewer : opens
  QuartoGlue --> DiagramZoom : observe(document, match)

classDiagram
  class DiagramZoom {
    +enlarge(svg, options)
    +attach(svg, options) button
    +observe(root, options) handle
  }
  class Viewer {
    -zoom
    +fit()
    +step(factor)
    +close()
  }
  class QuartoGlue {
    +match(svg) bool
  }
  DiagramZoom --> Viewer : opens
  QuartoGlue --> DiagramZoom : observe(document, match)

Show the source
digraph files {
  rankdir=LR;
  node [shape=box, style=rounded, fontname="Helvetica"];
  "diagram-zoom.lua" -> "diagram-zoom.js";
  "diagram-zoom.lua" -> "diagram-zoom.css";
  "diagram-zoom.lua" -> "diagram-zoom-quarto.js";
  "diagram-zoom-quarto.js" -> "diagram-zoom.js" [label="observe()"];
}

files diagram-zoom.lua diagram-zoom.lua diagram-zoom.js diagram-zoom.js diagram-zoom.lua->diagram-zoom.js diagram-zoom.css diagram-zoom.css diagram-zoom.lua->diagram-zoom.css diagram-zoom-quarto.js diagram-zoom-quarto.js diagram-zoom.lua->diagram-zoom-quarto.js diagram-zoom-quarto.js->diagram-zoom.js observe()

An entity-relationship diagram

Show the source
erDiagram
  BOOK ||--|{ CHAPTER : contains
  CHAPTER ||--o{ FIGURE : holds
  FIGURE ||--o| DIAGRAM : "may be"
  DIAGRAM ||--|| VIEWER : "opens in"
  BOOK {
    string title
    string output
  }
  DIAGRAM {
    string kind "mermaid or graphviz"
    bool zoomable
  }

erDiagram
  BOOK ||--|{ CHAPTER : contains
  CHAPTER ||--o{ FIGURE : holds
  FIGURE ||--o| DIAGRAM : "may be"
  DIAGRAM ||--|| VIEWER : "opens in"
  BOOK {
    string title
    string output
  }
  DIAGRAM {
    string kind "mermaid or graphviz"
    bool zoomable
  }

Graphviz

Show the source
digraph viewer {
  rankdir=LR;
  node [shape=box, style=rounded, fontname="Helvetica"];
  closed [label="Page"];
  open [label="Viewer open\n(fitted)"];
  zoomed [label="Zoomed"];
  panned [label="Panned"];
  closed -> open [label="Enlarge"];
  open -> zoomed [label="wheel · pinch · +"];
  zoomed -> zoomed [label="wheel · pinch · + / −"];
  zoomed -> panned [label="drag · arrows"];
  panned -> zoomed [label="wheel · pinch"];
  zoomed -> open [label="0 · Fit"];
  open -> closed [label="Esc · Close"];
  zoomed -> closed [label="Esc · Close"];
  panned -> closed [label="Esc · Close"];
}
viewer closed Page open Viewer open (fitted) closed->open Enlarge open->closed Esc · Close zoomed Zoomed open->zoomed wheel · pinch · + zoomed->closed Esc · Close zoomed->open 0 · Fit zoomed->zoomed wheel · pinch · + / − panned Panned zoomed->panned drag · arrows panned->closed Esc · Close panned->zoomed wheel · pinch
Figure 2: The viewer’s input model, drawn with Graphviz.

Opting out

Wrap a diagram in a .no-zoom div and it keeps its normal rendering, without a button:

Show the source
flowchart LR
  small --> nobutton["no button"]

flowchart LR
  small --> nobutton["no button"]