首页 > 解决方案 > 如何在 graphviz/pydot 中强制执行网格布局?

问题描述

tl; dr:如何让 graphviz 坚持节点的网格布局?

我正在尝试为时间序列绘制“完整因果图”。这意味着我有一些图表,其中单位和时间索引在时间方向上重复。

我想用 Graphviz 绘制图形,因为它是程序化的。我不知道单位数,也不知道时间步数。随着项目的继续,这将有所不同。我可能还想以编程方式调整颜色、笔画宽度等,作为机器学习模型的可视化。

为了使图表易于阅读,我需要考虑一些布局注意事项:

因此,我正在尝试复制这个 powerpoint 模型。 我想画什么

为了实现这一点,我从一些“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];
}
}

标签: graphvizpydot

解决方案


我认为这种情况下的技巧是指定完整的(网格)图,然后使不需要的部分不可见。这是您的案例的一个最小示例。(我刚刚省略了颜色。)

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 输出


推荐阅读