我面临以下问题:我想创建一种森林风格,使森林边缘的长度取决于边缘上标签的长度。为了得到节省空间的树木,同时按级别对齐,我希望边缘的长度取决于最大标签大小在每个层面上树的。
为了解决这个问题,我从针对该问题提供的解决方案开始:
我稍微调整了这个例子,以计算整棵树中的最大标签大小,并得出以下解决方案,将边缘大小调整为出现在整棵树:
\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}
不过,我真的希望长度能够根据等级。我的第一个想法是,这应该很容易实现,不用变量\mymaxlabelwidth
,而是用一个这样的变量数组来计算(然后访问)每个级别的最大值。不幸的是,我没能实现这个想法。我尝试了几种在 latex 中实现数组的方法(例如,使用pgffor
或pgfkeys
),但没有一种对我有用(我想是因为相应数组的范围不正确)。我也对进行了一些尝试expl3
,但可能是因为我对此缺乏了解,所以我没能实现所需的功能。
如果有人能帮助我解决这个问题,那就太好了!非常感谢!
答案1
这个答案超出了OP的要求。它不仅实现了每级调整,而且还尝试计算l
需要更可靠地适合边缘标签。
让我们从级别开始。正如 OP 所建议的,我们需要存储多个边标签的标签宽度,最简单的方法是声明一个新的森林(维度)选项:declare dimen={my edge label width}{0pt}
。森林选项自动与节点相关,因此数据结构立即到位。处理数据的工具也是如此;特别是,我们将利用聚合函数max
。
实际上,下面的代码实现了三种样式,不同之处在于拟合的“局部性”。 和 之间的区别fit edge labels children
从fit edge labels level
下fit edge labels tree
图中应该很明显。
这三种风格都使用 Forest 的聚合函数max
(处理程序形式为.max
)来计算所需的l
,并使用空间传播器在相关节点for ...
设置 的值l
。不同之处在于.max
聚合的内容:children
或;这些相同的节点行走(形式为)也用于将 的新值分发level=...
到相关节点。descendants
for ...
l
现在讨论另一个问题,即更可靠地计算l
。原始代码的工作原理是假设l
和s
坐标(父节点和子节点的坐标)是边缘端点的良好近似值。并且在某种程度上,它们是,并且可以手动修复差异 --- 这是+ 10pt
原始代码中实现的。但如果我们精确的话,l
和s
是节点的 的坐标anchor
,而边缘的端点对应于parent anchor
父节点的 和child anchor
子节点的 。
但我们如何才能坐标父节点和子节点的宽度是多少?为了得到它们,下面的代码实际上排版了父节点和子节点(当然,结果会被丢弃)。这是通过宏 完成的\measureedge
,这个宏是从我们定义的 pgfmath 函数 调用的l_for_my_edge_label
,它使用 提供的坐标\measureedge
和 中存储的边缘标签的宽度my edge label width
来计算l
边缘标签适合所需的 。有关更多详细信息,请参阅代码中的注释。
然而,即使所有这些也不能给我们带来完全万无一失的解决方案。当parent/child anchor
为空(默认值)时,TikZ 会将边缘绘制到一个神奇确定的边界点,而当我们“移动”节点(通过调整l
)时,接触点可能会发生变化。幸运的是,(i)效果并不十分明显,只有在宽节点上才能看到;(ii)当我们使用固定的父/子锚点时,问题就会消失;(iii)手动解决方法非常简单。有关详细信息,请参阅第一棵树的代码中的注释 (2)。
最后,请注意代码(l_for_my_edge_label
特别是)假设树水平生长。好吧,对于向西生长,它开箱即用;对于向东生长,在定义中交换{above}
和以翻转边缘标签的位置。{below}
my edge label a
\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}