【发布时间】:2019-08-06 17:00:36
【问题描述】:
tl;dr:如何让 graphviz 坚持节点的网格布局?
我正在尝试为时间序列绘制“完整因果图”。这意味着我有一些图表,其中单位和时间索引在时间方向上重复。
我想用 Graphviz 绘制图形,因为它是程序化的。我不知道单位数,也不知道时间步数。随着项目的继续,这将有所不同。我可能还想以编程方式调整颜色、笔画宽度等,作为机器学习模型的可视化。
为了使图表易于阅读,我需要考虑一些布局注意事项:
- 按行对齐的单位
- 列中的时间索引
- 图表中的边周期性重复(下图中橙色为垂直,蓝色为 1 个时间步宽,棕色为两个时间步宽)
为了实现这一点,我从一些“SO”帖子中获得了灵感,并添加了带有rank=same 的子图以及不可见的边缘。这个帖子显示了它:https://stackoverflow.com/a/49736304/4050510
从其他 SO 帖子中,我能够以我喜欢的方式对节点进行排序。当前输出如下所示。由于我使用的是pydot,python 代码和点代码非常难看。我会根据要求链接到它。
如您所见,除了一些怪癖之外,一切正常:
1) 不可见节点与可见节点不对齐 1)橙色箭头是弯曲的,因为它们与看不见的箭头相撞
有什么方法可以让 Graphviz 优雅地处理这个问题? 如何强制网格布局,如何使橙色箭头笔直?
上图的 Pydot 源代码
import io
import pydot
import matplotlib.image as img
import matplotlib.pyplot as plt
def render_pydot(g: pydot.Dot, prog):
# noinspection PyUnresolvedReferences
png_bytes = g.create(prog=prog, format="png")
bytes_as_inmemory_file = io.BytesIO(png_bytes)
img2 = img.imread(bytes_as_inmemory_file)
plt.figure()
plt.imshow(img2, aspect='equal')
plt.axis(False)
plt.grid(False)
plt.show()
def create_dot_for_timeseries_with_pydot():
"""Generate a dot object for a static 'full time series'"""
g = pydot.Dot(rankdir='LR')
units = ["Alfa", "Beta", "Gamma"]
time_steps = list(range(0, 5)) # five steps, two invisible
for t in time_steps:
sg = pydot.Subgraph(rank="same", rankdir="TB")
for u, _ in enumerate(units):
# create nodes
this_node_name = f"{t}_{u}"
opts = {'name': this_node_name,
'label': this_node_name
}
if t not in time_steps[1:-1]:
opts['style'] = 'invis'
opts['color'] = 'gray70'
n = pydot.Node(**opts)
# create invisible edges to enforce order vertically and horizontally
# https://stackoverflow.com/q/44274518/4050510
if u != 0:
prev = f"{t}_{u - 1}"
e = pydot.Edge(src=prev, dst=this_node_name,
style='invis',
color="gray70",
weight=1000)
sg.add_edge(e)
if t in time_steps[:-1]:
next = f"{t + 1}_{u}"
g.add_edge(pydot.Edge(src=this_node_name, dst=next,
style="invis",
color="gray70", weight=1000))
sg.add_node(n)
g.add_subgraph(sg)
# Draw lag 0 effects
if t in time_steps[1:-1]:
g.add_edge(pydot.Edge(f"{t}_{0}", f"{t}_{1}", color="orange"))
# Draw lag 1 effects
if t in time_steps[:-1]:
for u, _ in enumerate(units):
g.add_edge(pydot.Edge(f"{t}_{u}", f"{t + 1}_{u}", color="blue"))
g.add_edge(pydot.Edge(f"{t}_{0}", f"{t + 1}_{1}", color="blue"))
g.add_edge(pydot.Edge(f"{t}_{1}", f"{t + 1}_{2}", color="blue"))
# Draw lag 2 effects
if t in time_steps[:-2]:
g.add_edge(pydot.Edge(f"{t}_{0}", f"{t + 2}_{1}", color="brown"))
return g
g = create_dot_for_timeseries_with_pydot()
print(g) # print the dot document as text for inspection
render_pydot(g, prog='dot') # show the image
从上面的 python 文件生成的 DOT 代码
digraph G {
rankdir=LR;
splines=False;
"0_0" -> "1_0" [color=gray70, style=invis, weight=1000];
"0_1" -> "1_1" [color=gray70, style=invis, weight=1000];
"0_2" -> "1_2" [color=gray70, style=invis, weight=1000];
subgraph {
rank=same;
rankdir=TB;
"0_0" [color=gray70, label="0_0", style=invis];
"0_0" -> "0_1" [color=gray70, style=invis, weight=1000];
"0_1" [color=gray70, label="0_1", style=invis];
"0_1" -> "0_2" [color=gray70, style=invis, weight=1000];
"0_2" [color=gray70, label="0_2", style=invis];
}
"0_0" -> "1_0" [color=blue];
"0_1" -> "1_1" [color=blue];
"0_2" -> "1_2" [color=blue];
"0_0" -> "1_1" [color=blue];
"0_1" -> "1_2" [color=blue];
"0_0" -> "2_1" [color=brown];
"1_0" -> "2_0" [color=gray70, style=invis, weight=1000];
"1_1" -> "2_1" [color=gray70, style=invis, weight=1000];
"1_2" -> "2_2" [color=gray70, style=invis, weight=1000];
subgraph {
rank=same;
rankdir=TB;
"1_0" [label="1_0"];
"1_0" -> "1_1" [color=gray70, style=invis, weight=1000];
"1_1" [label="1_1"];
"1_1" -> "1_2" [color=gray70, style=invis, weight=1000];
"1_2" [label="1_2"];
}
"1_0" -> "1_1" [color=orange];
"1_0" -> "2_0" [color=blue];
"1_1" -> "2_1" [color=blue];
"1_2" -> "2_2" [color=blue];
"1_0" -> "2_1" [color=blue];
"1_1" -> "2_2" [color=blue];
"1_0" -> "3_1" [color=brown];
"2_0" -> "3_0" [color=gray70, style=invis, weight=1000];
"2_1" -> "3_1" [color=gray70, style=invis, weight=1000];
"2_2" -> "3_2" [color=gray70, style=invis, weight=1000];
subgraph {
rank=same;
rankdir=TB;
"2_0" [label="2_0"];
"2_0" -> "2_1" [color=gray70, style=invis, weight=1000];
"2_1" [label="2_1"];
"2_1" -> "2_2" [color=gray70, style=invis, weight=1000];
"2_2" [label="2_2"];
}
"2_0" -> "2_1" [color=orange];
"2_0" -> "3_0" [color=blue];
"2_1" -> "3_1" [color=blue];
"2_2" -> "3_2" [color=blue];
"2_0" -> "3_1" [color=blue];
"2_1" -> "3_2" [color=blue];
"2_0" -> "4_1" [color=brown];
"3_0" -> "4_0" [color=gray70, style=invis, weight=1000];
"3_1" -> "4_1" [color=gray70, style=invis, weight=1000];
"3_2" -> "4_2" [color=gray70, style=invis, weight=1000];
subgraph {
rank=same;
rankdir=TB;
"3_0" [label="3_0"];
"3_0" -> "3_1" [color=gray70, style=invis, weight=1000];
"3_1" [label="3_1"];
"3_1" -> "3_2" [color=gray70, style=invis, weight=1000];
"3_2" [label="3_2"];
}
"3_0" -> "3_1" [color=orange];
"3_0" -> "4_0" [color=blue];
"3_1" -> "4_1" [color=blue];
"3_2" -> "4_2" [color=blue];
"3_0" -> "4_1" [color=blue];
"3_1" -> "4_2" [color=blue];
subgraph {
rank=same;
rankdir=TB;
"4_0" [color=gray70, label="4_0", style=invis];
"4_0" -> "4_1" [color=gray70, style=invis, weight=1000];
"4_1" [color=gray70, label="4_1", style=invis];
"4_1" -> "4_2" [color=gray70, style=invis, weight=1000];
"4_2" [color=gray70, label="4_2", style=invis];
}
}
【问题讨论】:
-
带有您的代码的 MWE 将是有利的。
-
我不确定在上面的例子中什么被认为是“最小的”,所以我提供了包含图表的完整源代码。
-
最好是
dot代码。关于弯曲线,splines=false可能有帮助(参见 a.o. stackoverflow.com/questions/3006792/…) -
现在包括点代码。它非常难看,因为它是从
pydot生成的。splines=false没有解决问题。