如何在graphviz / pydot中强制执行网格布局?

时间:2019-08-06 17:00:36

标签: graphviz pydot

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

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

我想用Graphviz绘制图形,因为它是程序性的。我不知道单位数,也不知道时间步数。随着项目的继续,这将有所不同。我可能还希望通过编程方式来调整颜色,笔划宽度等,以使机器学习模型更具生气。

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

  • 单位在行中对齐
  • 列中的时间索引
  • 图中的边缘周期性地重复(在下面的图像中,橙色是垂直的,蓝色是1步宽,棕色是2步宽)

因此,我正在尝试复制此PowerPoint样机。 What i'm trying to draw

为此,我从一些'SO帖子中得到了启发,并添加了带有rank=same和不可见边缘的子图。这篇文章显示了它:https://stackoverflow.com/a/49736304/4050510

从其他SO帖子中,我已经能够按照自己喜欢的方式订购节点。当前输出如下。由于我使用的是pydot,因此python代码和点代码非常难看。我将根据要求链接到它。

enter image description here

如您所见,除了一些古怪之处,它都可以工作:

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];
}
}

1 个答案:

答案 0 :(得分:2)

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

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