Projects About AlBasmala
build status

org-special-block-extras: Org lists, drawn as Graphviz graphs

Article image
  

Ξ  

⭐ The code lives at github.com/alhassy/org-special-block-extras —star it, or discuss it there!

1. An outline is already a graph

Say we are showing a friend how 1 * 2 + 3 - 4 parses. On paper, we would draw a tree. In Org, we would reach for Graphviz —and write that tree a second time, in a second language:

digraph { "-" -> "+"; "-" -> 4; "+" -> "*"; "+" -> 3; "*" -> 1; "*" -> 2; }

Every arc there restates what an outline's indentation already says. So we write the outline, and let the outline be the graph:

- + 4 * 3 1 2

That is the whole idea. A #+begin_graph block compiles the Org we already write —nested lists, two-column tables, links, tags, footnotes— into Graphviz dot. Nesting is an arc; the rest of this README refines that one sentence, one gap at a time.

1.1. Setup

org-graph-block extends org-special-block-extras:

(require 'org-special-block-extras)
(require 'org-graph-block)

Graphviz must be on our PATH: dot -V should work in a terminal.

1.2. A REPL for pictures

The fun part is how quickly a picture grows. Start with the seven tokens as a flat list, one item each, and press C-c C-c anywhere in the block: seven lonely nodes. Now M-S-<right> demotes an item, children and all —Org's own list editing— and another C-c C-c shows the new arc. A minute later, the flat list is the tree above. (Plain M-<right> demotes the item alone, leaving its children behind, as anywhere in Org.)

C-c C-c renders a PNG and links it beneath the block, under #+RESULTS:. The file's name is made up, unless :file picks one:

#+begin_graph :file parse-tree.png
…
#+end_graph

When the picture surprises us, C-u C-c C-c shows the generated dot instead; see Comforts while editing.

2. Arcs that speak: description lists

A bare arc says “related”, and little else. Explaining how Emacs starts up, we want each arc to say how: Emacs reads its init file, which loads packages. Org's description list, - TERM :: DEFINITION, already attaches a word to an item; in a graph, that word sits on the arc into the item. Read left to right, the line is the arc —Emacs, then starts, then server:

Emacs init.el reads server starts packages loads emacsclient answers

Two arcs between the same nodes are just two items; a client that both requests and retries reaches one server twice:

+ client
  + request :: server
  + /retry/ :: server
client -> server [ label=request ];
client -> server [ label=<<I>retry</I>> ];
  • The term may be marked up, as /retry/ is; it is then an HTML-like label.
  • It is the terse spelling of | :edge.label | request |, met below; an explicit :edge.label row wins.
  • A top-level item has no arc into it, so a term there is an error —except for a legend entry, met later.
  • As in Org, the :: needs whitespace on both sides: std::vector is a name, not a description.

4. Ids and labels

Links make us name things, and a name wants to be short where a label wants to be clear. So every node has an id, which arcs and links name, and a label, which is drawn. By default both are the item's text; these three items, spelling it out more and more, all declare one and the same node:

+ Lisp

+ <<Lisp>> Lisp

+ Lisp
  | :id   | Lisp |
  | label | Lisp |
Lisp [ label=Lisp ];

When the two should differ, we set either one apart: a <<target>> or an | :id | row gives the id, and a | label | row gives the label:

+ <<lam>> λ-calculus
+ Combinatory Logic
  | :id | combinatory |
+ Church
  | label | Alonzo Church |
lam [ label="λ-calculus" ];
combinatory [ label="Combinatory Logic" ];
Church [ label="Alonzo Church" ];
  • The id is the :id row, else the <<target>>, else the text —sans markup, links, footnotes, and tags.
  • The label is the label row, else the text; markup makes it an HTML-like one.
  • Two items with one id are one node, so an item may appear twice to gain more children.

5. Styling: one key-value syntax

Out of the box, Graphviz draws black ellipses; soon we want a box here and a red arc there. Graphviz's attributes are key-value pairs, and Org's key-value syntax is the two-column table. So a table beneath an item styles that item: one syntax, from a single node up to the whole graph.

5.1. A node's table, and two kinds of keys

By way of example, this item has one row of each kind:

+ Alonzo
  | label | Alonzo Church |
  | :id   | church        |
church [ label="Alonzo Church" ];

A plain key, such as label, is a Graphviz attribute of the node, passed straight through. We quote its value for you, so Alonzo Church, #00ff00, and x -> y all need no quotes; already-quoted values, and <…> HTML labels, are left alone.

A key starting with a colon is reserved: it means something to the graph block itself, never to Graphviz. A leading colon is never a Graphviz attribute, so the two kinds cannot clash. The reserved keys are listed in org-graph-reserved-keys; each is met in due time:

Key Meaning
:id the node's name for cross-references; else its <<target>>, else its text
:edge.KEY attribute KEY of the arc from the parent
:edge[T].KEY attribute KEY of this item's arc to T
:inherit inherit the rows of #+name: templates; also :NAME: tags
:children-rank { rank=… } over this item's immediate children

An unknown :key is an error, rather than a silently ignored attribute.

5.2. An arc's table: :edge.KEY

An arc is drawn from parent to child, so the child's table is where it is styled: an :edge.KEY row there is attribute KEY of the arc into the item. Every attribute is its own row, exactly like a node's. Our client's two arcs to one server now look different:

+ client
  + request :: server
    | :edge.color | blue |
  + retry :: server
    | :edge.style | dashed |
client -> server [ label=request color=blue ];
client -> server [ label=retry style=dashed ];

A reference is styled just the same; this is why an inline link graduates to one:

+ Lisp
  + borrows :: lam
    | :edge.style | dotted |
Lisp -> lam [ label=borrows style=dotted ];
  • A parallel arc is simply a second child, styled on its own; a KEY given twice is an error.
  • As in Org, an item's tables and directives may also follow its sub-items; they still belong to it.

5.3. The whole graph: the #+begingraph line

Small graphs deserve no preamble at all. So the header line takes the graph's title and any graph attribute:

#+begin_graph Lambda calculus :rankdir LR :node.shape box :edge.color grey
+ λ
  + Lisp
#+end_graph
graph [ label="Lambda calculus" rankdir=LR ]
node [ shape=box ]
edge [ color=grey ]
  • The title, the words before the first :key, is the graph's label; :directed nil makes the graph undirected, with -- arcs.
  • :KEY V is a graph attribute; :node.KEY V and :edge.KEY V are node and edge defaults —the same KEY, node.KEY, edge.KEY spelling as table rows.
  • As on Org's own #+attr_html: lines, values need no quotes: :bgcolor #6c7086, :fontname Helvetica,Arial. A value with spaces does: :fontname "Helvetica Neue".
  • :file, :svg, :directed, :wrap, :notes, :unflatten, :unknown-tags, :smartdiagram, :dpi, and :C-c_C-c belong to the block itself, never to Graphviz; each is met below. So do the Babel habits :exports and :results, which the block ignores.

5.4. When the preamble outgrows the line

A real graph's settings outgrow one line. Then #+graph:, #+node:, and #+edge: name what the table below them styles —the graph, every node, every arc— and they override the header. A drawer of any name, :SETTINGS: or :PALETTE:, folds them away with TAB, along with #+rank: and #+name: lines:

#+begin_graph Lambda calculus
:SETTINGS:
#+graph:
| rankdir | LR |
#+node:
| shape | box |
:END:
+ λ
  + Lisp
#+end_graph
graph [ label="Lambda calculus" rankdir=LR ]
node [ shape=box ]

5.5. Subtrees inherit #+node:, #+edge:, and #+rank:

Sometimes a style belongs to one branch, not the whole graph. Org's properties are inherited: a :PROPERTIES: drawer on a heading reaches its subheadings. So it is with an item's #+node: and #+edge: tables: they style every node and arc beneath the item, while the item itself keeps its own | key | value | rows; its #+rank: lines apply there too. Graphviz scopes such defaults by an anonymous subgraph, which draws no box:

+ Weather
  #+node:
  | shape | box |
  #+edge:
  | style | dotted |
  + Rain
    + Umbrella
  + Sun
+ Climate
Weather [ label=Weather ];
subgraph {
  node [ shape=box ]
  edge [ style=dotted ]
  Weather -> Rain;
  Rain [ label=Rain ];
  Rain -> Umbrella;
  Umbrella [ label=Umbrella ];
  Weather -> Sun;
  Sun [ label=Sun ];
}
Climate [ label=Climate ];

A deeper item's tables override a shallower one's, as nested :PROPERTIES: do; a node first declared elsewhere keeps the defaults of where it was declared.

6. Saying it once: templates

A family tree repeats itself: every marriage is the same pink, two-headed arc, and every wife the same boxed node. Restating those rows each time is how styles drift apart. So we name them once: a #+name: table is a reusable bundle of rows, and a tag applies it. On a headline, :work:urgent: classifies the entry; on an item, tags classify the node. After an item, a tag styles its node; after a reference, its arc:

#+name: married
| color | "#cc3399" |
| dir   | both      |
#+name: wife
| shape | box |

+ Muhammad
+ Khadija :wife:
  + Muhammad :married:
    | :edge.color | red |
Muhammad [ label=Muhammad ];
Khadija [ shape=box label=Khadija ];
Khadija -> Muhammad [ dir=both color=red ];

Explicit rows win over a template's, so the marriage above is red. A line may carry several tags, :married:faint:, each a template, the earlier winning on a clash. The same rule reaches every other line a template could apply to:

Line What its tags style
+ Khadija :wife: the node
+ [[Muhammad]] :married: the arc to the reference
+ wed :married: :: Khadija :wife: the arc, by the description term's tags; the node, by its own
#+node: :wife: every node; likewise #+edge: and #+graph:
#+name: wife :base1:base2: the template wife, which inherits base1 and base2

6.1. Tags are sugar for :inherit

Each tag is terse sugar for an :inherit row, which names one or more templates and is often the clearer choice in a long-lived graph: | :inherit | wife | in an item's table or under #+node:, | :edge.:inherit | married | for the arc into an item. Wherever it sits, the item's explicit rows still win.

  • A template may inherit others; one that, directly or not, inherits itself contributes nothing the second time.
  • A name that no #+name: defines is an error, rather than a silent nothing.
  • As with Babel's named tables, a #+name: table anywhere in the buffer, outside any graph, is a template every graph in the buffer may use; so a house style ---wife, married— is named once per file rather than once per graph. A graph's own template of the same name wins, and a template inside one graph stays that graph's own.

6.2. Which colons are tags

Tags are trailing :word: groups, exactly as on a headline; inside graph blocks, they wear Org's org-tag face, so they do not read as part of the label. Colons anywhere else are just text, and a label that needs a trailing :word: of its own puts it in =verbatim=, or in a | label | row:

+ Ratio 1:2
+ std::vector
+ Meeting at 12:30 :faint:
+ Vim's =:wq:=
"Ratio 1:2" [ label="Ratio 1:2" ];
"std::vector" [ label="std::vector" ];
"Meeting at 12:30" [ style=dotted label="Meeting at 12:30" ];
"Vim's :wq:" [ label=<Vim's <FONT FACE="monospace">:wq:</FONT>> ];

Unlike Org's, our tags may be kebab-case: :killed-by:. A tag naming no template is an error, rather than a typo silently becoming part of the label.

6.3. Prototype first, name later

That strictness is right for a finished graph, and in the way of a sketch: while prototyping, we want shapes before names. #+begin_graph :unknown-tags random-templates makes up a template for each unknown tag —a colour, a shape, a font— the same at every use, so the tags read at a glance. A legend entry gives each its meaning; when the design settles, #+name: tables replace the made-up templates, one tag at a time:

Prototype first, name later Legend a calculus a programming language an editor λ-calculus Combinatory logic Lisp Scheme Emacs Lisp Emacs

The Nth unknown tag takes the Nth of org-graph-random-template-colours, -shapes, and -fonts, so renders are stable, and the first eight tags differ in every way.

6.4. Legends are description lists

A reader meeting pink arcs asks what pink means. A legend answers: a glossary of templates. Org's glossary is the description list, so a top-level - :T: :: MEANING —which has no parent to arc from— is a legend entry: a sample styled by template T, captioned MEANING. Wrap the entries in a #+begin_cluster to box them:

#+name: married
| color | pink |
| dir   | both |
#+name: wife
| shape | box |

+ Muhammad
+ Khadija :wife:
  + Muhammad :married:
#+begin_cluster Legend
- :married: :: marriage
- :wife: :: /wife/
#+end_cluster
subgraph cluster_Legend {
  label=Legend
  legend_1 [ shape=point style=invis ];
  legend_1_end [ shape=point style=invis ];
  legend_1 -> legend_1_end [ color=pink dir=both label=marriage ];
  legend_2 [ shape=box label=<<I>wife</I>> ];
}
  • The legend can never drift from the graph: change married, and its sample changes too.
  • A template that styles some arc in the graph —by a reference's or a term's tags, an :edge.:inherit row, or an #+edge: line— is sampled as an arc; any other, as a node.
  • …~, sample their templates together.
  • The sample drops constraint, which shapes the layout rather than the arc.

6.5. Checkboxes are templates

A checkbox says how far along a thing is; so a graph of tasks is just a checklist. [X], [-], and [ ] apply the templates checked, partial, and unchecked, as a :checked: tag would; and C-c C-c on a checkbox toggles it, as Org's own does, then renders the graph:

#+name: unchecked
| style | dashed |

+ [-] compile
  + [X] parse
  + [ ] typecheck
compile [ color=darkorange fontcolor=darkorange label=compile ];
parse [ color=forestgreen fontcolor=forestgreen label=parse ];
typecheck [ style=dashed label=typecheck ];

checked and partial default to green and orange, by org-graph-checkbox-templates; a graph's own #+name: template wins. A checkbox whose template exists nowhere, such as unchecked by default, styles nothing.

6.6. Macros name single values

Templates name whole bundles of rows; sometimes we want to name one value. A palette repeats itself, and Org's #+macro: names a value once, so {{{name}}} uses it —in a table, an item, a cluster's header, anywhere in the block:

#+macro: gold #f9e2af
#+begin_graph
+ Church
  + Turing
    | :edge.color     | {{{gold}}} |
    | :edge.fontcolor | {{{gold}}} |
#+end_graph

An undefined macro is an error. In similar spirit, inline src_LANG{…} blocks and call_NAME() calls run at export, as anywhere in Org, their results landing in the label; C-c C-c shows them as written.

7. Richer labels

A label is more than a name: we want a word in bold, a type in monospace, a formula, a second line. Again, Org already has the syntax.

7.1. Markup makes rich labels

*bold*, /italic/, _underline_, +strike+, =code=, and ~pre~ in an item's text make its label an HTML-like one, while its id stays the plain text:

+ *Church* encodes =Bool=
"Church encodes Bool" [ label=<<B>Church</B> encodes <FONT FACE="monospace">Bool</FONT>> ];

We escape &, <, >, and " for you. Graphviz has no way to tell =code= from ~pre~, so both are monospace. A #+record: table's cells keep their markup too. Names such as a_b, C++, and org-graph.el are not mistaken for markup.

7.2. Entities, scripts, and snippets

Org's entities are their characters, and braced sub- and superscripts are Graphviz's own; as under Org's #+options: ^:{}, an unbraced a_b stays text, so snake_case ids are safe. For whatever Org has no markup for, a @@dot:…@@ export snippet is raw label HTML; a snippet for any other backend vanishes:

+ \alpha \to \beta
+ x^{2} H_{2}O
+ @@dot:<FONT COLOR="red">@@Alert@@dot:</FONT>@@
"α → β" [ label="α → β" ];
"x2 H2O" [ label=<x<SUP>2</SUP> H<SUB>2</SUB>O> ];
Alert [ label=<<FONT COLOR="red">Alert</FONT>> ];
  • Timestamps, statistics cookies, and citations show as written; LaTeX fragments too, since Graphviz has no math.
  • Syntax that means nothing in a graph —a headline, a #+begin_quote, an unknown #+key:— is an error, rather than silently dropped. Comments, and footnote definitions, are fine.

7.3. \\ breaks a line, :wrap fills

A long label makes a wide node, and a wide node a sprawling graph. In Org, a \\ ending a line is a hard line break; in a graph, it breaks the node's label. Since a label seldom wants a literal \\, it may sit mid-line too, so a two-line node stays one terse line:

+ Emacs
  + provides :: <<closures>> Closures \\ (lexical scope)
  + *Lisp* \\
    McCarthy, 1958
Emacs -> closures [ label=provides ];
closures [ label=<Closures<BR/>(lexical scope)> ];
Emacs -> "Lisp McCarthy, 1958";
"Lisp McCarthy, 1958" [ label=<<B>Lisp</B><BR/>McCarthy, 1958> ];

The label becomes an HTML-like one, as with markup; the id joins the lines with a space, so a node reached by a link usually wants a <<target>>, as closures above.

It is the one line break wherever a label is written: a description term, a table value, a #+record: cell. So an arc's label breaks just as its node's does:

+ Monad
  + every monad \\ is an adjunction :: Adjunction
  + Turing
    | :edge.label | Church– \\ Turing thesis |
Monad -> Adjunction [ label=<every monad<BR/>is an adjunction> ];
Monad -> Turing [ label="Church–\nTuring thesis" ];

Breaking every long label by hand is tedious, though. #+begin_graph :wrap N fills node and arc labels to lines of N characters, so long item text needs no \\ at all; a label already broken by \\ stays as written.

9. Layout: ranks, boxes, and records

Graphviz lays out the graph for us, and mostly well; but sometimes the layout says something the graph does not mean. Here is how we take the wheel back.

9.1. Ordered lists keep their order

Look again at our parse tree: 1 * 2 + 3 - 4 subtracts 4 from the rest, never the other way around, yet nothing in an unordered list promises Graphviz will draw 4 on the right. Org's + - * bullets say “these things”; 1. 2. 3. say “first, second, third”. So ordered children sit side by side, on one rank, in the order written —Graphviz no longer reshuffles them to save a crossing:

+ -
  1. +
     1. *
        1. 1
        2. 2
     2. 3
  2. 4
"-" [ ordering=out label="-" ];
"-" -> "+";
"-" -> 4;
{ rank=same; "+"; 4 }
…
  • An ordered top-level list, or one inside a cluster, shares a rank too.
  • An explicit | :children-rank | min | wins over the rank=same; a lone ordered child gets no rank at all.
  • For one rank without the fixed order, use | :children-rank | same | with + bullets.

Nodes with no common parent share a rank by #+rank:, which takes bare ids or links; a link spares a multi-word id its quotes:

+ Logic
  + λ calculus
+ Lisp
#+rank: Lisp [[λ calculus]]
{ rank=same; "λ calculus"; Lisp }

Under splines=curved, Graphviz drops the label of an arc between two nodes on one rank; so there, such an arc's label becomes an xlabel, which it still draws.

9.2. Clusters draw boxes

Some nodes belong together —Emacs's packages, say— and a box around them says so. A cluster is a subgraph, so it gets the graph's own spelling: a #+begin_cluster block takes a title and :KEY V attributes, :node.KEY and :edge.KEY defaults included, exactly as the #+begin_graph line does; and, being a block, it folds with TAB:

+ Emacs
  #+begin_cluster Packages :bgcolor lightgrey :node.shape box
  + Org
    #+begin_cluster
    + agenda
    + babel
    #+end_cluster
  + Magit
  #+end_cluster
graph [ compound=true ]
subgraph cluster_Packages {
  label=Packages
  bgcolor=lightgrey
  node [ shape=box ]
  Org [ label=Org ];
  subgraph cluster_anonymous_1 { label="" agenda; babel; }
  Org -> agenda [ lhead=cluster_anonymous_1 ];
  Magit [ label=Magit ];
}
Emacs -> Org [ lhead=cluster_Packages ];
  • A cluster block inside an item is that item's child: above, Emacs has an arc to Packages. An arc to or from a cluster lands on the cluster's first node, clipped at its border, as lhead shows.
  • An untitled cluster is an anonymous box, with no label and no name to draw arcs to.
  • The title runs up to the first :KEY; #+begin_cluster Time 12:30 is titled Time 12:30.
  • When the attributes outgrow the line, a table opening the block styles the box, as an item's table styles its node; #+node: and #+edge: tables set defaults inside it, as usual:

    #+begin_cluster Prophets
    | style     | filled  |
    | fillcolor | #ddeeff |
    #+node:
    | fillcolor | #aaccff |
    + Adam
    #+end_cluster
    
  • C-c C-c, C-M-i, and eldoc work inside a cluster block just as anywhere else in the graph.

9.3. Records: a #+record: table

Some nodes have insides: a C struct has fields, a UML class has members, and an arc may want to leave from one particular field. A record is a node drawn as a table, and Org already has tables: under #+record:, an item's table is its label, laid out exactly as written —just as #+graph:, #+node:, and #+edge: name what the table below them is. A cell's <<target>> is its port, a cell's [[link]] is an arc from that port, and an arc reaches a port by record::port —just as Org's [[file:notes.org::target]] reaches into a file:

+ struct1
  #+record:
  | left | <<mid>> middle struct2::one | right |
+ struct2
  #+record:
  | <<one>> one | two   |
  | ^           | three |
+ reader
  + struct1::mid
struct1 [ shape=plain label=<<TABLE …><TR><TD>left</TD><TD PORT="mid">middle</TD><TD>right</TD></TR></TABLE>> ];
struct1:mid -> struct2:one;
struct2 [ shape=plain label=<<TABLE …><TR><TD PORT="one" ROWSPAN="2">one</TD><TD>two</TD></TR><TR><TD>three</TD></TR></TABLE>> ];
reader -> struct1:mid;

Org tables have no merged cells, so two markers stand in for them: ^ continues the cell above, as one does, and an empty cell continues the cell to its left —or, in the first column, the one above. Together they draw any layout that Graphviz's old { } nesting could:

+ struct3
  #+record:
  | hello \\ world | b |            |   | g | h |
  | ^              | c | <<here>> d | e | ^ | ^ |
  | ^              | f |            |   | ^ | ^ |

Here b and f span three columns, hello, g, and h three rows; a \\ breaks a line, as in an item. The gallery has both records live.

Hlines and column cookies mean what they mean to Org. An hline draws a rule between rows, and a table with any hline draws one outer border rather than a box around every cell —so a UML class is just the table we would write anyway. A row of <l>, <c>, and <r> cookies aligns its columns, and cells keep their markup:

+ Person
  #+record:
  | <l>            |
  | *Person*       |
  |----------------|
  | name: =String= |
  | age: =Int=     |
  |----------------|
  | greet()        |
Person [ shape=plain label=<<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0"><TR><TD ALIGN="LEFT" BALIGN="LEFT"><B>Person</B></TD></TR><HR/>…</TABLE>> ];
  • A record is otherwise an ordinary node: its children are nodes of their own, and its attributes, tags, and templates apply as usual; color, fillcolor, and fontcolor colour the table. Its attribute table goes above #+record:, since rows right below the record table are part of it.
  • A record is shape=plain, the table being its whole outline; an explicit shape row wins.
  • A cell with a link but no target is ported by its text, spaces as underscores; <<x marks the spot>> is likewise reached as [[struct3::x marks the spot]].
  • Ports belong to their record, so two records may both have a head port: r1::head and r2::head.
  • A cell's trailing tags style its arcs, as a reference's style its arc: | <<mid>> middle [[struct2::one]] :dashed: | draws a dashed arc. A cell with tags but no link is an error; write =:x:= for literal text.
  • a::b names a port only when a names a record; otherwise it is an ordinary node, such as std::vector. Record and port names may contain dots: [[org.el::v1.2]].
  • Graphviz spells ports record:port; that is the spelling in the generated dot.

9.4. Mind maps: :layout twopi

Every graph so far flows top to bottom; a mind map grows outward from its centre. Graphviz's layout attribute picks its engine, so the header line turns a tree into a mind map: twopi places root in the middle and each generation on a ring around it. With :wrap, long labels stay compact:

Org mode, radially Org mode Agenda Babel Export TODO keywords Scheduling and deadlines Tangling Evaluating source blocks HTML LaTeX

Sans :root, twopi picks a centre itself. neato, fdp, and circo suit graphs that are not trees.

9.5. Wide trees: :unflatten N

A node with many children lays them out in one wide row: a ribbon, too wide for the page. :unflatten N passes the dot through Graphviz's own unflatten -l N, which staggers those leaves over N ranks, so the graph is a block rather than a ribbon:

Lisp's children Lisp Scheme Common Lisp Clojure Racket Emacs Lisp Fennel Janet Hy

10. Graphs in their document

A graph rarely stands alone: prose refers to it, and an outline may already be one. Org's own document machinery reaches graphs too.

10.1. #+name: and #+caption: make a figure

We want to write “as Figure 1 shows”, and have the number stay right. In Org, #+name: is what [[links]] reach, and #+caption: captions a table or an image. A graph is an image, so both mean what they always do:

As lineage shows, Emacs is a Lisp.

#+name: lineage
#+caption: How Emacs came to be
#+begin_graph
+ λ-calculus
  + Lisp
    + Emacs Lisp
#+end_graph
  • The graph renders to lineage.png —both on C-c C-c and on export— and exports as that image, numbered and captioned: [[lineage]] reads “Figure 1” in HTML, and \ref in LaTeX.
  • An explicit :file still wins: #+begin_graph :file elsewhere.png renders there, and the #+name: still labels it. With :svg t, the figure is lineage.svg.
  • An unnamed graph exports as before: inlined into HTML, or as dot source elsewhere.

10.2. Sections are graphs: the :graph: tag

Sometimes a whole section is nothing but the outline we want drawn; wrapping it in a block is ceremony. A #+begin_graph block is a function on a region; a :graph: tag is the same function on a section. The section's text is the graph, and its :PROPERTIES: drawer —Org's own home for key-value settings— is the #+begin_graph line: :RANKDIR: LR is :rankdir LR, :NODE.SHAPE: box is :node.shape box, and :FILE: is :file. See the live example:

* Emacs's lineage :graph:
:PROPERTIES:
:RANKDIR: LR
:NODE.SHAPE: box
:END:
+ λ-calculus
  + inspired :: Lisp
    + Emacs Lisp
  • The heading stays a heading; only its text becomes the graph, on export.
  • :graph_Some_Title: titles the graph Some Title, as the words before the first :key would.
  • Org's own properties, such as :CUSTOM_ID: and :CATEGORY:, stay Org's; they never reach Graphviz.

11. Beyond Graphviz: smartdiagram

Graphviz lays out a graph; but a README's opening often wants something more posed than a layout. LaTeX's smartdiagram package typesets a handful of fixed pictures —a planet amid its satellites, a flow of boxes, a column of terms and their descriptions— that suit it better than any graph. :smartdiagram TYPE hands the very same list to smartdiagram instead; C-c C-c renders it with pdflatex, and LaTeX export typesets it in place:

graph
  • TYPE is one of constellation, connected-constellation, bubble, flow, flow:horizontal, circular, circular:clockwise, descriptive, priority-descriptive, or sequence.
  • A constellation or bubble has one top item, its centre; that item's children orbit it. A descriptive diagram's items are + TERM :: DESCRIPTION. Every other type reads the top items, in order.
  • The #+graph: rows are \smartdiagramset's, in smartdiagram's own words: | planet color | black!75 |.
  • An item's | color |, | fontcolor |, and | fontsize | rows —Graphviz's names for them— colour its shape and its text, and size its text; its :tags: splice #+name: templates of those rows, as in any graph. An item's later paragraphs sit beneath its text.
  • The title sits above the diagram; prose before the list, between the two; prose after it, beneath.
  • :dpi N sets the PNG's resolution, 300 by default. C-u C-c C-c shows the LaTeX.
  • C-M-i and eldoc know which kind of graph they are in: under :smartdiagram, they offer smartdiagram's keys, with their defaults, rather than Graphviz's.
  • Graphviz's own syntax ---#+node:, #+edge:, #+rank:, links between items, records— draws nothing here, so it is an error rather than silently dropped.

11.1. Org's markup in place of LaTeX's

Why bother, when LaTeX could draw it directly? The diagram above began life as a #+begin_src latex-as-png block, via ob-latex-as-png. Every LaTeX incantation it needed has an Org counterpart that we already write daily:

LaTeX Org
\smartdiagram[constellation diagram]{…} :smartdiagram constellation, over a nested list
\smartdiagramset{planet color=black!75, …} a #+graph: table, one row a key
\newcommand{\old}[1]{\textcolor{black!70}{\texttt{#1}}} #+macro: old [[color:gray][=$1=]]
\texttt{(:k plist)}, \textbf{…} =(:k plist)=, *…*
\mbox{\texttt{…}}, lest code break mid-circle nothing: code never breaks
\textcolor{black!70}{replaces} [[color:gray][replaces]]
{\color{white}…} on the planet a fontcolor row of white
\centering{One syntax … \\ …} above the diagram the block's title
\vspace{1em}{\textcolor{black!44}\hrule}\vspace{1em} -----
\hspace{7em} (map-elt :school), line by line a #+begin_src block, indented as we read it
% \new{(N xs)} \replaces … # + =(N xs)= …
\resizebox, \vspace, {\color{white} Bye!} padding nothing: the image is cropped to a small border

The LaTeX column is a second language inside our Org file: its macros, its colour syntax, and hand-tuned \hspace's that drift the moment a line changes. The Org column is the language the rest of the file is already in; it folds, links, exports, and completes like everything around it. The only backslashes that survive the move are \\ —which Org itself reads as a line break— and the font names, such as \small, that smartdiagram's own settings take as values.

12. Comforts while editing

Graphviz has 177 attributes, and nobody remembers them. So the block helps as we type, wherever we type.

  1. C-M-i (completion-at-point) and eldoc know about graph keys —in any Org buffer's graph blocks, and in the org-graph-edit-mode buffer that C-c ' opens on one, as for a source block:
    • In a key cell, completion offers the attributes valid there, plus the reserved keys; after :edge. or :edge[T]., it offers only edge attributes, prefix and all. In a value cell, it offers the values of that row's attribute, such as vee for :edge.arrowhead. Each candidate is annotated with its description.
    • On the #+begin_graph line, :r C-M-i offers :rankdir and the other graph attributes, every :node.KEY and :edge.KEY, and the block's own :file, :svg, :directed, and :C-c_C-c; :rankdir L C-M-i then offers LR.
    • Eldoc shows the description of the key on the current row, or at point on the #+begin_graph line, ending with Graphviz's default; e.g., | :edge.arrowhead | vee | shows “Style of arrowhead on the head node of an edge. Default: normal.”
  2. When we would rather pick than type, C-u RET on any table row inside a graph inserts a new | key | value | row interactively: a completion menu for the key, then one for the value —or a free-text prompt, when the attribute accepts arbitrary strings. Plain RET is unaffected. The menu shows only the attributes valid where the table sits:

    • Table under #+graph: ⟶ graph-level attributes (rankdir, splines, bgcolor, …)
    • Table under #+node: ⟶ default node attributes (shape, style, color, …)
    • Table under #+edge: ⟶ default edge attributes (arrowhead, dir, penwidth, …)
    • Table on a list item ⟶ node attributes, as above

    For a colour ---bgcolor, fillcolor, fontcolor, or color— the value is picked from Helm's colour list, when Helm is installed; RET inserts the colour's hex code, and its name as a comment. (This replaces our old ritual of M-x helm-colors, then C-z, then C-c r.) C-u RET works in the C-c ' buffer too.

  3. Where do the descriptions come from? org-graph-attributes, which lives on its own in org-graph-attributes.el: every Graphviz attribute, scraped from Graphviz's attribute table. To add or adjust one, edit its entry there, or M-x customize-variable RET org-graph-attributes; each entry has the form:

    (:name "shape" :valid-on (nodes) :default "ellipse"
     :values ("box" "polygon" "ellipse" …)  ;; nil → free-text prompt
     :description "Sets the shape of a node.")
    

    Reserved keys are described by org-graph--reserved-key-docs, and the header's own keys by org-graph--header-key-docs.

  4. When the picture is wrong, we want to see what we built. C-u C-c C-c shows the generated dot in a *org-graph dot* buffer instead of rendering a PNG, and inserts —or replaces— a #+RESULTS: holding it as a #+begin_src dot block, so the source is preserved. For a block where we always want dot, :C-c_C-c dot in its header makes plain C-c C-c show dot, and C-u C-c C-c toggles back to PNG: the prefix always flips whatever the block's default is.
  5. The image C-c C-c renders is clickable. Hovering over a node or cluster shows its tooltip —its footnotes and notes— and a click follows it: to its link's target, when its item has a web, file, or [[*Heading]] link; else to the item itself, in the #+begin_graph block. Graphviz writes an image map (-Tcmapx) beside the PNG, from the same layout, and its areas become the image's :map; it is scaled as the image is, say by org-image-actual-width. The map lives only as long as the image does: an image Org redisplays itself, say on reopening the file, is clickable again after another C-c C-c.
  6. Big graphs make big PNGs; (setq org-image-actual-width 700) keeps them on screen.

13. Reference

Once the demo is over, this is the page we keep open.

13.1. Syntax at a glance

Construct Dot equivalent
#+begin_graph TITLE graph [ label=TITLE ]
#+begin_graph :KEY V graph [ KEY=V ]; also :node.KEY V and :edge.KEY V; V needs no quotes
#+begin_graph :directed nil graph { … }, with -- arcs
#+name: N above #+begin_graph renders to N.png: a figure that [[N]] reaches, and #+caption: captions
* HEADING :graph:, with :PROPERTIES: the section's text is a graph; :RANKDIR: LR is :rankdir LR, and so on
:NAME: … :END: drawer the directives inside, folded away
#+graph: table graph [ … ]
#+node: table node [ … ]
#+edge: table edge [ … ]
#+rank: n1 [[n 2]] { rank=same; n1; "n 2" }
#+name: T + table style template T, used via :T: tags or :inherit rows; in or out of graphs
top-level - :T: :: MEANING a legend entry: a sample of template T, captioned MEANING
#+begin_cluster TITLE :KEY V subgraph cluster_TITLE { label=TITLE KEY=V … }
+ NAME list item node NAME declaration
indent + CHILD under + PARENT edge PARENT → CHILD
+ L :: CHILD under + PARENT edge PARENT → CHILD, labelled L
1. A, 2. B under + PARENT edges PARENT → A, B; { rank=same; A; B }, in that order
+ <<ID>> NAME node ID, labelled NAME
+ <<<NAME>>> node NAME; every item mentioning NAME arcs to it
+ NAME [[T]], + NAME [[T][L]] edge NAME → T, labelled L
+ NAME :T₁:T₂: node NAME, with templates T₁ and T₂ spliced in
+ NAME [fn::NOTE] node NAME, with tooltip=NOTE
a later paragraph of + NAME a note: NAME's tooltip, or beneath its label under :notes inline
+ NAME \\ MORE node NAME MORE, its label broken over two lines; likewise in table cells
+ NAME [[https://…]], [[file:x.png]] node NAME, with URL="https://…", or image="x.png"
+ [X] NAME, [-], [ ] node NAME, with template checked, partial, or unchecked, if defined
{{{M}}}, after #+macro: M V V, anywhere in the block
*b* /i/ _u_ +s+ =c= ~p~ an HTML-like label: <B>, <I>, <U>, <S>, monospace
∣ key ∣ value ∣ node attribute; values are quoted for you
∣ :id ∣ alias ∣ cross-reference name for this node
∣ :edge.KEY ∣ value ∣ attribute of the arc from the parent; each KEY at most once
+ [[T]] under + PARENT edge PARENT → T, a node declared elsewhere; record::port targets a port
∣ :edge[T].KEY ∣ value ∣ attribute of this item's arc to T, from a [[T]] link; see Strictly tables
∣ :inherit ∣ T ∣, ∣ :edge.:inherit ∣ T ∣ inherit template T's rows; explicit rows win
∣ :children-rank ∣ same ∣ { rank=same; <immediate children> }
∣ shape ∣ cluster ∣ subgraph cluster_ID { … } around the children; see Strictly tables
#+record: table under + NAME node NAME, labelled by that table; <<P>> in a cell is port P

13.2. Strictly tables: :edge[T].KEY and | shape | cluster |

Some of us prefer every attribute in a key-value table, never in the list's shape. Two spellings serve that taste; the examples above avoid them, but they are fully supported.

An inline link's arc is styled by :edge[T].KEY, naming the arc by where it goes; :edge[T].:inherit applies a template to it. A row with no [[T]] link in the item's text is an error:

+ <<lam>> λ-calculus
+ Lisp borrows
  | :edge[lam].style | dotted |
Lisp -> lam [ label=borrows style=dotted ];

A | shape | cluster | row makes an item a cluster, as #+begin_cluster does. Its children are then its members rather than arc targets, so arcs out of them are inline links, styled as above:

+ Emacs
  + Packages
    | shape | cluster |
    + Org
    + Magit
graph [ compound=true ]
subgraph cluster_Packages { label=Packages Org; Magit; }
Emacs -> Org [ lhead=cluster_Packages ];

Generated by Emacs and Org-mode (•̀ᴗ•́)و
Creative Commons License
Life & Computing Science by Musa Al-hassy is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License