Cycoe@Home

使用 Emacs 与 Graphviz 绘制流程图

2020-03-11_10-40-29_app.png

前几天在写 使用 Emacs 与 Org mode 进行 GTD 管理 博客时,用到了 Graphviz 绘制流程 图。正巧之前在 LaTeX 中也用它画过一些图,但都没有系统地入过门,那就趁最近学习一 下吧。

从官网上可以看到,Graphviz 的全称为Graph可视化 Visualization工具Software,这里的图说的应该不是数学意 义上的函数图像那种图,而是数据结构中的图,强调的关系。

1. 安装

1.1. Graphviz

Graphviz 本质上是一个命令行工具,在 Archlinux 上只需要通过 sudo pacman -S graphviz 安装即可,其他系统类似。

1.2. graphviz-dot-mode

Graphviz 在 Emacs 中的集成是通过 graphviz-dot-mode 这个包来实现的,只需要 <M-x> package-install graphviz-dot-mode <RET> 即可。

然后在 Emacs 配置中加入以下内容来启用

(use-package graphviz-dot-mode
  :ensure t
  :config
  (setq graphviz-dot-indent-width 4))

(use-package company-graphviz-dot)

2. 布局器

Graphviz 中包含了众多的布局器:

  • dot 默认布局方式,主要用于有向图
  • neato 基于 spring-model(又称 force-based)算法
  • twopi 径向布局
  • circo 圆环布局
  • fdp 用于无向图

3. 基本使用

Graphviz 构建组件为图,节点,边,用属性对其进行描述。

创建一个 dot 布局的图,可以使用以下代码

digraph first{
    a;
    b;
    c;
    d;
    a->b;
    b->d;
    c->d;
}
first.png

上述代码等价于

digraph first{
    a->b;
    b->d;
    c->d;
}

如果要直接在命令行进行绘图,使用命令 dot -Tpng first.dot -o first.png 。如果利 用 Emacs Babel 进行绘制,需要如下所示的 Headers。

#+begin_src dot :file first.png :cmdline -Tpng :results silent
  digraph first{
      a->b;
      b->d;
      c->d;
  }
#+end_src

其中, :results silent 表示不在 Org 文件中进行任何输出。

4. 绘制属性

一个图中可能有非常多的节点与边,如果每次都需要声明一个节点的属性会非常麻烦。类似 于面向对象编程的思想,我们可以用 nodeedge 类标签来设置全局的属性,也可以用对 象标签设置单个节点与边的属性。

digraph G{
    // 设置图片最大尺寸
    size = "40, 40";
    // 设置节点排布方向
    rankdir = LR;
    // 设置节点字体
    node[fontname="SimHei"];
    // 设置连线字体
    edge[fontname="SimHei"];

    // 设置默认节点属性
    node[shape=box, color=blue, style=unfilled];
    // 设置边的属性
    edge[color=red];

    // 节点
    小蓝;
    小红[color=red];
    // 边
    小蓝->小红; // 默认边属性
    小红->小蓝[color=blue, style=dashed]; // 自定义边属性
}

property.png

所有可用的属性见 官网,此处列举部分常用的属性

  • charset 编码,一般设置 UTF-8
  • fontname 字体名称,这个在中文的情况需要设置,否则导出图片的时候会乱码,一般设 置微软雅黑("Microsoft YaHei"), linux 下也是同样设置系统带的字体就好,其他字体 设置见fontpath 属性
  • fontcolor 字体颜色
  • fontsize 字体大小,用于文本内容
  • fillcolor 用于填充节点或者集群(cluster)的背景颜色。
  • size 图形的最大宽度和高度
  • label 图形上的文本标记
  • margin 设置图形的边距
  • pad 指定将绘制区域扩展到绘制图形所需的最小区域的长度(以英寸为单位)
  • style 设置图形组件的样式信息。 对于聚类子图或者节点,如果 style = filled,则 填充聚类框的背景

    Screenshot_20200311_143758_yfrBsd.png
  • rankdir 设置图形布局的排列方向 (全局只有一个生效). "TB", "LR", "BT", "RL", 分 别对应于从上到下,从左到右,从下到上和从右到左绘制的有向图。

    digraph {
        rankdir = RL;
        A->B->C[color=green];
        A->C[label="RL"];
    }
    
    rankdir.png
  • ranksep 以英寸为单位提供所需的排列间隔
  • ratio 设置生成图片的纵横比

4.1. 节点

节点的默认属性为 shape = ellipse, width = .75, height = 0.5 并且用节点标识符作为 节点的显示文字。

图 1 中所示,声明两个节点 小蓝小红/,/小蓝小红 就表示这个节点的节点标 识符,后面紧跟的是该节点的属性列表;另一种用法为 节点标识符:节点部分:方向[属性列 表] 小蓝:body[style=filled color=lightblue], 这个为单一节点声明的方式。

节点中的基本属性有

  • shape 形状,全部形状见 Graphviz 官网,一些常用的图形有 box, circle, ellipse, plaintext, square
  • width height, 图形的宽度和高度,如果设置了 fixedsize 为 true,则宽和高为最终的 长度
  • fixedsize 如果为false,节点的大小由其文本内容所需要的最小值决定
  • rank 子图中节点上的排列等级约束. 最小等级是最顶部或最左侧,最大等级是最底部或 最右侧。
    • same. 所有节点都位于同一等级
    • min. 所有节点都位于最小等级上
    • source. 所有节点都位于最小等级上,并且最小等级上的唯一节点属于某个等级 source 或 min 的子图
    • max sink. 和上类似

4.2.

有向图中的的边用 -> 表示,无向图用 -- 表示。

可以同时连接多个节点或者子图,但是只能有一个属性列表,如下

digraph {
    rankdir = LR
    A -> B -> c[color=green]
}
edge_color.png

一些关于边的属性如下:

  • len 首选边的长度
  • weight 边的权重, 权重越大越接近边的长度

    如果我们希望图中的绿色边为主要逻辑分支,需要设置 A->B->C->D->F 的权重最大,修改 绿色的分支的权重为 100,使其变成主要逻辑分支,修改后的效果如下

    digraph {
        rankdir = LR
        splines = ortho
    
        A -> B -> C -> D -> F [color=green, weight=100]
        E -> F -> B -> D [color=blue]
        B -> E -> H[color=red]
    }
    
    edge_weight.png
  • lhead 逻辑边缘的头部(箭头那个位置),compound 设置为 true 时,边被裁减到子图的边界处
  • ltail 类似 lhead
  • headlabel 边上靠近箭头部分的标签
  • taillabel 边上靠近尾部部分的标签
  • splines 控制如何显示边,取值可以是
    • none 或者 "", 无边

      digraph {
          rankdir = LR
          splines = none
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_none.png
    • true 或者 spline, 样条线(无规则,可为直或者曲线)

      digraph {
          rankdir = LR
          splines = spline
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_spline.png
    • false 或者 line, 直线

      digraph {
          rankdir = LR
          splines = line
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_line.png
    • polyline, 折线

      digraph {
          rankdir = LR
          splines = polyline
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_polyline.png
    • curved, 曲弧线,看起来像贝塞尔曲线

      digraph {
          rankdir = LR
          splines = curved
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_curved.png
    • ortho, 正交折线

      digraph {
          rankdir = LR
          splines = ortho
      
          A -> B -> C -> D -> F [color=green]
          E -> F -> B -> D [color=blue]
          B -> E -> H[color=red]
      }
      
      edge_splines_ortho.png

4.3. 边的方向

示例 声明多部分节点,以及对各部分进行单独设置

node0 [label = "<postid1> string|<postid2> string|<postid3> string3", height=.5];
node0:head[color=lightblue];
digraph action {
    node [shape = record,height=.1];

    node0 [label = "<head> head|<body> body|<foot> foot", height=.5]
    node2 [shape = box label="mind"]

    node0:head:n -> node2:n [label = "n"]
    node0:head:ne -> node2:ne [label = "ne"]
    node0:head:e -> node2:e [label = "e"]
    node0:head:se -> node2:se [label = "se"]
    node0:head:s -> node2:s [label = "s"]
    node0:head:sw -> node2:sw [label = "sw"]
    node0:head:w -> node2:w [label = "w"]
    node0:head:nw -> node2:nw [label = "nw"]
    node0:head:c -> node2:c [label = "c"] // center,中间
    node0:head:_ -> node2:_ [label = "_"] // 任意

    node0:body[style=filled color=lightblue]
}
direction.png

5. 子图

subgraph 必须配合 cluster 一起使用,用法为 =subgraph cluster* {} 。

需要设置 compound 为 true,则在群集之间留出边缘,子图的边界关系在 边 的定义中有给出,这里直接给个示例。

digraph G {
    compound = true  // 允许子图间存在边
    ranksep = 1
    node [shape = record]

    subgraph cluster_hardware {
        label = "hardware"
        color = lightblue
        CPU Memory
    }

    subgraph cluster_kernel {
        label = "kernel"
        color = green
        Init IPC
    }

    subgraph cluster_libc {
        label = "libc"
        color = yellow
        glibc
    }

    CPU -> Init [lhead = cluster_kernel ltail = cluster_hardware]
    IPC -> glibc [lhead = cluster_libc ltail = cluster_kernel]
}
subgraph.png
Author: Cycoe (cycoejoo@163.com)
Date: <2020-03-11 Wed 10:37>
Generator: Emacs 29.1 (Org mode 9.6.6)
Built: <2024-01-27 Sat 21:20>