我经常会画出如下的图画:
我使用下面的代码来实现这个:
\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}
缺点是复制粘贴不起作用,因为字体可能与另一个文档不同,这会破坏绝对坐标。此外,当人们首先绘制这些树时,找到正确的坐标是很繁琐的。
问题是:是否有可能编写一些代码,通过传递路径上的所有节点来连接两个节点,如图所示。基本上:沿着树向上走,直到遇到一个控制两个节点的节点,然后再向下走。
答案1
如果我正确理解了 OP 的问题,那么目标是有一种机制,可以自动在两个节点之间绘制一个弯曲的箭头,经过连接它们的树路径中的所有节点,但避免击中任何一个节点。我自己也需要这样的东西,考虑了一下,然后放弃了,没有写一行代码。总的来说,实现这一点对我来说就像黑魔法一样。
编辑:新版本的cfr 的答案实际上实现了黑魔法,或者至少是非常非常深的灰色;-) 我向 cfr 和作者致敬hobby
!!!
编辑:现在思考将子树边界(get min s tree boundary
和get max s tree boundary
)输入到是否hobby
可以产生完美的结果?
在这个答案中,我坚持 cfr 原始答案的一个更简单的目标:开发一个系统,首先从子节点走到父节点,然后从父节点走到子节点,从当前节点走到给定节点,并在此过程中执行一些操作。我重新实现了 cfr 的原始机制,使其更加通用:最重要的是,下面的解决方案将节点行走的实现和在访问的节点之间画线的行为清晰地分开(实际上,它允许对相邻的节点对执行任何类型的操作)。
让我们从示例开始,该示例展示了如何使用概念上最简单的实现键:nodewalk 多步键:walk to
。walk to=<nodewalk>
通过父子路径从当前节点走到给定节点路径末尾的节点。在示例中,此节点由名称 ( verb
) 给出,并且键以其空间传播器形式使用for walk to
。在节点 处执行-d
,将从到 的for walk to={name=verb}{red}
树路径上的所有节点都涂成红色,-d
V0
排除 -d
和包括 V0
。(排除原点背后的想法是,手动包含它很容易(只是说current
)但排除它很难。)
(以下代码包含所有已实现键的定义,因此只需将其余示例复制粘贴到以下文档主体中即可。)
\documentclass[tikz,border=5pt,multi]{standalone}
\usepackage[linguistics]{forest}
\begin{document}
\forestset{%
walk@to@lowest@common@ancestor/.nodewalk style={
save={walkto@other}{group={#1}},
save={walkto@others@ancestors}{load={walkto@other},ancestors},
while={}{
if in saved nodewalk={current}{walkto@others@ancestors}{break}{parent},
},
},
walk@from@lowest@common@ancestor/.nodewalk style={
reverse/.process=Ow{id}{
fake={load={walkto@other}},
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}},
save={walkto@others@ancestors}{load={walkto@other},ancestors},
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}
为了在 访问的节点之间绘制箭头walk to
,我们开发了一种通用样式,使其能够轻松地对节点漫步邻居进行某些操作。在 的每个步骤(但第一步除外)between nodewalk steps=<nodewalk><node keys>
执行;至关重要的是,在 内,并引用节点漫步的前一个和当前节点的名称。(请注意,下面是节点漫步的第一步。)<node keys>
<nodewalk>
<node keys>
#1
#2
current
\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}
这几乎给出了我们所需要的。想象一下,我们想要在边缘上精确地绘制箭头。我们需要说\draw(#1.north)--(#2.south)}
在向上和\draw(#1.south)--(#2.north)}
向下的路上。一般来说,我们可能需要参考最低公共祖先 (LCA) 本身,或者从当前节点到 LCA 或从 LCA 到最终节点的子路径。这正是 nodewalk 步骤lowest common ancestor
、walk to lowest common ancestor
和所实现的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}
最后,为了便于使用和加快编译速度,我们定义了between nodewalk steps in walk to=<nodewalk><every step up to LCA><every step down from LCA>
,其工作原理与 相同,between nodewalk steps
但对路径的两个部分采用两个单独的键列表。(在下面的示例中,我也对箭头进行了些许移动。)
\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}
我很想知道您是否认为这些密钥应该是软件包本身的一部分。
更新 #2: 使用爱好
采纳 cfr 评论中的建议,我修改了上述解决方案,以便能够将其用于爱好。
为了使用 hobby,我们需要构建以下标记列表:
\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);
使用上述解决方案来实现这一点有点困难,因为从第一个(当前)到最后一个(给定)节点的路径包含下面列出的五个部分,每个部分都以不同的方式为爱好规范做出贡献。
第一个节点
第一个节点和 LCA 之间的节点
生命周期评估
LCA 和最后一个节点之间的节点
最后一个节点
下面的实现只有一个(多步骤)节点行走键,walk from to
它接受两个参数,第一个节点和最后一个节点(更准确地说:从当前节点到那些节点的节点行走),并从第一个节点到最后一个节点(包括两个节点)行走整个路径,但也将此路径的相关部分保存到不同的已保存的节点行走中,以供load
用户根据需要进行编辑。
load=walk first
load=walk from first to LCA
load=walk LCA
load=walk from LCA to last
load=walk last
有了这个,构建爱好代码至少是可管理的;请参阅hobby curve
下面的样式。
\documentclass[tikz,border=5pt,multi]{standalone}
\usepackage[linguistics]{forest}
\usetikzlibrary{hobby}
\begin{document}
\forestset{%
walkfromto@toLCA/.nodewalk style={
save={walkfromto@anc}{load={walk last},ancestors},
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}{
fake={load={walk last},parent},
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,
load={walk last},
},
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)}},
for load={walk LCA}{
pass through={([yshift=4.5pt]##1.south)}},
for load={walk from LCA to last}{
pass through={([yshift=2.5pt]##1.north west)}},
between nodewalk steps={load=walk first,load=walk last}{
!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}
答案2
编辑 编辑现在已做出调整。
编辑现在有了风格。
下面提供了一种在两个节点之间构建节点行走并使用某种样式在它们之间绘制路径的方法。visit process
支持创建自定义样式。
要建立一种新风格,visit <foo>
需要两件事。
visit <foo>={%
visit process={#1}{<foo>},
}
应该简单地将其参数传递给visit process
具有相关名称的<foo>
。此外,visit <foo> using
应该定义一个带有两个参数的样式,即要访问的节点的引用和样式的自定义。
visit <foo> using/.style n args=2{%
这应该visit trace
与节点引用一起调用,以构建到要访问的节点的相关路径。
visit trace={#1}{.north west}{.north east}{.children}{},
第一个参数是节点引用。第二个参数是用于共同祖先之前的中间节点的锚点。第三个参数是用于共同祖先和要访问的节点之间的中间节点的锚点。第四个参数是用于共同祖先的锚点。第五个参数是使用曲线处理列表时对峰值节点的调整hobby
(例如[yshift=2.5pt]
- 注意方括号)。否则不应指定。这些参数定义了用于创建要连接的点列表的坐标。
before typesetting nodes={
这种风格的其余部分应该定义 Ti钾Z代码绘制当前节点和要访问的节点之间的路线。
tikz+/.process={%
RRw2{visit me}{visit keylist a}{%
\draw [visit style, #2] (.parent) \foreach \i in {##2} { -- (\i) } -- (##1.parent);
},
},
}
},
以下内容可用于创建 Ti钾Z 代码:
visit keylist a
是一串不带括号的坐标或节点列表,适合与 一起使用。这些将采用或 的\foreach
形式,具体取决于 给出的选项。<node>.<anchor>
<node>
visit trace
visit me
是要访问的节点的名称。visitees
是经过处理的坐标或节点列表,适合传递给曲线。这些将采用或 的hobby
形式,峰值节点指定为。(<node>.<anchor>) .. (<node>.<anchor) ..
(<node>) .. (<node>) .. (<node>) ..
(<adjustment><node>.<anchor>)
visit <foo>
然后将提供一种采用一个参数的样式。这可以是一个简单的节点引用,也可以是节点引用后跟一个冒号和自定义样式选项。
visit <foo>=<node-to-be-visited>:{style options}
此外,visit style=<style options>
可用于为所有访问设置默认样式。
作为示例,提供了以下三种访问风格:
visit plain
:使用直线并经过适当的中间点,绘制从这里到那里的连续路径;visit steps
:从这里画直线路径到第一个中间节点,从该节点到下一个节点等等,直到到达那里。visit hobby
:绘制一条从这里到那里的平滑曲线作为连续的路径,经过必要的中间点。
此外,以下内容可用于这些样式的垂直调整。
visit hobby peak yshift
:用于将曲线峰值hobby
上移的尺寸。visit yshift
plain
:改变和样式中的所有中间节点的维度steps
。
结果是
\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}
生产
完整代码:
\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}}},
split register={visit keylist a}{,}{add visitee},
},
},
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},
}%
},
add visitee/.style={%
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}