A Typst package for drawing beautiful tidy tree easily
This package uses fletcher to render and customize nodes and edges
Getting Started
Import the package using:
#import "@preview/tdtr:0.2.0" : *
From list
The most common case is to draw a tree directly from a bullet list.
Typical structure of the bullet list is:
- Parent node
  - Child node 1
    - Grandchild node 1
    - Grandchild node 2
  - Child node 2
where there must be only one root node (the top-level bullet list item), and each bullet list item represents a node in the tree, and the indentation level represents the parent-child relationship between nodes.
Here is an example:
#tidy-tree-graph(compact: true)[
  - $integral_0^infinity e^(-x) dif x = 1$
    - `int main() { return 0; }`
      - Hello
        - This
        - Continue
        - Hello World
      - This
    - _literally_
      - Like
    - *day*
      - tomorrow $1$
]
where compact: true option will try its best to make the tree more compact by reducing the space between nodes, however, it may cause overlapping nodes in some cases.
Another common case is that you may want to specify the labels of some edges, and then you can add a numbered list item before the child bullet list item (the item pointed by the edge) to specify the label of the edge.
Typical structure of the bullet list with edge labels is:
- Parent node
  + Edge label 1
  - Child node 1
    + Edge label 2
    - Grandchild node 1
//    + Edge label 3
    - Grandchild node 2
  + Edge label 4
  - Child node 2
where each numbered list item represents an edge label, and it’s optional if you don’t want to label the edge.
Here is an example:
#tidy-tree-graph(
  spacing: (20pt, 20pt),
  node-inset: 4pt
)[
  - $I_0$
    + $E$
    - $I_1$
      + $+$
      - $I_6$
        + $T$
        - $I_9$
          + $F$
          - $I_7$
        + $F$
        - $I_3$
        + $a$
        - $I_4$
        + $b$
        - $I_5$
    + $T$
    - $I_2$
      + $F$
      - $I_7$
        + $*$
        - $I_8$
      + $a$
      - $I_4$
      + $b$
      - $I_5$
    + $F$
    - $I_3$
      + $*$
      - $I_8$
    + $a$
    - $I_4$
    + $b$
    - $I_5$
]
where spacing: (20pt, 20pt) option specifies the horizontal and vertical spacing between nodes, and node-inset: 4pt option specifies the padding inside each node. They are passed to fletcher.diagram internally.
During the drawing of the tidy tree, the package compresses nodes horizontally by default to make the tree more compact and thus beautiful and tidy, which is why this package is provided.
Here is an example for a larger tree:
#tidy-tree-graph(
  draw-edge: tidy-tree-draws.horizontal-vertical-draw-edge
)[
  - Hello
    - World
      - How
        - Whats
          - Day
        - the
        - Time
          - Hello
            - World
              - How
                - Whats
                  - Day
                - the
                - Time
                  - Hello
      - This
      - Day
        - Hello
      - People
    - are
      - Hello
          - World
        - Day
          - Hello
          - World
          - Fine
          - I'm
          - Very
            - Happy
            - That
            - They
            - have
            - what
        - you
        - Byte
        - integer
        - Today
      - you
    - !
      - Fine
      - Day
      - You
        - World
        - This
    - Day One
      - doing
        - abcd
        - efgh
      - today
        - Tomorrow
        - Tomorrow
        - Tomorrow
    - Hello
      - Day
      - One
    - Fine
      - Hello
      - Fine
      - Day
    - Hello
]
where draw-edge: tidy-tree-draws.horizontal-vertical-draw-edge option specifies a pre-defined edge drawing function to draw edges in a horizontal-vertical manner.
From file
You can also draw import a tree from a file, supporting JSON and YAML formats, where every key and every value in the file represents a node in the tree.
Edge labels are not supported when importing from a file.
JSON
Here is an example of importing a tree from a JSON file:
test.json:
{
    "Hello": {
        "World": {
            "How": {
                "Whats": [
                    "Day",
                    "the",
                    1
                ],
                "the": {},
                "Time": {
                    "Hello": [
                        1, 2, 3, 4, 5
                    ]
                }
            }
        },
        "This": {
            "Hello": {}
        },
        "Day": {},
        "People": {}
    }
}
#tidy-tree-graph(json("test.json"))
YAML
Here is an example of importing a tree from a YAML file:
test.yaml:
app:
  server:
    host: localhost
    port: 8080
  database:
    user: 
      admin: admin
    password: 
      secret: kdi90gs78a7fgasad123gf70aa7ds0
#tidy-tree-graph(yaml("test.yaml"))
Note
- 
The json and yaml files should not contain any structure that an dictionary is included in an array, e.g.
{ "A": [ {"B": "C"} // this structure is not supported ], "B": [ "D" // this structure is supported ] }A: - B: C # this structure is not supported B: - D # this structure is supported 
Customization
You might think the default edge drawing style is not suitable for your case, and you can customize it by providing edge drawing function when creating the tidy tree:
#tidy-tree-graph(
  draw-node: tidy-tree-draws.default-draw-node,
  draw-edge: tidy-tree-draws.default-draw-edge
)[
  ...
]
Pre-defined drawing functions
This package provides several pre-defined drawing functions for nodes and edges, which are defined in tidy-tree-draws dictionary variable.
Default node and edge drawing functions are defined as follows:
#let tidy-tree-draws = (
  /// default function for drawing a node
  default-draw-node: (name, label, (i, j, k, x)) => 
    fletcher.node((x, i), [#label], name: name, shape: rect),
  // ...
  /// default function for drawing an edge
  default-draw-edge: ((from-name, from-label, (i1, j1, k1, x1)), (to-name, to-label, (i2, j2, k2, x2)), edge-label) => {
    if edge-label == none {
      fletcher.edge(from-name, to-name, "-|>")
    } else {
      fletcher.edge(from-name, to-name, "-|>", box(fill: white, inset: 2pt)[#edge-label], label-sep: 0pt, label-anchor: "center")
    }
  },
  // ...
)
where default-draw-node draws every node as a rectangle, and default-draw-edge draws every edge with an arrowhead, and if the edge has a label, it will be drawn inside a white box to avoid overlapping with the edge.
other node and edge drawing functions include but not limited to:
circle-draw-node: draw every node as a circlereversed-draw-edge: draw every edge in reversed directionhorizontal-vertical-draw-edge: draw every edge in a horizontal-vertical manner
Custom drawing functions
You can also define your own drawing functions for nodes and edges.
For draw-node, the function should have the following signature:
(
  name: label, 
  label: any, 
  position: (
    i: int, 
    j: int, 
    k: int, 
    x: int | float
  )
) -> fletcher.node
where
name: the unique label of the node, used for drawing edge, should not be changed, and should be used only byfletcher.node(..., name: name, ...)label: the content of the node, if the tree is from list, it’scontenttype; if the tree is from file, it’sstrtype, default used byfletcher.node(..., [#label], ...)position: a tuple representing the position of the node in the tree, whereiis the depth of the node,jis the index of the parent node ini - 1level,kis the index of the node among its siblings, andxis the x-coordinate of the node after horizontal compression, used for positioning the node, default used byfletcher.node((x, i), ...). Specially, the(i, j, k)of the root node is(0, 0, 0).- return value: should be a 
fletcher.nodeobject representing the drawn node 
For draw-edge, the function should have the following signature:
(
  from: (
    name: label, 
    label: any, 
    position: (
      i: int, 
      j: int, 
      k: int, 
      x: int | float
    )
  ), 
  to: (
    name: label, 
    label: any, 
    position: (
      i: int, 
      j: int, 
      k: int, 
      x: int | float
    )
  ), 
  edge-label: any) -> fletcher.edge
where
from: a tuple representing the starting node of the edge, with the same structure as the parameters ofdraw-nodeto: a tuple representing the ending node of the edge, with the same structure as the parameters ofdraw-nodeedge-label: the label of the edge, if the edge has no label, it’snonetype- return value: should be a 
fletcher.edgeobject representing the drawn edge 
API Reference
The main function provided by this package is tidy-tree-graph, which has the following signature:
(
  // main body of the diagram
  body: content | dictionary | array,
  // for customization of drawing functions
  draw-node: ..., // see above
  draw-edge: ..., // see above
  // make the tree more compact by reducing gaps between nodes
  // might cause overlapping nodes in some cases
  compact: bool = false,
  // the minimum relative gap while calculating the horizontal axis of nodes
  // do NOT use it unless you know what you are doing
  min-gap: int | float = 1,
  // set text(size: text-size)
  text-size: int | float = 8pt,
  // passed to fletcher.diagram
  node-stroke: stroke = .25pt,
  spacing: (length, length) = (6pt, 15pt),
  node-inset: length = 3pt,
  edge-corner-radius: length | none = none
) -> fletcher.diagram