# Drawing lines or arrows along node pathes with forest

I quite often find me drawing pictures like the following:

I use the following code for this:

\documentclass{article}

\usepackage{forest}
\useforestlibrary{linguistics}
\forestapplylibrarydefaults{linguistics}

\forestset{
sm edges/.style={for tree={parent anchor=south, child anchor=north,base=bottom},
where n children=0{tier=word}{}
},
}

\begin{document}

\forestset{default preamble'={
for tree={align=center,parent anchor=south, child anchor=north,base=bottom},
before drawing tree={
sort by=y,
for min={tree}{baseline}
}
}}

\begin{forest}
sm edges
[IP
[NP, name=subject [der Junge$_i$,roof]]
[I$'$
[VP
[V$'$
[NP, name=dobject [der Frau, roof]]
[V$'$
[NP,   name=aobject [\_$_i$]]
[V$^0$,name=verb [gezeigt wir-, roof]]]]]
[I$^0$ ,name=Infl [-\/d]]]]
\draw[->,dotted] (Infl.north) .. controls (2.5,.35)   and (-1.5,-.05) .. ($(subject.north)+(0,.1)$);
\draw[->,dashed] (verb.north) .. controls (1.9,-3.3)  and (0.5,-3.0)  .. ($(dobject.north)+(0,.1)$);
\draw[->]        (verb.north) .. controls (1.7,-4.2) and (1.2,-4.2) .. ($(aobject.north)+(0,.1)$);
\end{forest}\hspace{1cm}
\begin{tabular}[b]{ll@{}}
\tikz[baseline]\draw[dotted](0,1ex)--(1,1ex);&just case\\
\tikz[baseline]\draw(0,1ex)--(1,1ex);&just theta-role\\
\tikz[baseline]\draw[dashed](0,1ex)--(1,1ex);&case and theta-role\\
\\
\end{tabular}

\end{document}


The disadvantage is that copy and paste does not work since the font may be different in another document and this brakes the absolute coordinates. Furthermore it is tedious to find the correct coordinates when one draws these trees in the first place.

The question is: Would it be possible to write some code that connects two nodes like in the figure by passing all nodes that are on the way. Basically: going up the tree until you hit a node that dominates both nodes and then going down again.

If I understand OP's question correctly, the goal is to have a mechanism which automatically draws a curved arrow between two nodes, passing by all the nodes in the tree path connecting them but avoiding to hit any of them. I have had a need for such a thing myself, thought about it and I gave up without writing a single line of code. Implementing this, in general, seems like black magic to me.

EDIT: The new version of cfr's answer actually achieves the black magic, or at least a very very dark shade of gray ;-) My hat's off to cfr and the author of hobby!!!

EDIT: Thinking now whether feeding the subtree boundary (get min s tree boundary and get max s tree boundary) to hobby could produce the perfect result?

In this answer, I keep to a simpler goal of cfr's original answer: to develop a system which walks, first child-to-parent, then parent-to-child, from the current to the given node, and does stuff along the way. I reimplement cfr's original mechanism to make it more generic: most importantly, the solution below cleanly separates the implementation of the nodewalk and act of drawing the lines between the visited nodes (actually, it allows for any kind of action on adjacent pairs of nodes).

Let us start with the example showing how to use the conceptually simplest of the implemented keys: nodewalk multi-step key: walk to. walk to=<nodewalk> walks, via parent-child adges, from the current node to the node at the end of the given nodewalk. In the example, this node is given by name (verb), and the key is used in its spatial propagator form for walk to. Executed at node -d, for walk to={name=verb}{red} colors red all the nodes on the tree path from -d to V0, excluding -d and including V0. (The idea behind excluding the origin is that it is easy to include it manually (just say current) but difficult to exclude.)

(The following code contains the definitions of all implemented keys, so just copy-paste the rest of the examples in the document body of the following one.)

\documentclass[tikz,border=5pt,multi]{standalone}
\usepackage[linguistics]{forest}
\begin{document}

\forestset{%
walk@to@lowest@common@ancestor/.nodewalk style={
save={walkto@other}{group={#1}},
while={}{
if in saved nodewalk={current}{walkto@others@ancestors}{break}{parent},
},
},
walk@from@lowest@common@ancestor/.nodewalk style={
reverse/.process=Ow{id}{
until={>O_={id}{##1}}{current,fake=parent}
}
},
define long step={walk to lowest common ancestor}{n args=1}{
walk@to@lowest@common@ancestor={#1},
},
define long step={walk from lowest common ancestor}{n args=1}{
% This doesn't work:
%%%% fake={walk@to@lowest@common@ancestor={#1}},
% "save" introduces an embedded nodewalk, which doesn't restore the value
%  of "fake" after doing its work ... (will be) fixed in v2.1.5
% Workaround:
save={walkto@other}{group={#1}},
while={}{
if in saved nodewalk={current}{walkto@others@ancestors}{break}{fake=parent},
},
% end of workaround
walk@from@lowest@common@ancestor
},
define long step={walk to}{n args=1}{
walk to lowest common ancestor={#1},
walk@from@lowest@common@ancestor,
},
define long step={lowest common ancestor}{n args=1}{
group={walk to lowest common ancestor={#1}}
},
between nodewalk steps/.style 2 args={
for nodewalk={#1}{every step={options/.process=OOw2{!b.name}{name}{#2}}}
},
between nodewalk steps in walk to/.style n args=3{
for nodewalk={
current,
every step'={options/.process=OOw2{!b.name}{name}{#2}},
walk@to@lowest@common@ancestor={#1},
every step'={},
current,
every step'={options/.process=OOw2{!b.name}{name}{#3}},
walk@from@lowest@common@ancestor,
}{}
}
}

\begin{forest}
where n children=0{tier=word}{},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb,
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl,
[-\/d,
draw,for walk to={name=verb}{red}
]
]
]
]
\end{forest}

\end{document}


To draw arrows between the nodes visited by walk to, we develop a generic style making it easy to do something with pairs of nodewalk neighbours. between nodewalk steps=<nodewalk><node keys> executes <node keys> at each but first step of <nodewalk>; crucially, within <node keys>, #1 and #2 refer to names of the previous and current node of the nodewalk. (Note that below, current is the first step of the nodewalk.)

\begin{forest}
where n children=0{tier=word}{},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb,
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl,
[-\/d,
between nodewalk steps={current,walk to={name=verb}}{
tikz+={\draw[->,red](#1)--(#2);}},
]
]
]
]
\end{forest}


This almost gives us what we need. Imagine we'd want to draw arrows exactly over the edges. We'd need to say \draw(#1.north)--(#2.south)} on the way up and \draw(#1.south)--(#2.north)} on the way down. In general, we might need to refer to the lowest common ancestor (LCA) itself or to the subpaths leading from the current node to the LCA or from the LCA to the final node. This is precisely what is achieved by nodewalk steps lowest common ancestor, walk to lowest common ancestor and walk from lowest common ancestor.

\begin{forest}
where n children=0{tier=word}{},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb,
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl,
[-\/d,
for walk to lowest common ancestor={name=verb,u11}{red},
for lowest common ancestor={name=verb,u11}{green},
for walk from lowest common ancestor={name=verb,u11}{blue},
]
]
]
]
\end{forest}


Finally, for both ease of use and faster compilation, we define between nodewalk steps in walk to=<nodewalk><every step up to LCA><every step down from LCA>, which works just like between nodewalk steps but takes two separate keylists for the two parts of the path. (In the example below, I also had some fun shifting the arrows a bit.)

\begin{forest}
where n children=0{tier=word}{},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb,
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl,
[-\/d,
between nodewalk steps in walk to={name=verb}
{tikz+={
\draw[->,red] let \p1=(#1.north), \p2=(#2.south), \n1={2pt} in
($(\p1)!-\n1!90:(\p2)$)--($(\p2)!\n1!90:(\p1)$);}}
{tikz+={
\draw[->,red] let \p1=(#1.south), \p2=(#2.north), \n1={2pt} in
($(\p1)!-\n1!90:(\p2)$)--($(\p2)!\n1!90:(\p1)$);}}
]
]
]
]
\end{forest}


I'd be interested to know if you think that these keys should be a part of the package itself.

UPDATE #2: use hobby

Taking up the suggestion in cfr's comment, I modify the solution above to be able to use it with hobby.

In order to use hobby, we need to construct the following token list:

\draw[<style>]
(<start node>.north)
to [curve through={
(<node1 between first and LCA>.north east) .. <...> ..(<node n between first and LCA>.north east)
.. (<LCA>.south) ..
(<node1 between LCA and last>.north west) .. <...> ..(<node n between LCA and last>.north west)
}
.. (<last node>.north);


This is a bit problematic to achieve with the above solution, as the path from the first (current) to the last (given) node contains five parts listed below, each of which contributes to the hobby specification in a different way.

1. the first node

2. the nodes between the first node and the LCA

3. the LCA

4. the nodes between the LCA and the last node

5. the last node

The implementation below has only one (multi-step) nodewalk key, walk from to, which takes two arguments, the first and the last node (more precisely: the nodewalks from the current node leading to those nodes), and walks the entire path from the first to the last node (including both), but also saves the relevant parts of this path into different saved nodewalk, to be loaded by the user as (s)he needs.

1. load=walk first

2. load=walk from first to LCA

3. load=walk LCA

4. load=walk from LCA to last

5. load=walk last

With this in place, constructing the hobby code is at least manageable; see style hobby curve below.

\documentclass[tikz,border=5pt,multi]{standalone}
\usepackage[linguistics]{forest}
\usetikzlibrary{hobby}
\begin{document}

\forestset{%
walkfromto@toLCA/.nodewalk style={
walk and save={walk from first to LCA}{
fake=parent,
while={}{
if in saved nodewalk={current}{walkfromto@anc}
{break}
{
% This doesn't work:
%%%% fake={walkfromto@toLCA={#1}}
% "save" introduces an embedded nodewalk, which doesn't restore the value
% of "fake" after doing its work ... (will be) fixed in v2.1.5
% So instead of saying "current" below, whoever uses this style must
% appropriately define walkfromto@toLCA@step.
walkfromto@toLCA@step,
fake=parent
},
},
},
},
walkfromto@fromLCA/.nodewalk style={% we're at LCA now
walk and save={walk from LCA to last}{
if in saved nodewalk={current}{walk last}{% other = LCA
}{
reverse/.process=Ow{id}{
until={>O_={id}{##1}}{current,fake=parent}
}
}
}
},
define long step={LCA}{n args=2}{
fake={group={#1}},
% In v2.1.5, replace this workaround by "fake={walkfromto@toLCA={#2}}"
walkfromto@toLCA@step/.style={fake=current}, walkfromto@toLCA={#2},
% end of workaround (do the same in all step definitions below)
current
},
define long step={walk from to}{n args=2}{
save={walk last}{group={#2}},
walk and save={walk first}{group={#1}},
walkfromto@toLCA@step/.style={current}, walkfromto@toLCA={#2},
walk and save={walk LCA}{current},
walkfromto@fromLCA,
},
between nodewalk steps/.style 2 args={
for nodewalk={#1}{every step={options/.process=OOw2{!b.name}{name}{#2}}}
},
}

\begin{forest}
where n children=0{tier=word}{},
hobby curve/.style 2 args={
for nodewalk={walk from to={current}{#1}}{},
temptoksa={},
temptoksb={},
pass through/.style={
temptoksa+/.register=temptoksb,temptoksb={..},
temptoksa+/.process=Ow{name}{##1}},
for load={walk from first to LCA}{
pass through={([yshift=2.5pt]##1.north east)}},
pass through={([yshift=4.5pt]##1.south)}},
for load={walk from LCA to last}{
pass through={([yshift=2.5pt]##1.north west)}},
!r.tikz+/.process=Rw{temptoksa}{%
\draw[#2] (##1.north)
to [curve through={####1}]
(##2.north);
}},
}
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb, hobby curve={name=dobject}{red,->}
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl, hobby curve={r1}{red,->},
[-\/d,
]
]
]
]
\end{forest}


• I guess you posted this while I was revising my proof of concept. – cfr Feb 11 '17 at 16:57
• It would be useful if there was an easier way to find the intersection of two nodewalks i.e. a common ancestor. I couldn't find anything in current Forest last night, but may have missed something. – cfr Feb 11 '17 at 16:59
• Intersection of nodewalks and LCA are actually two different things. Both could be useful, both can be done using Forest keys only, but none is implemented in the package so far. – Sašo Živanović Feb 11 '17 at 17:07
• I guess what the OP can do is combine your code with hobby? That would be more general than my answer, but with the curves. This looks really nice. (I deliberately didn't read it earlier, even though I voted for it :-).) – cfr Feb 12 '17 at 0:05
• I couldn't get save to work. That is, I didn't really want walk and save, but save didn't seem to get me anything useful. Obviously, I used it wrong .... – cfr Feb 12 '17 at 0:10

EDIT Now with styles.

The follow provides a way to construct a nodewalk between two nodes and draw a path between them using some style. visit process supports the creation of custom styles.

To set up a new style, visit <foo>, two things are required.

visit <foo>={%
visit process={#1}{<foo>},
}


should simply pass its argument to visit process with the relevant name, <foo>. In addition, visit <foo> using should define a style taking at two arguments, the reference for the node to be visited and customisation of the style.

  visit <foo> using/.style n args=2{%


This should call visit trace with the node reference, to construct the relevant path to the node-to-be-visited.

    visit trace={#1}{.north west}{.north east}{.children}{},


The first argument is the node reference. The second is the anchor to use for intermediate nodes prior to the common ancestor. The third is the anchor for intermediate nodes in between the common ancestor and the node-to-be-visited. The fourth is the anchor for the common ancestor. The fifth is an adjustment for the peak node when using the processed list with a hobby curve (e.g. [yshift=2.5pt] - note the square brackets). It should not be specified otherwise. These define the coordinates used in creating the list of points to be joined.

    before typesetting nodes={


The remainder of this style should define TikZ code to draw the route between the current node and the node-to-be-visited.

      tikz+/.process={%
RRw2{visit me}{visit keylist a}{%
\draw [visit style, #2] (.parent) \foreach \i in {##2} { -- (\i) } -- (##1.parent);
},
},
}
},


The following are available for use in creating the TikZ code:

• visit keylist a is a list of coordinates or nodes, without parenthesis, suitable for use with \foreach, say. These will be of the form <node>.<anchor> or <node>, depending on the options given to visit trace.

• visit me is the name of the node-to-be visited.

• visitees is a processed list of coordinates or nodes, suitable for passing to a hobby curve. These will be of the form (<node>.<anchor>) .. (<node>.<anchor) .. or (<node>) .. (<node>) .. (<node>) .., with the peak node specified as (<adjustment><node>.<anchor>).

visit <foo> will then provide a style taking one argument. This may either be a simple node reference or a node reference followed by a colon and custom style options.

visit <foo>=<node-to-be-visited>:{style options}


In addition, visit style=<style options> can be used to set default styling for all visits.

As examples, the following three visiting styles are provided:

• visit plain: draw a continuous path from here to there, using straight lines and passing through the appropriate intermediate points;

• visit steps: draw straight paths from here to the first intermediate node, from that node to the next and so on, until we get there.

• visit hobby: draw a smooth curve as a continuous path from here to there, passing through the requisite intermediate points.

In addition, the following may be used for vertical adjustments with these styles.

• visit hobby peak yshift: a dimension intended for use shifting the peak of the hobby curve up.

• visit yshift: a dimension which shifts all intermediate nodes in the plain and steps styles.

The upshot of this is that

\begin{forest}
sm edges,
visit style={->},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb, visit hobby=uu1:red, visit steps=uu1:{green!75!black, dashed}, visit hobby=u1:blue, visit steps=u1:{green!75!black, dashed}, visit plain=u1:{densely dotted, blue}, visit plain=uu1:{densely dotted, blue}
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl, visit hobby=r1:red, visit steps=r1:{dashed, green!75!black}, visit plain=r1:{densely dotted, blue}
[-\/d]
]
]
]
\draw [dashed] (current bounding box.east |- !rL.north) ++(10mm,2ex) coordinate (a) -- ++(1,0) node [anchor=west] {case and theta-rule};
\draw  (a) ++(0,\baselineskip) coordinate (b) -- ++(1,0) node [anchor=west] {just theta-rule};
\draw [dotted] (b) ++(0,\baselineskip) coordinate (b) -- ++(1,0) node [anchor=west] {just case};
\end{forest}


produces

Complete code:

\documentclass[tikz,border=5pt,multi]{standalone}
\usepackage{forest}
\useforestlibrary{linguistics}
\forestapplylibrarydefaults{linguistics}
\forestset{
sm edges/.style={%
sn edges,
where n children=0{tier=word}{},
},
}
\usetikzlibrary{hobby}
\begin{document}

\forestset{%
default preamble'={%
for tree={
align=center, parent anchor=children, child anchor=parent, base=bottom
},
before drawing tree={
sort by=y,
for min={tree}{baseline}
},
},
declare keylist register=visit keylist a,
declare keylist register=visit keylist b,
declare toks register=visit peak,
declare toks register=visit me,
declare toks register=visitees,
declare dimen register=visit hobby peak yshift,
visit hobby peak yshift'=4.5pt,
declare dimen register=visit yshift,
visit yshift'=2.5pt,
visit keylist a'=,
visit keylist b'=,
visit peak'=,
visit me'=,
visitees'=,
visit trace/.style n args=5{
before typesetting nodes={
visit keylist a'=,
visit keylist b'=,
visitees=,
for nodewalk={
walk and save={walk 1}{current and ancestors},
origin,
#1,
visit me/.option=name,
for ancestors={
if in saved nodewalk={current}{walk 1}{visit peak/.option=name, break}{visit keylist b/.process={Ow{name}{##1#2}}}%
}%
}{},
for nodewalk={
current,
until={%
>O+tR+t={name}{visit peak}%
}{parent, if={>O+tR+t={name}{visit peak}}{}{visit keylist a/.process={Ow{name}{##1#3}}}},
}{},
visit keylist a/.process={Rw{visit peak}{#5##1#4}},
if visit keylist b={}{}{visit keylist a/.process={R+t+r{visit keylist b}}},
},
},
visit plain using/.style n args=2{%
visit trace={#1}{.north west}{.north east}{.children}{},
before typesetting nodes={
tikz+/.process={%
RRw2{visit me}{visit keylist a}{%
\draw [visit style, #2] (.parent) \foreach \i in {##2} { -- ([yshift=\foresteregister{visit yshift}]\i) } -- (##1.parent);
},
},
}
},
visit hobby using/.style n args=2{%
visit trace={#1}{.north west}{.north east}{.children}{[yshift=\foresteregister{visit hobby peak yshift}]},
before typesetting nodes={
tikz+/.process={%
RRw2{visitees}{visit me}{%
\draw [visit style, #2] (.parent) to [curve through={##1}]  (##2.parent);
},
},
}
},
visit steps using/.style n args=2{%
visit trace={#1}{}{}{.children}{},
before typesetting nodes={
tikz+/.process={%
RRw2{visit me}{visit keylist a}{%
\foreach \i [remember=\i as \ilast (initially .parent)] in {##2}
\draw [visit style, #2] ([yshift=\foresteregister{visit yshift}]\ilast) -- ([yshift=\foresteregister{visit yshift}]\i);
\draw [visit style, #2] ([yshift=\foresteregister{visit yshift}]\ilast) -- (##1.parent);
},
},
}
},
visit/.style={visit plain=#1},
visit plain/.style={%
visit process={#1}{plain},
},
visit hobby/.style={%
visit process={#1}{hobby},
},
visit steps/.style={%
visit process={#1}{steps},
},
visit process/.style n args=2{%
temptoksa=#1,
split register={temptoksa}{:}{temptoksb,temptoksc},
visit #2 using/.process={RRw2{temptoksb}{temptoksc}{{##1}{##2}}},
},
/tikz/visit style/.style={},
visit style/.code={%
\tikzset{%
visit style/.style={#1},
}%
},
if visitees={}{%
visitees={(#1)},
}{%
visitees+={.. (#1)},
},
},
}

\begin{forest}
sm edges,
visit style={->},
[IP
[NP, name=subject
[der Junge$_i$,roof]
]
[I$'$
[VP
[V$'$
[NP, name=dobject
[der Frau, roof]
]
[V$'$
[NP,   name=aobject
[\_$_i$]
]
[V$^0$,name=verb, visit hobby=uu1:red, visit steps=uu1:{green!75!black, dashed}, visit hobby=u1:blue, visit steps=u1:{green!75!black, dashed}, visit plain=u1:{densely dotted, blue}, visit plain=uu1:{densely dotted, blue}
[gezeigt wir-, roof]
]
]
]
]
[I$^0$ ,name=Infl, visit hobby=r1:red, visit steps=r1:{dashed, green!75!black}, visit plain=r1:{densely dotted, blue}
[-\/d]
]
]
]
\draw [dashed] (current bounding box.east |- !rL.north) ++(10mm,2ex) coordinate (a) -- ++(1,0) node [anchor=west] {case and theta-rule};
\draw  (a) ++(0,\baselineskip) coordinate (b) -- ++(1,0) node [anchor=west] {just theta-rule};
\draw [dotted] (b) ++(0,\baselineskip) coordinate (b) -- ++(1,0) node [anchor=west] {just case};
\end{forest}

\end{document}

• I tested save instead of walk and save and it seems to work. origin (a very cool solution) is then not needed anymore. – Sašo Živanović Feb 12 '17 at 0:37
• Thanks a lot! This is great! It is almost exactly what I wanted. Would it be possible to make the (red) arrow pass right in the middle between the node label (IP and V') and the lines starting below the label? And the pointer of the arrow (the beginning) not overlap the line connecting the tree nodes? – Stefan Müller Feb 12 '17 at 12:52
• @StefanMüller See the Forest chat room for the first. See maybe hobby's documentation for the other. – cfr Feb 12 '17 at 16:07
• @StefanMüller See edit above. If you really want to calculate it, you could. I wouldn't think it was worth the effort, though. – cfr Feb 13 '17 at 0:40
• @StefanMüller You give the checkmark to the answer which helps you most. It doesn't make sense (in most cases) to think of one answer as 'right' and the rest as 'wrong'. But I'm happy for it to go to Sašo, if that's what you're asking. If you use his code, you should do that because his answer will have helped you most ;). – cfr Feb 13 '17 at 19:37