About this package#
A mind map the way it grows on a whiteboard: one term in the middle, the headings to its right and left, the examples behind them. brainroot sets such a map from a nested list. Every branch gets a colour it passes down to its leaves; the boxes are filled with a lighter shade of that colour, the connections run as soft curves.
The package builds on CeTZ. Boxes are measured, so they follow the font of the surrounding document.
Quick start#
#import "@preview/brainroot:0.2.0": brainroot, branchThe simplest input is a list: every item becomes a node, indented items its children.
#brainroot(title: [Forms of energy])[
- Kinetic energy
- Motion
- Wind
- Elastic energy
- Stretching
- Thermal energy
- Fire
- Potential energy
- Gravity
- Weight
]Three marks in the list carry node options: a Typst label <name> at the end gives the node its id for cross-links, an item that is nothing but *bold* highlights it, one that is nothing but _emphasised_ becomes a gap.
#brainroot(title: [Photosynthesis], links: (connect("light", "dark", label: [ATP]),))[
- Light reaction <light>
- *Photolysis*
- _ATP_
- Dark reaction <dark>
- Calvin cycle
- _Glucose_
]To describe a branch in more detail, write it as branch(label, ..children). A child is then either content, a leaf, or another branch(...). A list and branch calls may stand side by side; without title the first argument is the root.
Sides and colours#
Left alone, brainroot distributes the branches itself: the first ones go right until the right side is about half as tall as all branches together, the rest go left. side: left or side: right pins a branch, color gives it its own colour instead of the next one from the palette.
#brainroot(title: branch([Photosynthesis], fill: green.lighten(50%)), spacing: (max-width: 3cm),
branch([Light reaction], branch([Photolysis], [Water is split]), [ATP], side: right),
branch([Dark reaction], [Calvin cycle], [Glucose], color: purple),
branch([Requirements], [Light], [Water], [CO₂], side: left),
)Nodes#
A label is content: formulas, images and links work as they do anywhere. An icon sits left of the label or, with icon-at: "top", above it. The root is a branch too: title: branch([Energy], icon: ..., fill: ...). Per node you can set fill (a colour, or none for a ring), ink, and mark: true for a highlighted key term.
#brainroot(title: branch([Energy], icon: text(size: 1.6em, emoji.bolt), icon-at: "top"),
branch([Formula], [$E = 1/2 m v^2$], [$E = m g h$], icon: emoji.abacus),
branch([Link], link("https://typst.app")[typst.app]),
branch([Important], branch([Key term], mark: true), branch([Ring], fill: none), branch([Red], ink: red)))Gaps and the solution#
blank: true on a branch draws its box empty, at full size. blanks:,
"leaves""branches" or "all" does that for a whole class of nodes. The same map with solution: true fills the gaps in, with solution-ink in a colour that makes the answers stand out. Task and solution come from one source.
#brainroot(title: [Forms of energy], blanks: "leaves", map)
#brainroot(title: [Forms of energy], blanks: "leaves", solution: true, solution-ink: red, map)Edge labels#
edge-label puts a small label on the edge that leads to a node. That turns the mind map into a probability tree or a decision tree. equal: true on a branch makes its children the same size, "width" or "height" in one direction; on the root it applies to the first level.
#brainroot(title: branch([Start], equal: true), layout: "right", theme: "outline", palette: "plain",
branch([Heads], branch([Heads], edge-label: $1/2$), branch([Tails], edge-label: $1/2$), edge-label: $1/2$, equal: true),
branch([Tails], branch([Heads], edge-label: $1/2$), branch([Tails], edge-label: $1/2$), edge-label: $1/2$, equal: true))Cross-links, braces, clouds#
A mind map is a tree; the ideas in it rarely are. id names a node, connect(from, to) in links draws a curve between two nodes over the map, with label, arrow, dash and bend. The root is called "root". summary puts a labelled brace beyond a node’s children, cloud lays a cloud behind its subtree. Braces and clouds exist in the tree layouts, not in radial and star. arrange: "links" orders the branches and turns children around so that linked nodes come close together; here CO₂ moves next to glucose.
#brainroot(title: [Photosynthesis], arrange: "links",
links: (connect("light", "dark", label: [ATP, NADPH]), connect("co2", "glucose", label: [C], dash: "dotted")),
branch([Light reaction], [Photolysis], [ATP], [NADPH], id: "light", cloud: true),
branch([Dark reaction], [Calvin cycle], branch([Glucose], id: "glucose"), id: "dark", summary: [products]),
branch([Requirements], [Light], [Water], branch([CO₂], id: "co2"), summary: [from outside]))Points and building up#
points on a branch counts for grading a map; brainroot-points with the same arguments adds them up, show-points: true shows them as a badge on the box. reveal draws only the first branches, or those for which a function of the index is true. The layout stays put, so a map builds up branch by branch, in typstage with build(from => brainroot(..., reveal: i => from(i + 2)), steps: 5).
#brainroot(title: [Forms of energy], reveal: 2, show-points: true,
branch([Kinetic energy], branch([Motion], points: 1), branch([Wind], points: 1)),
branch([Elastic energy], branch([Stretching], points: 2)),
branch([Thermal energy], [Fire]))Layouts#
layout decides how the branches sit around the root. both is the two-sided map from above, right and left put everything on one side. down and up set a tree from top to bottom or the other way round, like an org chart. radial is the classic Buzan mind map: the whole tree fans out from the root, every subtree in a sector of its own. star only puts the branches on a circle around the root, their subtrees grow horizontally outward. fishbone is the Ishikawa cause-and-effect diagram: the root as the head of a spine, the branches as ribs alternating above and below, the leaves along the ribs; two levels below the root. layout also takes a dictionary: (kind: "down", align-levels: true) puts every level on one line in the tree layouts, as in an org chart; start is the angle of the first branch in radial and star. Distances live in spacing, see spacing-defaults.
#brainroot(title: [Forms of energy], layout: "down", map)#brainroot(title: [Forms of energy], layout: "radial", map,
branch([Pressure]), branch([Radiation], [Light]))#brainroot(title: [Forms of energy], layout: "star", map,
branch([Pressure]), branch([Radiation], [Light]))#brainroot(title: [Bad grade], layout: "fishbone", theme: "outline",
branch([Preparation], [started late], [no plan], [no practice]),
branch([Lessons], [absent], [no notes]),
branch([Exam], [time misjudged], [task misread]),
branch([Setting], [noise], [tiredness]))#brainroot(title: [Substances], layout: (kind: "down", align-levels: true), theme: "blocks",
branch([Pure substances], branch([Elements], [Metals], [Non-metals]), [Compounds]),
branch([Mixtures], [homogeneous], [heterogeneous]))With radial and star the first branch sits at start (default 60deg), the others follow clockwise. With radial the children share their parent’s sector, weighted by the size of their subtrees, and sit on the ring of their depth; the rings begin at spacing.root and are stretched until no two boxes overlap.
Palettes#
palette provides the colours of the branches and the root; the boxes get the branch colour lightened by tint. Ten palettes are built in, all shown with the same map:
poster#
Bright and bold, like markers on a whiteboard. The default.
#brainroot(title: [Forms of energy], palette: "poster", map)pastel#
Soft, muted tones.
#brainroot(title: [Forms of energy], palette: "pastel", map)grayscale#
Greys only, for black-and-white printing.
#brainroot(title: [Forms of energy], palette: "grayscale", map)mono#
One blue in varying lightness.
#brainroot(title: [Forms of energy], palette: "mono", map)plain#
One dark ink for everything, as if drawn with a fountain pen.
#brainroot(title: [Forms of energy], palette: "plain", map)earth#
Earth tones: terracotta, ochre, olive, sand.
#brainroot(title: [Forms of energy], palette: "earth", map)ocean#
Sea: turquoise, teal, sea green.
#brainroot(title: [Forms of energy], palette: "ocean", map)sunset#
Evening sky: red, orange, pink, violet.
#brainroot(title: [Forms of energy], palette: "sunset", map)forest#
Forest: green with a little brown.
#brainroot(title: [Forms of energy], palette: "forest", map)neon#
Loud, saturated colours.
#brainroot(title: [Forms of energy], palette: "neon", map)Your own colours go in as an array, palette: (red, blue, green), or with a root colour as a dictionary, palette: (colors: (red, blue), root: black). A palette may also set ink, ink-dark, ink-light and ink-threshold, the text colours; base takes a built-in palette as the starting point: palette: (base: "ocean", root: black). The root itself is coloured by title: branch([...], fill: ...).
Themes#
A theme decides how boxes and edges look; the colours still come from the palette. Ten are built in:
soft#
Pastel boxes with rounded corners, soft S-curves. The whiteboard original.
#brainroot(title: [Forms of energy], theme: "soft", map)outline#
White boxes with a coloured border, curves.
#brainroot(title: [Forms of energy], theme: "outline", map)blocks#
Solid square boxes with white text, right angles: org-chart look.
#brainroot(title: [Forms of energy], theme: "blocks", map)lines#
No boxes: the text sits on its coloured line and the edges flow into it. The classic mind map.
#brainroot(title: [Forms of energy], theme: "lines", map)sketch#
Thin border without fill, dashed straight lines.
#brainroot(title: [Forms of energy], theme: "sketch", map)bubbles#
Pills with pastel fill and straight connections.
#brainroot(title: [Forms of energy], theme: "bubbles", map)hand#
Like soft, but hand-drawn: every line wobbles slightly.
#brainroot(title: [Forms of energy], theme: "hand", map)scribble#
Scribbled: no fill, every line drawn twice.
#brainroot(title: [Forms of energy], theme: "scribble", map)marker#
Felt-tip: solid colour, wide straight strokes with a long wobble.
#brainroot(title: [Forms of energy], theme: "marker", map)pencil#
Pencil: thin lines with a fine tremor, right angles.
#brainroot(title: [Forms of energy], theme: "pencil", map)organic#
Organic after Buzan: branches that thin out towards the leaves, pastel pills.
#brainroot(title: [Forms of energy], theme: "organic", map)twigs#
Twigs: white circles on the first level, bare leaves on a shared spine with a twig each, the infographic look.
#brainroot(title: [Forms of energy], theme: "twigs", map)Shapes#
The theme field shape turns the boxes into circles or ellipses; size gives a fixed diameter per depth, as in a bubble tree. With a fixed diameter the font shrinks to 60% so the text fits; if that is not enough the disc grows. Without size, circles suit short labels, a long word makes a large disc. shade steps the branch colour per level.
#brainroot(title: [Energy], layout: "radial", palette: "sunset",
theme: (fill: "solid", shape: "circle", size: (5em, 4em, 2.8em), edge: "straight",
thickness: (0.5em, 0.25em), scale: (1.1, 0.9, 0.7), shade: 25%))[ ... ]#brainroot(title: [Mind Map], theme: (base: "outline", shape: "ellipse"), palette: "ocean", map)Hand-drawn#
The themes hand, scribble, marker and pencil wobble every line after the pattern of the TikZ decoration sketch: the path is walked in small steps, every point is offset perpendicular to it by a slowly running sine wave whose rhythm is set by a random walk. The randomness is reproducible, the same map looks the same on every compile. A handwriting font such as “Patrick Hand” or “Kalam” suits it; set it with set text(font: ...) or the theme field font.
wobble sets the strength: wobble: 0.5 wobbles half as much, wobble: 2 twice. The field hand controls the wobble in detail: amplitude (excursion in pt), wavelength (in pt), randomness (irregularity, 1 is a pure sine), segment (step in pt) and passes (how often each line is drawn). Any theme can be made hand-drawn with it:
#set text(font: "Patrick Hand")
#brainroot(title: [Forms of energy],
theme: (base: "blocks", hand: (amplitude: 1, wavelength: 60, randomness: 2, segment: 1.5, passes: 1)),
map)Adapting a theme#
A dictionary overrides individual fields; base picks the starting theme, otherwise soft. A theme carries everything about the look: boxes (shape, size, fill, stroke, radius, inset, underline, font, scale, bold-depth, tint, tint-min, shade), edges (edge, thickness, dash, taper, edge-label-fill), hand for the wobble, and root and branches with overrides for the root and the first level only. theme-defaults lists every field with its default; a misspelt field is an error, not a silent nothing.
#brainroot(title: [Forms of energy], layout: "right",
theme: (base: "outline", edge: "elbow", radius: 0pt), map)The four levels#
Everything that shapes a map sits on one of four levels, and each has its place:
- Theme is the look of boxes and edges: shape, fill, border, font sizes per level, line widths, colour steps, the wobble. A name or a dictionary, fields in
theme-defaults. - Palette assigns the colours: of the branches, the root, the text. A name, an array of colours or a dictionary.
- Layout is the arrangement,
spacingthe distances:levelandrootalong the direction of growth,siblingandbranchacross,max-widthfor wrapping,brace,summary,cloud,label(how far an edge label sits off its edge) andpadding. Fields inlayout-defaultsandspacing-defaults. - Nodes are
branch(...), the root included: icon, fill, ink, highlight, gap, edge label,id, brace, cloud, points.
What stays on brainroot itself are the knobs per map: wobble, links, blanks, solution, solution-ink, show-points, reveal, width, zoom, background and alt. width scales the finished map, text included, to a width given as a length or as a share of the surrounding block (width: 100%); zoom is a factor on top. Both change only the size, never the layout.
Whatever is the same throughout a document becomes a preset:
#let map = brainroot.with(theme: "hand", palette: "ocean", spacing: (level: 5em))
#map(title: [Forms of energy])[ ... ]Accessibility and performance#
The map is a figure with alternative text: alt: auto writes the tree out as text, a string is used as given, none leaves it out. Tagged PDFs thus carry the content of the map for screen readers too.
A map of about 200 nodes compiles in a little over half a second, hand-drawn in about two; measuring the boxes and wobbling the lines are the two costs. Very large maps do not get slow, then, but they get hard to read, and that is the reason to split them.
Functions#
brainroot#
brainroot(
..branches,
title: none,
layout: "both",
theme: "soft",
palette: "poster",
spacing: (:),
wobble: 1,
links: (),
arrange: "keep",
blanks: none,
solution: false,
solution-ink: auto,
show-points: false,
reveal: auto,
width: auto,
zoom: 100%,
background: none,
alt: auto,
) -> contentDraws the mind map. The first-level branches come as positional arguments: branch(...) calls, plain content, or a Typst list whose items become branches and whose nested lists become children. In a list, <name> at the end of an item gives the node an id, an item that is nothing but *bold* marks it, one that is nothing but _emphasised_ makes it a gap.
Recurring settings go into a preset: #let map = brainroot.with(theme:.
"hand", palette: "ocean")
| Name | Typ | Vorgabe | Beschreibung |
|---|---|---|---|
..branches | content | dictionary | – | First-level branches: branch(...), content, or a list. Without title, the first positional argument is the root. |
title | content | dictionary | none | none | The root: content, or a branch(...) to give it an icon, a fill or an ink of its own. |
layout | str | dictionary | "both" | Arrangement of the branches: a name (both, right, left, down, up, radial, star, fishbone) or a dictionary with kind and the further fields of layout-defaults. |
theme | str | dictionary | "soft" | How boxes and edges look: the name of a theme in themes, or a dictionary that overrides fields of one (base: picks the starting theme, otherwise soft); see theme-defaults for the fields. |
palette | str | array | dictionary | "poster" | The colours: the name of a palette in palettes, an array of colours, or a dictionary (base: picks the starting palette) with colors, root, ink, ink-dark, ink-light and ink-threshold. |
spacing | dictionary | (:) | Distances, overriding fields of spacing-defaults. |
wobble | float | ratio | 1 | Strength of the wobble in hand-drawn themes, a factor on their amplitude; 0 draws straight, 2 twice as restless. |
links | array | () | Cross-links between nodes, each a connect(...). |
arrange | str | "keep" | "keep" draws the branches in the order given; "links" orders the first-level branches, and turns the children of a branch around where that helps, so that nodes joined by cross-links come close together. Tree layouts only; up to seven branches are tried exhaustively, more by swapping neighbours. |
blanks | none | str | none | Draws whole classes of nodes as gaps: "leaves", "branches" (the first level) or "all"; none only honours each node’s own blank. |
solution | bool | false | true fills the gaps in: the solution of a map with blanks. |
solution-ink | auto | color | auto | Text colour for filled-in gaps, so the solution stands out; auto uses the normal text colour. |
show-points | bool | false | Shows each node’s points as a badge on its box. |
reveal | auto | int | function | auto | Which first-level branches are drawn: auto all, an integer the first so many, or a function of the branch index (from 0) returning a bool. The layout stays the same, so a map can build up branch by branch – in typstage: build(from => brainroot(..., reveal: i => from(i + 2))). |
width | auto | length | ratio | auto | auto draws the map at its natural size; a length or a ratio of the surrounding block scales the whole map, text included, to that width. |
zoom | ratio | float | 100% | A factor on the whole map, applied on top of width; zoom: 50% halves it. |
background | color | none | none | A colour behind the whole map, framed by spacing.padding; none leaves the page as it is. |
alt | auto | str | none | auto | Alternative text for the map in tagged PDFs: auto writes the tree out as text, a string is used as given, none adds nothing. |
contentbrainroot-points#
brainroot-points(..branches, title: none) -> int | floatpoints of every node in a map. Takes the same branches, lists and title as brainroot(), so the call can be repeated with the same arguments, or the arguments kept in a variable and spread.| Name | Vorgabe |
|---|---|
..branches | – |
title | none |
int | floatbranch#
branch(
label,
..kids,
color: none,
side: auto,
icon: none,
icon-at: "left",
fill: auto,
ink: auto,
mark: false,
blank: false,
edge-label: none,
id: none,
summary: none,
cloud: none,
points: none,
equal: false,
) -> dictionarybranch(...) calls or plain content, which then counts as a leaf without children of its own.| Name | Typ | Vorgabe | Beschreibung |
|---|---|---|---|
label | content | str | – | Label of the node. |
..kids | content | dictionary | – | Children: branch(...) calls or content (leaves). |
color | color | none | none | Colour of the branch. Only read on the first level; below it every node inherits the colour of its parent. none takes the next colour from the palette. |
side | alignment | auto | auto | left or right forces the side in the two-sided layout; auto lets brainroot balance. Only read on the first level. |
icon | content | none | none | An icon, emoji or image set beside the label. |
icon-at | str | "left" | Where the icon goes: "left" of the label or "top", above it. |
fill | auto | color | none | auto | Fill of this node’s box; auto follows the theme, none leaves the box unfilled with a border in the branch colour (a ring). |
ink | auto | color | auto | Text colour of this node; auto follows the fill. |
mark | bool | false | Highlights the node: bold text and a strong border in the branch colour, for key terms. |
blank | bool | false | Draws the box empty, at its full size, unless the map is set with solution: true – a gap to fill in. |
edge-label | content | none | none | A small label on the edge that leads to this node, for decision trees and probability trees. |
id | str | none | none | A name for connect(...) to address this node by. The root is "root". |
summary | content | none | none | A brace beyond this node’s children with a label, summarising them. Not drawn in the radial and star layouts. |
cloud | bool | color | none | none | A soft cloud behind this node’s whole subtree: true for a light tint of the branch colour, or a colour. Not drawn in the radial and star layouts. |
points | int | float | none | none | Points this node is worth when a map is graded; brainroot-points() adds them up and show-points: true shows them as a badge. |
equal | bool | str | false | Makes this node’s children the same size: true in width and height, "width" or "height" in one of them. Every child grows to the largest sibling, so a row of leaves lines up. |
dictionaryconnect#
connect(
from,
to,
label: none,
arrow: true,
dash: "dashed",
bend: auto,
color: auto,
thickness: 0.09em,
) -> dictionaryid and pass the connections to brainroot(links: (...)).| Name | Typ | Vorgabe | Beschreibung |
|---|---|---|---|
from | str | – | id of the node the curve starts at; "root" is the root. |
to | str | – | id of the node the curve ends at. |
label | content | none | none | A label at the middle of the curve. |
arrow | bool | str | true | An arrowhead at the end; "both" puts one at each end. |
dash | str | "dashed" | Dash pattern of the curve. |
bend | auto | ratio | auto | How far the curve bows out, as a share of the distance: auto bows away from the root, a ratio bows to the left of the direction of travel, negative to the right, 0% is a straight line. |
color | color | auto | auto | Colour of the curve; auto is a dark grey. |
thickness | length | 0.09em | Line width. |
dictionaryDefaults#
The fields of theme, palette, layout and spacing, with their defaults:
layout-defaults#
dictionarykind is one of both, right, left, down, up, radial, star, fishbone; start the angle of the first branch in radial and star; align-levels puts every level on one line in the tree layouts.palettes#
dictionaryposter, pastel, grayscale, mono, plain, earth, ocean, sunset, forest, neon. Each sets colors (an array, handed out to the branches in order) and root; a palette may also set ink (auto picks by the fill’s luminance), ink-dark, ink-light and ink-threshold. Take one as the starting point for your own: palette: (base: "ocean", root: black).spacing-defaults#
dictionarylevel between parent and child, root between the root and its branches (the first ring in radial and star). Across it: sibling between siblings, branch between the first-level branches. max-width wraps longer labels. brace and summary size a summary brace and its gaps, cloud pads a cloud, label is the gap between an edge label and its edge, padding frames a coloured background. Lengths in em follow the font size around the map.theme-defaults#
dictionaryshape ("rect", "circle", "ellipse"), size (a fixed diameter per depth, or none), fill ("tint", "solid", "white", "none"), stroke (border width), radius, inset, underline (text on a line instead of a box), font, scale (font size per level), bold-depth, tint and tint-min (how the branch colour is lightened for the boxes), shade (colour steps per level). Edges: edge ("curve", "elbow", "straight", "taper", "comb"), thickness (per level), dash, taper (factors at parent and child), edge-label-fill. hand is none or a dictionary with amplitude, wavelength, randomness, segment and passes for hand-drawn lines. root and branches override any of the box fields for the root and the first level only.themes#
dictionarysoft, outline, blocks, lines, sketch, bubbles, hand, scribble, marker, pencil, organic, twigs. A theme is a dictionary of everything that decides how boxes and edges look; each preset sets only what differs from the defaults, see theme-defaults for the full list of fields. Adapt one with theme: (base: "hand", hand: (amplitude: 1)).