如何在 graphviz/pydot 中实施网格布局?
How to enforce grid layout in graphviz/pydot?
tl;dr:如何使 graphviz 坚持节点的网格布局?
我正在尝试为时间序列绘制 "full causal graph"。这意味着我有一些图表,其中单位和时间索引在时间方向上重复。
我想用 Graphviz 绘制图形,因为它是程序化的。我不知道单位的数量,也不知道时间步长的数量。随着项目的继续,这将有所不同。我可能还想以编程方式调整颜色、笔划宽度等,作为机器学习模型的可视化。
为了使图表易于阅读,我需要考虑一些布局注意事项:
- 单位按行排列
- 列中的时间索引
- 图中的边周期性地重复(在下图中橙色是垂直的,蓝色是 1 个时间步长,棕色是两个时间步长)
因此,我正在尝试复制此 powerpoint 模型。
为了实现这一点,我从一些'SO post中获得了灵感,并添加了带有 rank=same
的子图以及不可见的边。 post 显示:
从其他 SO posts,我已经能够以我喜欢的方式订购我的节点。当前输出如下。由于我使用的是 pydot
,因此 python 代码和点代码非常难看。我会根据要求 link。
如您所见,除了一些怪癖外,一切正常:
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
#
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];
}
}
我认为这种情况下的诀窍是指定完整的(网格)图,然后使不需要的部分不可见。
这是您的案例的一个最小示例。 (我刚刚省略了颜色。)
digraph{
# Columns
subgraph {
"0_0" [style=invis]
"0_1" [style=invis]
"0_2" [style=invis]
}
subgraph {
"1_0"
"1_1"
"1_2"
}
subgraph {
"2_0"
"2_1"
"2_2"
}
subgraph {
"3_0"
"3_1"
"3_2"
}
subgraph {
"4_0" [style=invis]
"4_1" [style=invis]
"4_2" [style=invis]
}
# Rows
subgraph {
rank=same
"0_0"
"1_0"
"2_0"
"3_0"
"4_0"
}
subgraph {
rank=same
"0_1"
"1_1"
"2_1"
"3_1"
"4_1"
}
subgraph {
rank=same
"0_2"
"1_2"
"2_2"
"3_2"
"4_2"
}
# Straight edges
"0_0" -> "1_0"
"0_1" -> "1_1"
"0_2" -> "1_2"
"1_0" -> "2_0"
"1_1" -> "2_1"
"1_2" -> "2_2"
"2_0" -> "3_0"
"2_1" -> "3_1"
"2_2" -> "3_2"
"3_0" -> "4_0"
"3_1" -> "4_1"
"3_2" -> "4_2"
"0_0" -> "0_1" [style=invis]
"1_0" -> "1_1"
"2_0" -> "2_1"
"3_0" -> "3_1"
"4_0" -> "4_1" [style=invis]
"0_1" -> "0_2" [style=invis]
"1_1" -> "1_2" [style=invis]
"2_1" -> "2_2" [style=invis]
"3_1" -> "3_2" [style=invis]
"4_1" -> "4_2" [style=invis]
# Diagonal edges
"0_0" -> "1_1"
"0_0" -> "2_1"
"1_0" -> "3_1"
"2_0" -> "4_1"
"0_1" -> "1_2"
"1_1" -> "2_2"
"2_1" -> "3_2"
"3_1" -> "4_2"
}
Graphviz output
tl;dr:如何使 graphviz 坚持节点的网格布局?
我正在尝试为时间序列绘制 "full causal graph"。这意味着我有一些图表,其中单位和时间索引在时间方向上重复。
我想用 Graphviz 绘制图形,因为它是程序化的。我不知道单位的数量,也不知道时间步长的数量。随着项目的继续,这将有所不同。我可能还想以编程方式调整颜色、笔划宽度等,作为机器学习模型的可视化。
为了使图表易于阅读,我需要考虑一些布局注意事项:
- 单位按行排列
- 列中的时间索引
- 图中的边周期性地重复(在下图中橙色是垂直的,蓝色是 1 个时间步长,棕色是两个时间步长)
因此,我正在尝试复制此 powerpoint 模型。
为了实现这一点,我从一些'SO post中获得了灵感,并添加了带有 rank=same
的子图以及不可见的边。 post 显示:
从其他 SO posts,我已经能够以我喜欢的方式订购我的节点。当前输出如下。由于我使用的是 pydot
,因此 python 代码和点代码非常难看。我会根据要求 link。
如您所见,除了一些怪癖外,一切正常:
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
#
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];
}
}
我认为这种情况下的诀窍是指定完整的(网格)图,然后使不需要的部分不可见。 这是您的案例的一个最小示例。 (我刚刚省略了颜色。)
digraph{
# Columns
subgraph {
"0_0" [style=invis]
"0_1" [style=invis]
"0_2" [style=invis]
}
subgraph {
"1_0"
"1_1"
"1_2"
}
subgraph {
"2_0"
"2_1"
"2_2"
}
subgraph {
"3_0"
"3_1"
"3_2"
}
subgraph {
"4_0" [style=invis]
"4_1" [style=invis]
"4_2" [style=invis]
}
# Rows
subgraph {
rank=same
"0_0"
"1_0"
"2_0"
"3_0"
"4_0"
}
subgraph {
rank=same
"0_1"
"1_1"
"2_1"
"3_1"
"4_1"
}
subgraph {
rank=same
"0_2"
"1_2"
"2_2"
"3_2"
"4_2"
}
# Straight edges
"0_0" -> "1_0"
"0_1" -> "1_1"
"0_2" -> "1_2"
"1_0" -> "2_0"
"1_1" -> "2_1"
"1_2" -> "2_2"
"2_0" -> "3_0"
"2_1" -> "3_1"
"2_2" -> "3_2"
"3_0" -> "4_0"
"3_1" -> "4_1"
"3_2" -> "4_2"
"0_0" -> "0_1" [style=invis]
"1_0" -> "1_1"
"2_0" -> "2_1"
"3_0" -> "3_1"
"4_0" -> "4_1" [style=invis]
"0_1" -> "0_2" [style=invis]
"1_1" -> "1_2" [style=invis]
"2_1" -> "2_2" [style=invis]
"3_1" -> "3_2" [style=invis]
"4_1" -> "4_2" [style=invis]
# Diagonal edges
"0_0" -> "1_1"
"0_0" -> "2_1"
"1_0" -> "3_1"
"2_0" -> "4_1"
"0_1" -> "1_2"
"1_1" -> "2_2"
"2_1" -> "3_2"
"3_1" -> "4_2"
}
Graphviz output