我想画出类似这样的图。从数学上讲,你可以将它们描述为带有悬垂弧的有向无环图。它们有点让人想起路线图。
期望输出
来源这里。图的顶部有四条悬垂的线,然后有四个节点对这些线进行操作,最后有三条悬垂的线从图的底部伸出。我知道如何在 TikZ 中创建这个特定的图表,但我想制作一个用于创建它们的包。
所需输入
对我来说,重要的是:
- 用户需要指定尽可能少的内容,以便于创建和修改大图表,并且
- 该图是按逐行方式组成的。
这是我要的代码喜欢我的用户输入,以便生成上面的图片(我在下一小节中解释代码):
\begin{wickersondiagram}[%
initial lines={a/{pos=10}, b/{pos=30}, c/{pos=50}, d/{pos=70}}]
\step[left=0, right=40, text=decide,%
incoming lines={a,b},%
outgoing lines={e/{pos=10,label=yes}, f/{pos=30,label=no}}]
\\
\step[left=20, right=60,%
pale,%
incoming lines={f,c},%
outgoing lines={g/{pos=30},h/{pos=50}}]
\\
\step[left=0, right=40, text=join,%
incoming lines={e,g},%
outgoing lines={i/{pos=20}}]
\step[left=60, right=80,%
incoming lines={d},%
outgoing lines={j/{pos=70}}]
\\
\end{wickersondiagram}
我所说的“逐行”是指,如果我从该代码中删除最后几行,我仍然会获得一张完全有效的图片,它看起来就像上面的图片,但缺少了下部。
所需输入的解释
用户通过指定 的水平位置来启动图表。initial lines
每条线都分配有一个名称,这里是a
、b
和。在构建图表时,我的包应该保持一个隐式“状态”,其中包含线条的名称及其位置。在构建的这一点上,状态是c
d
{a ↦ 10, b ↦ 30, c ↦ 50, d ↦ 70}.
然后用户创建一个\step
。水平位置用left
和键指定。步骤上right
有可选的,在本例中是“决定”。从状态中移除,并将添加到状态中。此时状态变为:text
incoming lines
outgoing lines
{e ↦ 10, f ↦ 30, c ↦ 50, d ↦ 70}.
维护状态的目的是,无需用户不断指定,即可绘制与特定步骤无关的线条。例如,包应该隐式地在“决定”步骤的右侧绘制线条c
和。d
有时两个或多个步骤会同时发生,如上图底部所示。我建议在语法中通过使用\\
明确开始新的“行”步骤来处理此问题。
我目前的方法
到目前为止,我的方法是尝试将状态存储在一系列宏中,例如:
\def\state@line@a{10}
\def\state@line@b{30}
\def\state@line@c{50}
\def\state@line@d{70}
\def\state@line@e{10}
\def\state@line@f{30}
然后维护当前处于该状态的行的逗号分隔列表,例如:
\def\activelines{e,f,c,d}
我之前的两个问题(这里和这里),这是我尝试让这种方法发挥作用的结果。在这些问题中,我将代码压缩为最小的工作示例,以展示我面临的特定技术问题。我感谢回答者如此彻底和周到地解决这些问题。但是,由于每次都是同一个人回答我的问题,我怀疑我最好直奔主题,展示我真正在做什么。我仍然对我的问题进行了一些理想化,以便于解释,但我希望保留了大部分技术挑战。
扩展
我希望最终将我的图表扩展为比上面示例更复杂的图表。例如:
我希望我的线条具有可定制的粗细(因此“状态”的元素实际上应该是元组而不是单个整数)。
我想添加一个命令来“扭曲”两条或多条线,交换它们的水平位置。
用户当前必须为每个步骤指定一个
left
和right
位置,但我最终希望根据传入和传出线的位置自动计算这些。我想添加包含图表的特殊“复合”节点。
我想添加另一种类型的线。
我想添加一个命令来拍摄当前“状态”的快照,以及一个命令来恢复以前的“状态”。
因此,对我来说,重要的是您的解决方案足够强大且易于理解,以便我可以对它们进行大量摆弄!
答案1
借助几个子问题[1,2,3,4,5],我设法拼凑出了一个合理的解决方案。
用户输入已略作修改:现在可以\row
在每行的开头而不是\\
结尾输入。
这是我的代码:
\documentclass{article}
\usepackage{tikz}
\usepackage{etextools}
\makeatletter
\pgfkeys{/wickerson/.cd,
execute style/.style = {#1},
execute macro/.style = {execute style/.expand once=#1},
pos/.code= {\xdef\wickerson@pos{#1}},
left/.code= {\xdef\wickerson@left{#1}},
right/.code= {\xdef\wickerson@right{#1}},
label/.code= {\xdef\wickerson@label{#1}},
text/.code= {\xdef\wickerson@text{#1}},
pale/.code= {\xdef\wickerson@color{red!50}},
scale/.code= {\xdef\wickerson@scale{#1}},
initial lines/.code= {\xdef\wickerson@initiallines{#1}},
incoming lines/.code={\xdef\wickerson@incominglines{#1}},
outgoing lines/.code={\xdef\wickerson@outgoinglines{#1}},
}
\def\wickerson@color{red}
\newcommand*\@defineLine[2]{%
%\typeout{Defining wickerson@#1@pos = #2}
\expandafter\xdef\csname wickerson@#1@pos\endcsname{#2}
}
% Project the pos-field from the line with the given
% identifier. For instance, if \wickerson@foo@pos=35 then
% \@getLinePos{foo}
% will give 35.
\gdef\@getLinePos#1{\csname wickerson@#1@pos\endcsname}
% For tracking the distance from the top of the diagram.
\newcounter{VCursor}
% Default step height
\newcommand{\stepHeight}{10}
% Height of the current row.
\newcounter{RowHeight}
% A comma-separated list of active lines. Each element is a line identifier.
\gdef\@activeLines{}
% Add the given identifier to the list of active lines.
\newcommand*\@addToActiveLines[1]{%
\ifx\@activeLines\@empty\else\g@addto@macro\@activeLines{,}\fi
\g@addto@macro\@activeLines{#1}
}
\newcommand*\@removeFromActiveLines[1]{%
\@expandtwoargs\@removeelement{#1}\@activeLines\@activeLines
\global\let\@activeLines=\@activeLines
}
\newcommand*\row[1][20]{%
\setcounter{RowHeight}{#1}
\addtocounter{VCursor}{\theRowHeight}%
\typeout{Starting row. Drawing lines \@activeLines}
\foreach\i in \@activeLines {%
\draw[black] (\@getLinePos\i,\theVCursor)
-- (\@getLinePos\i,\theVCursor-\theRowHeight);
\node[text=black!25,anchor=west] at
(\@getLinePos\i,\theVCursor-\theRowHeight+5) {\strut \i};
}
}
\newcommand*\@printState{%
\typeout{Current state:}
\foreach\i in \@activeLines {%
\typeout{ (\i, \@getLinePos\i)}
}
\typeout{Current activeLines:}
\show\@activeLines
}
\newcommand*\@drawLineLabel[2]{%
\node[text=black,anchor=east] at
(#1,\theVCursor+5) {\strut #2};
}
\newcommand*\step[1][]{%
\typeout{Starting step.}
\pgfkeys{/wickerson/.cd,left=0,right=0,text={},%
incoming lines={},outgoing lines={},#1}
\fill[\wickerson@color] (\wickerson@left,\theVCursor-\stepHeight)
rectangle (\wickerson@right,\theVCursor);
\gdef\wickerson@color{red}
\node at (\wickerson@left*0.5 + \wickerson@right*0.5,
\theVCursor - 0.5*\stepHeight) {\wickerson@text};
\foreach\i in \wickerson@incominglines {
\typeout{Removing line \i}
\ExpandNext\@removeFromActiveLines\i
}
\foreach \i/\values in \wickerson@outgoinglines {
\pgfkeys{/wickerson/.cd,pos=0,label={},execute macro=\values}
\typeout{Adding (\i,\wickerson@pos) to the state.}
\ExpandNextTwo\@defineLine\i\wickerson@pos
\ExpandNext\@addToActiveLines\i
\ExpandNextTwo\@drawLineLabel\wickerson@pos\wickerson@label
}
\@printState
}
\newenvironment{wickersondiagram}[1][]{%
\typeout{Starting diagram.}
\pgfkeys{/wickerson/.cd,scale=1,initial lines={},#1}
\setcounter{VCursor}{0}%
\begin{tikzpicture}[x=1mm,y=-1mm]
\foreach \i/\values in \wickerson@initiallines {
\pgfkeys{/wickerson/.cd,pos=0,label={},execute macro=\values}
\typeout{Adding (\i,\wickerson@pos) to the state.}
\ExpandNextTwo\@defineLine\i\wickerson@pos
\ExpandNext\@addToActiveLines\i
\@printState
}
}{%
\end{tikzpicture}
}
\makeatother
\begin{document}
\begin{wickersondiagram}[%
initial lines={a/{pos=10}, b/{pos=30}, c/{pos=50}, d/{pos=70}}]
\row
\step[left=0, right=40, text=decide,%
incoming lines={a,b},%
outgoing lines={e/{pos=10,label=yes}, f/{pos=30,label=no}}]
\row
\step[left=20, right=60,%
pale,%
incoming lines={f,c},%
outgoing lines={g/{pos=30},h/{pos=50}}]
\row
\step[left=0, right=40, text=join,%
incoming lines={e,g},%
outgoing lines={i/{pos=20}}]
\step[left=60, right=80,%
incoming lines={d},%
outgoing lines={j/{pos=70}}]
\row[10]
\end{wickersondiagram}
\end{document}
这是输出。灰色字母是赋予行的内部名称,用于调试目的。
我鼓励任何人改进此代码,如果他们愿意的话。因此,我做出了这个回答社区维基。