2

I am facing the following problem: I would like to create a forest style such that the length of the edges in the forest depend on the length of the labels on the edges. To arrive at space-saving trees that at the same time are aligned per level, I want that the length of the edges depends on the maximum label size on each level of the tree.

For solving this problem I started with the solution offered to this question:

How to lengthen edge in forest to be the length of the edge label?

I adapted this example a bit to account for computing the maximum label size in the whole tree and arrived at the following solution that adapts the edge size to the maximal label length that occurs in the whole tree:

\documentclass[border=10pt, tikz, multi, varwidth]{standalone}
\usepackage{forest}
\standaloneenv{forest}

\begin{document}
\forestset{
   my edge label/.style={
    if={n>.5*(n_children("!u"))}{
      edge label = {node [midway, below, sloped] {#1}},
    }{
      edge label = {node [midway, above, sloped] {#1}},
    },
    TeX={\settowidth{\mylabelwidth}{#1}},
    TeX={\pgfmathsetlength{\mymaxlabelwidth}{max(\mylabelwidth,\mymaxlabelwidth)}},
  },
  ptreetest/.style= {
     TeX={\newlength{\mylabelwidth}},
     TeX={\newlength{\mymaxlabelwidth}},
     TeX={\settowidth{\mymaxlabelwidth}{5pt}}, % minimum width is 5pt
     for tree={%
        grow' = east,
        before computing xy={
            if={
                (l)<(sqrt(abs((\mymaxlabelwidth)^2 - s^2))) + 10pt)
            }{
                l={sqrt(abs((\mymaxlabelwidth)^2  - s^2))) + 10pt},
            }{},
         },
    },
  },
}

\begin{forest}
ptreetest
[A
    [B, my edge label={very long label}
        [E, my edge label={short}]
        [F, my edge label={ws}]
    ]
    [C, my edge label= {short}]
]
\end{forest}

\end{document}

However, I really want that the the lengths are adjusted per level. My first thought was that this should be easily achievable by instead of having the \mymaxlabelwidth variable, having an array of such variables that allows for computing (and later accessing) the maximum value per level. Unfortunately, I failed to implement this idea. I tried several approaches to implementing arrays in latex (e.g., using pgffor or pgfkeys), but none of them worked for me (I think because the scoping of the corresponding arrays was not right). I have also been experimenting a bit with expl3, but probably due to my lacking insights on that, I did not manage to achieve the desired functionality.

It would be create if someone could help me with this problem! Thank you a lot!

1
  • Very nice question!
    – cfr
    Dec 2 '20 at 4:05
2

This answer goes over and above OP's requirements. It not only implements per level adjustment, but also tries to compute the l needed to fit the edge label more reliably.

Let us start with the levels. As the OP suggests, we need to store the label width of multiple edge labels, and the easiest way to do this is to declare a new forest (dimension) option: declare dimen={my edge label width}{0pt}. Forest options are automatically related to nodes, so the data structure is immediately in place. And so are the tools to work with the data; in particular, we will avail ourselves to aggregate function max.

In fact, the code below implements three styles, differing in the "locality" of the fitting. The difference between fit edge labels children, fit edge labels level and fit edge labels tree should be clear from the following image.

enter image description here

All three styles use Forest's aggregate function max (in the handler form .max) to compute the required l, and a spatial propagator for ... to set this value of l at the relevant nodes. The difference is in what .max aggregates over: children, level=... or descendants; these same nodewalks (in the for ... form) are the also used to distribute the new value of l to the relevant nodes.

Now for the other issue, a more reliable computation of l. The original code works under the assumption that l and s coordinates (of the parent and the child) are a good approximation of the endpoints of the edge. And to some extent, they are, and the discrepancy can be fixed manually --- this is what + 10pt in the original code achieves. But if we are precise, l and s are the coordinates of a node's anchor, while the endpoints of the edge correspond to parent anchor on the paren't side and to child anchor on the child's side.

But how are we to get the coordinates of the parent and the child anchor? To get them, the code below actually typesets the parent and the child node (and throws away the result, of course). This is done by macro \measureedge, and this macro is called from a pgfmath function we define, l_for_my_edge_label, which uses the coordinates provided by \measureedge and the width of the edge label stored in my edge label width to compute the l required for the edge label to fit. For further details, see the comments in the code.

However, even all this does not buy us totally bullet-proof solution. When parent/child anchor is empty (the default), TikZ draws the edge to a magically determined border point, and when we "move" the node (by adjusting l), the contact point can change. Fortunately, (i) the effect is not very prominent, it is only visible with wide nodes; (ii) the problem disappears when we use a fixed parent/child anchor; and (iii) the manual workaround is easy enough. For details, see comment (2) in the code of the first tree.

As the final remark, note that the code (l_for_my_edge_label in particular) assumes that the tree grows horizontally. Well, it works out-of-box for growing to the west; for the east, exchange {above} and {below} in the definition of my edge label a to flip the location of the edge labels.

\documentclass[border=10pt, tikz, multi, varwidth]{standalone}
\usepackage{forest}
\standaloneenv{forest}

% This macro draws (and discards) a tree fragment: the current node, its parent
% and the edge between them.  After drawing the edge (which is assumed to
%   consist of a single line segment), it measures the bounding box of the edge
% path to get the "width" and the "height" of the edge.  The macro expects the
% x and y coordinates of the nodes to be computed.
\def\measureedge{%
  \begingroup
  \forestset{
    % Put the tree (fragment) into box 0, which we will not use.
    draw tree box=0,
    % Remember the current node.
    alias=l_for_my_edge_label,
    % Draw just this node and its parent.
    draw tree processing order/.nodewalk style={name=l_for_my_edge_label,parent},
    % Just in case, for generality, don't draw any associated tikz code.
    draw tree tikz processing order/.nodewalk style={},
    % After constructing the edge path, this code gets the dimensions of the
    % edge; see TikZ manual 15.6 for info on "path picture".  As this is
    % executed deep inside TeX groups, we need to assign "\edgewidth" and
    % "\edgeheight" globally.
    edge={
      path picture={%
        \pgfpointdiff
          {\pgfpointanchor{path picture bounding box}{south west}}%
          {\pgfpointanchor{path picture bounding box}{north east}}%
        \pgfgetlastxy\measureedgex\measureedgey
        \global\let\edgewidth\measureedgex
        \global\let\edgeheight\measureedgey
      }
    },
    % Draw the tree fragment. The '-variant preserves the forest node boxes, so
    % these nodes can be later typeset for real.
    draw tree',
  }%
  \endgroup
}

% We define a pgfmath function which returns the "l" needed at the current node
% to make the edge label fit; if the edge is already long enough, the current
% "l" is returned.
% 
% This function assumes that "grow'=east": The l-dimension corresponds to the
% x-dimension and to edge "width"; the s-dimension corresponds to y and edge
% "height". For further info on Forest's ls-coordinate system, see manual 2.4
% 
% Hmm, this function does not require any arguments, but if I use {0} instead
% of {1} below, then using this function in a \pgfmathparse expression returns
% an empty value. Is this a bug in pgfmath?
\pgfmathdeclarefunction{l_for_my_edge_label}{1}{%
  \begingroup
  % \measureedge sets \edgewidth and \edgeheight
  \measureedge 
  % Stores the length of the edge to \pgfmathresult
  \pgfmathparse{sqrt(\edgewidth^2 + \edgeheight^2)}%
  \ifdim\forestoption{my edge label width}>\pgfmathresult pt\relax
    \pgfmathparse{
      l() + % increase the current "l"
      % by the difference between
      sqrt(my_edge_label_width()^2 - \edgeheight^2) % the required edge width
      - \edgewidth % and the current one
    }%
  \else
    % The edge was long enough already, just return "l". To understand the
    % conspiracy leading to the definition of \pgfmathl, see Forest manual 3.18
    % and PGF manual section 97.
    \pgfmathl{}%
  \fi
  \pgfmathsmuggle\pgfmathresult
  \endgroup
}

 % Set the format of the label node here.
\newcommand\mylabelformat[1]{\scriptsize#1}

% Just a temporary register
\newlength{\mylabelwidth}

\forestset{
  % We declare a new option which will store the width of the label.
  declare dimen={my edge label width}{0pt},
  % The key assigns the label width to option "my edge label width" (via the
  %   temporary \mylabelwidth), and delays the construction of the tikz code
  % which will produce the label until we know the relative position of the
  % node wrt the parent.
  my edge label/.style={
    TeX={\settowidth{\mylabelwidth}{\mylabelformat{#1}}},
    my edge label width/.expanded=\the\mylabelwidth,
    before computing xy={my edge label a={#1}},
  },
  % We use the "s" coordinate to determine
  % whether the label should appear above or below the edge. This is more
  % reliable than using the child number, but needs to be done after packing.
  % 
  my edge label a/.style={
  % Some fancy .processing: "if s<0pt then ##1=below else ##1=above"
    edge label/.process = O_<?_w   {s}{0pt}{below}{above}{
      node [midway, ##1, sloped, inner ysep=0.1em] {\mylabelformat{#1}}
    },
  },
  fit edge labels children/.style= {
    for tree={grow'=east},
    before computing xy={
      % \measureedge needs x and y to be set. It is better to compute them
      % here, once for the entire tree, than to compute them for each node ---
      % "compute xy" computes x and y for the entire subtree.
      compute xy,
      % For each branching node ...
      where n children=0{}{%
        % Walk the children to compute the maximum "l" required by their
        % edge labels.
        tempdima/.max={l_for_my_edge_label()}{children},
        % Set this maximum "l" for all children.
        for children={l/.register=tempdima},
      },
    },
  },
  fit edge labels level/.style= {
    for tree={grow'=east},
    % This is not terribly efficient ...
    before computing xy={
      % How many levels are there in the tree?
      tempcounta/.max={level}{tree},
      compute xy,
      % Loop through the levels (in reverse, because this is easier to
      %   implement; below, tempcounta is interpreted as the current level)
      do until={tempcounta()==0}{
        tempdima/.max={l_for_my_edge_label()}{level/.register=tempcounta},
        % "for level" takes two arguments, the level and the code;
        % .process instruction "R{tempcounta}" feeds it the level and leaves
        % the code alone.
        for level/.process=R{tempcounta}{l/.register=tempdima},
        tempcounta-=1,
      },
    },
  },
  fit edge labels tree/.style= {
    for tree={grow'=east},
    before computing xy={
      compute xy,
      tempdima/.max={l_for_my_edge_label()}{descendants},
      for descendants={l/.register=tempdima},
    },
  },
}

\begin{document}

\begin{forest}
  fit edge labels children, 
  % (1) The minimum distance between a node and its parent is determined by the
  % node's "l" and the parent's "l sep". "my edge label" can only increase this
  % distance. Case in point: edges from B to E and F are longer than the edge
  % label. To make them match precisely, set the relevant "l"s and "l sep"s to
  % 0, like here (uncomment to see the effect):
  % for tree={l=0,l sep=0}
  % 
  % (*) See below
  % for tree={parent anchor=east, child anchor=west},
  [A, label={west:children:}
    [B, my edge label={very long label},
      % (2) Manually adjust the dimension of the label to overcome the
      % remaining problem which can occur when the parent/child anchor is
      % empty, i.e. when it is the "automatic border" anchor.  To see the
      % problem, change "A" to something longer, like "AAAAAAA"; to fix it,
      % uncomment this:
      % 
      % my edge label={\makebox[\dimexpr\width+1em]{very long label}},
      % 
      % The problem occurs because depending on the dimensions and positions of
      % the nodes, either end of the parent-child edge can "move" to a
      % different point on the node border.  So, the problem cannot occur if we
      % use fixed parent and child anchors; for example, the problem cannot
      % arise if you uncomment (*) above.
      [E, my edge label={sh}]
      [F, my edge label={s}]
    ]
    [C, my edge label= {short}
      [G, my edge label= {short}]
      [H, my edge label= {a very long label again}]
    ]
  ]
\end{forest}

\begin{forest}
  fit edge labels level,
  [A, label={west:level:}
    [B, my edge label={very long label},
      [E, my edge label={sh}]
      [F, my edge label={s}]
    ]
    [C, my edge label= {short}
      [G, my edge label= {short}]
      [H, my edge label= {a very long label again}]
    ]
  ]
\end{forest}


\begin{forest}
  fit edge labels tree,
  [A, label={west:tree:}
    [B, my edge label={very long label},
      [E, my edge label={sh}]
      [F, my edge label={s}]
    ]
    [C, my edge label= {short}
      [G, my edge label= {short}]
      [H, my edge label= {a very long label again}]
    ]
  ]
\end{forest}


\end{document}
6
  • Thank you so much for this amazing and general answer!
    – Clara
    Nov 28 '20 at 21:09
  • You're welcome! By the way, the best way to thank on this site is by upvoting and accepting the answer. ;-) Nov 30 '20 at 21:53
  • @SašoŽivanović New contributors can't upvote your answers unless you upvote their questions to give them enough points :).
    – cfr
    Dec 2 '20 at 4:06
  • @Clara You can accept the answer by clicking on the greyed-out tick at the top left of the answer. When you have enough points, you can upvote answers by clicking the up arrow at the top left. (I can't remember how many you need to upvote. I upvoted your question, but that might not be enough.)
    – cfr
    Dec 2 '20 at 4:08
  • Tnx @cfr I missed that one! ;-) Dec 2 '20 at 8:01

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.