4

Following up previous questions [1, 2], I would like to know how to stylize/format each node based on its level in terms of box style, text format and separation from its parent/child?

\documentclass[border=10pt,multi,tikz]{standalone}
\usepackage[edges]{forest}
\usepackage{calc}
\usetikzlibrary{arrows.meta}
\forestset{%
  declare dimen register={gap},
  gap'=20mm,
  declare dimen register={lbox width},
  lbox width=(\textwidth-2*\forestregister{gap})/3,
  rbox/.style = {draw=blue!80!black, fill=blue!20, rounded corners},
  lbox/.style = {align/.wrap pgfmath arg={@{}p{##1 pt}@{}}{(lbox_width)}},
}
\begin{document}
\begin{forest}
  forked edges,
  for tree={%
    font = \sffamily,
    edge = {draw, -{Latex}},
  },
  where n children=0{%
    lbox,
    no edge,
  }{%
    rbox,
  }
  [Some Text, name=Start, l sep+=5pt
    [Text Text, name=Block1
      [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
      ]
    ]
    [Text, name=Block2
      [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
      ]
    ]
    [Text Text Text, name=Block3
      [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
      ]
    ]
  ]
\end{forest}
\end{document}

enter image description here

12

There are actually multiple ways of styling nodes according to their level. In some cases, the way in which one change is made might alter the ways in which other changes need to be made.

A couple of bits of useful background on how Forest works.

  • Forest has a notion of stages. Within each stage, Forest works through one or more cycles. By default, Forest works through the following stages. However, stages can be redefined, eliminated or added, although this is not usually necessary.

    1. Initially, the tree is parsed. However, only some keys are immediately taken into account. The content for each node is saved and any initial options are applied. E.g. prooftrees defines additional stages which are inserted in between the appropriate default stages and does some other funny stuff to make things work.

    2. The nodes are typeset. These are individual nodes. They aren't actually typeset in the sense of producing any output. Rather, the typeset node is saved in a box. (Everything is a box, after all.)

    3. The nodes are packed, taking account of any relevant customisation. This involves Forest figuring out where to place each node in relation to its parent. (In terms of Forest's l/s coordinate system.)

    4. The positions of each node are now calculated in absolute TikZ terms i.e. in regular x/y coordinates.

    5. The tree is drawn or, optionally, saved into a box for later use.

For most purposes, the details of this don't matter, but it is helpful to know in general terms what happens because it is sometimes necessary to delay application of an option until a later cycle within a stage or until a later stage.

Suppose, for example, we want to draw the following tree.

final tree

We could just use label for each node. However, it would be more convenient to put the label into the node itself and have Forest turn it into a label.

We might, therefore, try something like this.

\begin{forest}
  for tree={%
    label/.option=content,
    content=,
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}

These are the initial letters of the Welsh alphabet. (Note to Alan Munn: in the important sense of 'letter'.)

But this would not give us what we might expect.

unexpected

The problem is that label is set to the content of the node before Forest has set that content. It needs to be delayed until Forest has been through the tree once to save the content. Only then should the label be set and the content disposed of.

There are various ways of doing this. The simplest, though not always the most robust, is delay, which tells Forest to delay execution for a cycle.

\begin{forest}
  for tree={%
    delay={%
      label/.option=content,
      content=,
    },
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}

This produces a much better result.

much better

We'd like the labels for the bottom level to go below the nodes. These are the nodes with no children so we can test whether a node has children and put the label below if not. Changing our delay={} to

    delay={%
      if n children=0{%
        label/.wrap pgfmath arg={-90:#1}{content},
      }{%
        label/.option=content,
      },
      content=,
    },

we get a less cluttered result.

less cluttered

To space out the nodes better, we need to increase the distance between siblings in the bottom and middle levels. To do this, we increase s sep for these nodes' parents. We could just do this for the whole tree, but let's suppose we want to increase the separation by 10pt for the middle level and 5pt for the bottom level.

Then, for the last layer of nodes (which have no children), we can add the lines

        !u.s sep+=5pt,
        !uu.s sep+=10pt,

The ! tells Forest that it needs to 'walk' to a (possibly different) node before applying the option. u is the parent of the current node. uu is the parent of the parent of the current node. The + tells Forest to add to the existing value.

\begin{forest}
  for tree={%
    delay={%
      if n children=0{%
        label/.wrap pgfmath arg={-90:#1}{content},
        !u.s sep'+=5pt,
        !uu.s sep'+=10pt,
      }{%
        label/.option=content,
      },
      content=,
    },
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}

then produces the desired tree

final tree

OK. Let's suppose we instead want to draw this tree.

target tree 2

Basically, this tree is formatted according to level, as described in the question. To avoid unnecessary repetition, we can put common formatting into for tree. There's not much of this.

  for tree={%
    double,
    rounded corners,
  },

step 1 for target 2

The options don't have any visible effect, of course, because we haven't filled or drawn anything.

They are all using double and the rounded corners won't hurt the circles in the bottom level.

Now we start specifying formatting by level. The first level, which contains the root node, is level 0. The next is level 1 and so on. So the bluish stuff needs to apply to level zero.

  where level=0{% for the root node
    minimum size=20pt,
    draw=magenta,
    inner color=magenta!25,
    outer color=magenta,
    ultra thick,
  }{%
  },

Notice that hardly any of the formatting keys here are Forest-specific. But Forest passes anything it doesn't recognise to TikZ, so it will all work fine. The above tells Forest to apply the given options to nodes at level 0 (i.e. the root) and do nothing to the rest.

step 2 target 2

Now, we want to divide all the nodes which aren't at level zero, into the nodes at level one and those at level two.

where level=0{%
  <options for root>
}{%
  if level=1{%
    <options for level 1>
  }{%
    <options for level 2>
  },
},

Filling this out

  where level=0{% for the root node
    minimum size=20pt,
    draw=magenta,
    inner color=magenta!25,
    outer color=magenta,
    ultra thick,
  }{%
    if level=1{% middle nodes
      minimum size=15pt,
      semithick,
      draw=blue!50!cyan,
      inner color=blue!50!cyan!25,
      outer color=blue!50!cyan,
    }{% remaining nodes
      delay={%
        if n children=0{%
          label/.wrap pgfmath arg={-90:#1}{content},
          !u.s sep'+=5pt,
          !uu.s sep'+=10pt,
          content=,
        }{},
      },
      green!75!black,
      circle,
      fill,
      fill opacity=.5,
      draw opacity=1,
      draw,
      minimum size=5pt,
      inner sep=0pt,
    },
  },

We use the same delaying tactic as before to format the nodes in the bottom level by pushing the node content into labels below the nodes and ditching the node content only after the contents has been noted by Forest.

target tree 2

This is what we want, but the code is more repetitious than it needs to be. We are repeating colours three times in each of level zero and one, and we are using a very similar style for these levels, as well, with just a different colour and size.

To simplify, we could use a TikZ or Forest style. For example,

\begin{forest}
  roundish filling/.style={%
    draw=#1,
    inner color=#1!25,
    outer color=#1,
  },
  for tree={%
    <options for tree>
  },
  where level=0{% for the root node
    <additional options for level 0>,
    roundish filling=magenta,
  }{%
    if level=1{% middle nodes
      <additional options for level 1>,
      roundish filling=blue!50!cyan,
    }{% remaining nodes
      <options for level 2>,
    },
  },

does the same thing, so that we can draw the tree above using the following code.

\begin{forest}
  roundish filling/.style={%
    draw=#1,
    inner color=#1!25,
    outer color=#1,
  },
  for tree={%
    double,
    rounded corners,
  },
  where level=0{% for the root node
    minimum size=20pt,
    roundish filling=magenta,
    ultra thick,
  }{%
    if level=1{% middle nodes
      minimum size=15pt,
      semithick,
      roundish filling=blue!50!cyan,
    }{% remaining nodes
      delay={%
        if n children=0{%
          label/.wrap pgfmath arg={-90:#1}{content},
          !u.s sep'+=5pt,
          !uu.s sep'+=10pt,
          content=,
        }{},
      },
      green!75!black,
      circle,
      fill,
      fill opacity=.5,
      draw opacity=1,
      draw,
      minimum size=5pt,
      inner sep=0pt,
    },
  },
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}

OK. Let's concentrate on spacing and colouring by level. Suppose we wish to typeset a tree like this.

target tree 3

So the basic tree can be typeset like this.

\begin{forest}
  for tree={%
    inner sep=0pt,
    minimum size=5pt,
    circle,
    fill,
  },
  [[[[][][]][[][[][]]]][[[[[][[[][][]][[][]][[[][]][[[][][][][]]][[][]]][]][[[[[][]]]]]]][[][]][[][]]][][]]]
\end{forest}

step 1 target 3

For reference, here's a version with the levels numbered.

annotated step 1 target 3

And here's the annotated target.

annotated target 3

The spacing is a bit odd for this tree so we need quite a complex conditional set-up to get it right. The children of the root (level 0) are a bit further from the root and a bit further from each other than is standard. So we'll need something like this schema.

  where level=0{%
    <increase distance to children>,
    <increase distance between children>,
  }{}

Level 4's children seem more spaced out than standard and the nodes at level 6 are much closer to their parents than standard.

  where level=0{%
    <increase distance to children>,
    <increase distance between children>,
  }{%
    if level=4{%
      <increase distance between children>,
    }{%
      if level=6{%
        <decrease distance to parents>
      }{},
    },
  }

What happens at level 7 is a bit different. The fit of the sub-trees here is altogether more spaced-out. Children are not permitted to encroach into the space below their cousins. A clear band of vertical space is reserved beneath the sub-trees, which obviously has significant effects on the way the tress are spaced out.

So our schema will look like this.

  where level=0{%
    <increase distance to children>,
    <increase distance between children>,
  }{%
    if level=4{%
      <increase distance between children>,
    }{%
      if level=6{%
        <decrease distance to parents>
      }{%
        if level=7{%
          <change fit of nodes' sub-trees>
        }{}
      },
    },
  }

For example,

  where level=0{%
    l sep+=10pt,
    s sep+=15pt,
  }{%
    if level=4{%
      s sep+=20pt
    }{%
      if level=6{%
        l-=20pt,
      }{%
        if level=7{%
          fit=band,
        }{}
      },
    },
  }

step 2 target 3: spacing

To see the difference fit=band, makes, compare the output of this section of the tree with fit=band

with <code>fit=band</code>

with that without it

without <code>fit=band</code>

For the colouring, we want the nodes to change from blue to magenta and back to blue. We can use the nodes' levels to set the proportion of blue to magenta in the colour mix. We can achieve this by splitthing the tree into levels 0-5 and levels 6-9. (We could do it in one go, but why make life complicated?)

  where={level()<6}{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{level()*100/5},
  }{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{(9-(level()))*100/5},
  },

Putting it altogether.

\begin{forest}
  for tree={%
    inner sep=0pt,
    minimum size=5pt,
    circle,
    fill,
  },
  where={level()<6}{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{level()*100/5},
  }{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{(9-(level()))*100/5},
  },
  where level=0{%
    l sep+=10pt,
    s sep+=15pt,
  }{%
    if level=4{%
      s sep+=20pt
    }{%
      if level=6{%
        l-=20pt,
      }{%
        if level=7{%
          fit=band,
        }{}
      },
    },
  }
  [[[[][][]][[][[][]]]][[[[[][[[][][]][[][]][[[][]][[[][][][][]]][[][]]][]][[[[[][]]]]]]][[][]][[][]]][][]]]
\end{forest}

produces our target tree.

final tree 3

Complete code:

\documentclass[border=10pt,multi,tikz]{standalone}
\usepackage{forest}
\begin{document}
\begin{forest}
  for tree={%
    label/.option=content,
    content=,
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    delay={%
      label/.option=content,
      content=,
    },
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    delay={%
      if n children=0{%
        label/.wrap pgfmath arg={-90:#1}{content},
      }{%
        label/.option=content,
      },
      content=,
    },
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    delay={%
      if n children=0{%
        label/.wrap pgfmath arg={-90:#1}{content},
        !u.s sep'+=5pt,
        !uu.s sep'+=10pt,
      }{%
        label/.option=content,
      },
      content=,
    },
    circle,
    fill,
    minimum size=5pt,
    inner sep=0pt,
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    double,
    rounded corners,
  },
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    double,
    rounded corners,
  },
  where level=0{% for the root node
    minimum size=20pt,
    draw=magenta,
    inner color=magenta!25,
    outer color=magenta,
    ultra thick,
  }{%
  },
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  for tree={%
    double,
    rounded corners,
  },
  where level=0{% for the root node
    minimum size=20pt,
    draw=magenta,
    inner color=magenta!25,
    outer color=magenta,
    ultra thick,
  }{%
    if level=1{% middle nodes
      minimum size=15pt,
      semithick,
      draw=blue!50!cyan,
      inner color=blue!50!cyan!25,
      outer color=blue!50!cyan,
    }{% remaining nodes
      delay={%
        if n children=0{%
          label/.wrap pgfmath arg={-90:#1}{content},
          !u.s sep'+=5pt,
          !uu.s sep'+=10pt,
          content=,
        }{},
      },
      green!75!black,
      circle,
      fill,
      fill opacity=.5,
      draw opacity=1,
      draw,
      minimum size=5pt,
      inner sep=0pt,
    },
  },
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}
\begin{forest}
  roundish filling/.style={%
    draw=#1,
    inner color=#1!25,
    outer color=#1,
  },
  for tree={%
    double,
    rounded corners,
  },
  where level=0{% for the root node
    minimum size=20pt,
    roundish filling=magenta,
    ultra thick,
  }{%
    if level=1{% middle nodes
      minimum size=15pt,
      semithick,
      roundish filling=blue!50!cyan,
    }{% remaining nodes
      delay={%
        if n children=0{%
          label/.wrap pgfmath arg={-90:#1}{content},
          !u.s sep'+=5pt,
          !uu.s sep'+=10pt,
          content=,
        }{},
      },
      green!75!black,
      circle,
      fill,
      fill opacity=.5,
      draw opacity=1,
      draw,
      minimum size=5pt,
      inner sep=0pt,
    }
  }
  [A
    [B [C][Ch]]
    [D [Dd][E][F][Ff]]
  ]
\end{forest}

\begin{forest}
  for tree={%
    inner sep=0pt,
    minimum size=5pt,
    circle,
    fill,
  },
  [[[[][][]][[][[][]]]][[[[[][[[][][]][[][]][[[][]][[[][][][][]]][[][]]][]][[[[[][]]]]]]][[][]][[][]]][][]]]
\end{forest}
\begin{forest}
  for tree={%
    inner sep=0pt,
    minimum size=5pt,
    circle,
    fill,
  },
  where level=0{%
    l sep+=10pt,
    s sep+=15pt,
  }{%
    if level=4{%
      s sep+=20pt
    }{%
      if level=6{%
        l-=20pt,
      }{%
        if level=7{%
          fit=band,
        }{}
      },
    },
  }
  [[[[][][]][[][[][]]]][[[[[][[[][][]][[][]][[[][]][[[][][][][]]][[][]]][]][[[[[][]]]]]]][[][]][[][]]][][]]]
\end{forest}
\begin{forest}
  for tree={%
    inner sep=0pt,
    minimum size=5pt,
    circle,
    fill,
  },
  where={level()<6}{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{level()*100/5},
  }{%
    fill/.wrap pgfmath arg={magenta!#1!blue}{(9-(level()))*100/5},
  },
  where level=0{%
    l sep+=10pt,
    s sep+=15pt,
  }{%
    if level=4{%
      s sep+=20pt
    }{%
      if level=6{%
        l-=20pt,
      }{%
        if level=7{%
          fit=band,
        }{}
      },
    },
  }
  [[[[][][]][[][[][]]]][[[[[][[[][][]][[][]][[[][]][[[][][][][]]][[][]]][]][[[[[][]]]]]]][[][]][[][]]][][]]]
\end{forest}
\end{document}
  • 3
    Again, you made a perfectly documented answer that clearly took you time and effort to put in that tutorial way. It is a perfect piece of minimal documentation, nothing more nothing less :)) I hope you don't lose this passion for writing self-explanatory answers someday. – Diaa Aug 30 '16 at 5:40
  • 1
    Thank you ;). This one took a bit longer than I intended when I started it! – cfr Aug 30 '16 at 9:30
  • I know how painful it is to write such a long answer. LaTeX is proud :)) – Diaa Aug 30 '16 at 10:09
  • After reading your code again, I would like to understand two things if you don't mind. 1- in label/.wrap pgfmath arg={-90:#1}{content}, I understand that you specify the radial position of the label, but where can I find more information on other manipulations like this (radial positioning) in the manual? 2- English is not my native language, so I didn't quite understand what you intended to do by setting fit=band. I put both outputs here for levels 6-8, and I would like to know what the effect of fit=band is. – Diaa Sep 4 '16 at 13:39
  • 1
    @DiaaAbidou No problem. It is nice to have somebody who doesn't just want code for an image ;). – cfr Sep 4 '16 at 17:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.