• home > OMD > UML >

    数据可视化Graphviz程序化绘图—学习资料收藏整理笔记

    Author:zhoulujun Date:

    之前介绍过PlanUML,《程序猿专属UML画图工具:PlantUML简介及应用》以及在编辑器写UML,《UML开发语言plantuml如何应用到编辑工具—mac上

    之前介绍过PlanUML,《程序猿专属UML画图工具:PlantUML简介及应用》以及在编辑器写UML,《UML开发语言plantuml如何应用到编辑工具—mac上类visio工具》,本篇学习Graphviz,http://www.graphviz.org/about/

    一般的Graphviz 库都支持DOT, PlantUML, UMLGraph 甚至SVG

    OmniGraffle 生成自动布局图形的基础是 Graphviz 引擎。Graphviz(Graph Visualization Software)是一个由AT&T实验室启动的开源工具包,能够支持基于 DOT 脚本,文件扩展名通常是 .gv 或 .dot 的描述绘制图形。DOT 是一种文本图形描述语言,将生成的图形转换成多种输出格式的命令行工具,其输出格式包括PostScript,PDF,SVG,PNG,含注解的文本等。DOT 本身非常原始,提供了一种非常简单的描述图形的方法,同时意味着可以在命令行终端使用,或者被其它编程语言调用(Graphviz 就可以作为一个库使用)。这一点非常关键,基于 Graphviz 应用开发者不必掌握布局的复杂算法,而是可以把精力放在业务方面,将最后的图对象交给绘图引擎来处理即可。


    推荐阅读知乎回答:数据可视化(三)基于 Graphviz 实现程序化绘图 - RiboseYim的文章 - 知乎

    对于前段同学,可以在nodejs生成,再在网页上显示

    https://www.npmjs.com/package/graphviz

    对于简单的关系图,可以直接调用http://www.gravizo.com/服务显示

    前端库:https://www.npmjs.com/package/ts-graphviz

    直接在前段网页,可以选择D3:https://www.npmjs.com/package/d3-graphviz

    基于此库,在vue reat框架有:

    https://www.npmjs.com/package/graphviz-react

    https://www.npmjs.com/package/vue-graphviz



    贴一下别人的总结:《Graphviz 画图的一些总结》 

    Graphviz 是一个自动排版的作图软件,可以生成 png pdf 等格式。

    dot 语言

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

    以下是定义DOT语言的抽象语法,约束的规则如下:

    • 元素的终止以 粗体 显示

    • 文字字符用单引号 '' 引起来

    • 圆括号 () 的内容为必选项

    • 方括号 [] 为可选项目

    • 竖杠 | 为择一选择

    声明结构
    graph[ strict ] (graph | digraph) [ ID ] '{' stmt_list '}'
    stmt_list[ stmt [ ';' ] stmt_list ]
    stmtnode_stmt | edge_stmt | attr_stmt | ID '=' ID | subgraph
    attr_stmt(graph | node | edge) attr_list
    attr_list'['** [ a_list ] **']' [ attr_list ]
    a_listID '=' ID [ (';' | ',') ] [ a_list ]
    edge_stmt(node_id | subgraph) edgeRHS [ attr_list ]
    edgeRHSedgeop (node_id | subgraph) [ edgeRHS ]
    node_stmtnode_id [ attr_list ]
    node_idID [ port ]
    port':' ID [ ':' compass_pt ] | ':' compass_pt
    subgraph[ subgraph [ ID ] ] '{' stmt_list '}'
    compass_pt(n | ne | e | se | s | sw | w | nw | c | _)

    ID 其实就是一个字符串,为该组件的名称或者属性的名称,命名规则如下:

    1. 所有的字母 [a-zA-Z\200-\377] 下划线,数字 [0-9],数字不能出现在起始位置

    2. 纯数字

    3. 所有用双引号引用的字符串 "..."

    4. HTML 格式的字符串 <>

    dot 语法的关键字

    • strict, 严格的图限定,禁止创建多个相同的边

    • graph, 无向图. 在图的创建时必须声明为有向图还是无向图

    • digraph, 有向图

    • node, 节点

    • edge, 边

    • subgraph, 子图

    通过 dot 的抽象语法可以看到

    1. 整个 graph 必须使用 graph 或 digraph {} 进行限定说明图的属性

    2. 图里面的声明列表可以为空,也可以为多个,每个声明后的 ; 为可选项

    3. 声明有几种类型

      1. 节点 node

      2. edge

      3. 子图 subgraph

      4. 属性列表

      5. ID = ID, 这个类型暂时还没有看到有什么作用

    4. 属性列表

      1. 必须使用中括号 [ ] 将列表项括起来

      2. 列表项为可选

    5. 属性列表项

      1. 以 key = value 的形式存在,列表项可选择 ',' 和 ';' 结尾

      2. 可存在多个列表项

    6. 边的声明

      1. 首端为 节点标识符或者子图,

      2. 右部分由边连接节点标识符或者子图构成,右部分可以存在多个

      3. 尾部可选属性列表

    7. 节点的声明
             示例 节点的用法  node0 [label = "<postid1> string|<postid2> string|<postid3> string3", height=.5]`  node0:head[color=lightblue]  // 设置该部分的颜色

      1. 首部为节点标识符 节点部分(post) 方向 组成,其中后两项为可选项。

      2. 后半部分为可选的属性列表

    方向说明
    nnorth 北
    nenorth east
    eeast 东
    sesouth east 东南
    ssouth 南
    swsouth west 西南
    wwest 西
    nwnorth west 西北
    ccenter 中部
    _任意

    一个方向的示例

    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"]    node0:head:_ -> node2:_ [label = "_"]
        node0:body[style=filled color=lightblue]
    }

    效果如下 图-1
        

    绘制属性

    一个图中有非常多的 node 和 edge,如果每次都需要声明一个节点的属性会非常麻烦,有一个简单的方式为声明一个公共的属性如

    digraph action {
        rankdir = LR // 设置方向
        node [shape=box color=blue]    edge [color=red]
    
        node1 // 默认节点属性
        node2 [color=lightblue] // 属于该节点的颜色属性
        node1 -> node2 // 默认边属性 
        node2 -> node1 [color=green] // 属于该变的属性}

    在声明位置之后的节点都有一个 默认 的形状和颜色属性。
       

    全部的属性见graphviz官网,这里列举部分常用的属性

    • charset 编码,一般设置 UTF-8

    • fontname 字体名称,这个在中文的情况需要设置,否则导出图片的时候会乱码,一般设置微软雅黑("Microsoft YaHei"), linux 下也是同样设置系统带的字体就好,其他字体设置见fontpath 属性

    • fontcolor 字体颜色

    • fontsize 字体大小,用于文本内容

    • fillcolor 用于填充节点或者集群(cluster)的背景颜色。

    • size 图形的最大宽度和高度

    • label 图形上的文本标记

    • margin 设置图形的边距

    • pad 指定将绘制区域扩展到绘制图形所需的最小区域的长度(以英寸为单位)

    • style 设置图形组件的样式信息。 对于聚类子图或者节点,如果style = "filled",则填充聚类框的背景 

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



    • ranksep 以英寸为单位提供所需的排列间隔

    • ratio 设置生成图片的纵横比

    节点(node)

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

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

    节点中最基本的属性为:

    • shape 形状,全部形状见graphviz官网,一些常用的图形有

    • width height, 图形的宽度和高度,如果设置了 fixedsize 为 true,则宽和高为最终的长度

    • fixedsize, 如果为false,节点的大小由其文本内容所需要的最小值决定

    • rank 子图中节点上的排列等级约束. 最小等级是最顶部或最左侧,最大等级是最底部或最右侧。

      • same. 所有节点都位于同一等级

      • min. 所有节点都位于最小等级上

      • source. 所有节点都位于最小等级上,并且最小等级上的唯一节点属于某个等级 source 或 min 的子图.

      • max sink. 和上类似

    边 (edge)

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

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

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

    一些关于边的属性如下:

    digraph {
        rankdir = LR
        splines = ortho
    
        A -> B -> C -> D -> F [color = green]
        E -> F -> B -> D [color = blue]
        B -> E -> H[color = red]
    }
    • len 首选边的长度

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

    • lhead 逻辑边缘的头部(箭头那个位置),compound 设置为 true 时,边被裁减到子图的边界处

    • ltail 类似 lhead

    • headlabel 边上靠近箭头部分的标签

    • taillabel 边上靠近尾部部分的标签
             设置 A->B->C->D->F的权重最大,修改绿色的分支的权重为 100,使其变成主要逻辑分支。
              

    • splines 控制如何以及是否表示边缘。其值如下

      • none 或者 "", 无边 

      • true 或者 spline, 样条线(无规则,可为直或者曲线)

      • false 或者 line, 直线段 

      • polyline, 折线 

      • curved, 曲弧线,两条? 

      • ortho, 正直的线(横竖)

    • dir 设置绘制箭头的边缘类型
              

    子图

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


    示例

    TCP IP 状态流程图

    展示了两个版本,怎么把这些图形节点稍微规范的显示出来

    digraph {
        compound=true
        fontsize=10
        margin="0,0"
        ranksep = .75
        nodesep = .65
    
        node [shape=Mrecord fontname="Inconsolata, Consolas", fontsize=12, penwidth=0.5]
        edge [fontname="Inconsolata, Consolas", fontsize=10, arrowhead=normal]
        "TCP/IP State Transition" [shape = "plaintext", fontsize = 16]
        // now start server state transition    
        "CLOSED" -> "LISTEN" [style = blod, label = "应用:被动打开\n发送:<无>"];
        "LISTEN" -> "SENT_REVD" [style = blod, label = "接收:SYN\n发送:SYN,ACK"]
        "SENT_REVD" -> "ESTABLISHED" [style = blod, label = "接收:ACK\n发送:<无>", weight = 20]    
        "ESTABLISHED" -> "CLOSE_WAIT" [style = blod, label = "接收:FIN\n发送:ACK", weight = 20]
        subgraph cluster_passive_close {    
            style = dotted
            margin = 10
    
            passive_close [shape = plaintext, label = "被动关闭", fontsize = 14]
            "CLOSE_WAIT" -> "LAST_ACK" [style = blod, label = "应用:关闭\n发送:FIN", weight = 10]    
        }    
        "LAST_ACK" -> "CLOSED" [style = blod, label = "接收:ACK\n发送:<无>"]
        // now start client state transition    
        "CLOSED" -> "SYN_SENT" [style = dashed, label = "应用:主动打开\n发送:SYN"]; 
        "SYN_SENT" -> "ESTABLISHED" [style = dashed, label = "接收:SYN,ACK\n发送:ACK", weight = 25]    
        "SYN_SENT" -> "SENT_REVD" [style = dotted, label = "接收:SYN\n发送:SYN,ACK\n同时打开"]    
        "ESTABLISHED" -> "FIN_WAIT_1" [style = dashed, label = "应用:关闭\n发送:FIN", weight = 20]    
        subgraph cluster_active_close {
            style = dotted
            margin = 10
            
            active_open [shape = plaintext, label = "主动关闭", fontsize = 14]
            "FIN_WAIT_1" -> "FIN_WAIT_2" [style = dashed, label = "接收:ACK\n发送:<无>"]
            "FIN_WAIT_2" -> "TIME_WAIT" [style = dashed, label = "接收:FIN\n发送:ACK"]
            "FIN_WAIT_1" -> "CLOSING" [style = dotted, label = "接收:ACK\n发送:<无>"]
            "FIN_WAIT_1" -> "TIME_WAIT" [style = dotted, label = "接收:SYN,ACK\n发送:ACK"]
            "CLOSING" -> "TIME_WAIT" [style = dotted]
        }    
        "TIME_WAIT" -> "CLOSED" [style = dashed, label = "2MSL超时"]
    }

    这是一个很挫的版本,排版乱飞了。
        

    digraph rankdot {
        compound=true
        margin="0,0"
        ranksep = .75
        nodesep = 1
        pad = .5
        //splines = ortho
    
        node [shape=Mrecord, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=14]
        edge [charset = "UTF-8" fontname="Microsoft YaHei", fontsize=11, arrowhead = normal]
    
    
        CLOSED -> LISTEN [style = dashed, label = "应用:被动打开\n发送:<无>", weight = 100];    
        "TCP/IP State Transition" [shape = "plaintext", fontsize = 16]
    
        {
            rank = same
            SYN_RCVD SYN_SENT
            point_1 [shape = point, width = 0]
            
            SYN_SENT -> point_1 [style = dotted, label = "应用关闭或者超时"]        
            // SYN_SENT -> SYN_RCVD 这个一行代码和上一行冲突了,syn_sent 会在syn_rcvd右边
            SYN_RCVD -> SYN_SENT [style = dotted, dir = back, headlabel = "接收:SYN\n发送:SYN,ACK\n同时打开"]
        }
    
        LISTEN -> SYN_RCVD [style = dashed, headlabel = "接收:SYN\n发送:SYN,ACK"]
        SYN_RCVD -> LISTEN [style = dotted, headlabel = "接收:RST"]
        CLOSED:es -> SYN_SENT [style = blod, label = "应用:主动打开\n发送:SYN"]
        {
            rank = same
            ESTABLISHED CLOSE_WAIT
    
            ESTABLISHED -> CLOSE_WAIT [style = dashed, label = "接收:SYN,ACK\n发送:ACK"]    }
    
        SYN_RCVD -> ESTABLISHED [style = dashed, label = "接收:ACK\n发送:<无>", weight = 9]    SYN_SENT -> ESTABLISHED  [style = blod, label = "接收:SYN,ACK\n发送:ACK", weight = 10]
        {
            rank = same
    
            FIN_WAIT_1
            CLOSING 
            LAST_ACK
            point_2 [shape = point, width = 0]
    
            FIN_WAIT_1 -> CLOSING [style = dotted, label = "接收:FIN\n发送:ACK"]        LAST_ACK -> point_2 [style = dashed, label = "接收:ACK\n发送:<无>"]    }
    
        CLOSE_WAIT -> LAST_ACK [style = dashed, label = "应用:关闭\n发送:FIN", weight = 10]
        {
            rank = same
            FIN_WAIT_2  TIME_WAIT
    
            point_3 [shape = point, width = 0]
            TIME_WAIT -> point_3 [style = blod, label = "2MSL超时"]    }
    
        ESTABLISHED -> FIN_WAIT_1 [style = blod, label = "应用:关闭\n发送:FIN"]    FIN_WAIT_1 -> FIN_WAIT_2 [style = blod, headlabel = "接收:ACK\n发送:<无>", weight = 15]
        FIN_WAIT_2 -> TIME_WAIT [style = blod, label = "接收:FIN\n发送:ACK", weight = 10]
        CLOSING -> TIME_WAIT [style = dotted, label = "接收:ACK\n发送:<无>", weight = 15]    FIN_WAIT_1 -> TIME_WAIT [style = dotted, label = "接收:ACK\n发送:<无>"]
        point_3 -> point_2 [arrowhead = none, style = dotted, weight = 10]
        point_2 -> point_1 [arrowhead = none, style = dotted]
        point_1 -> CLOSED [style = dotted]
    }

    这个版本看起来有内味了,最最最的主要的原因就是我使用 rank = same 属性,将一些图形固定在 同一行,一些需要横竖的直线的地方使用 weight 来调整权重,达到横竖的直接的效果,很多地方都是微调的结果。有一个很差的地方是 使用了rank限制若干图形后,就不能使用 subgraph 属性了,这样就不能在若干不同部分的节点周边画线(对比关闭的区域)了。
        

    epoll 相关数据结构及关系

    digraph rankdot {
        compound=true
        margin="0,0"
        ranksep = .75
        nodesep = 1
        pad = .5
        rankdir = LR
    
        node [shape=record, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=14]
        edge [style = dashed, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=11]
    
        epoll [shape = plaintext, label = "epoll 相关结构及部分关系"]
    
        eventpoll [
            color = cornflowerblue,
            label = "<eventpoll> struct \n eventpoll |
                <lock> spinlock_t lock; |
                <mutex> struct mutex mtx; |
                <wq> wait_queue_head_t wq; |
                <poll_wait> wait_queue_head_t poll_wait; |
                <rdllist> struct list_head rdllist; |
                <ovflist> struct epitem *ovflist; |
                <rbr> struct rb_root_cached rbr; |
                <ws> struct wakeup_source *ws; |
                <user> struct user_struct *user; |
                <file> struct file *file; |
                <visited> int visited; |
                <visited_list_link> struct list_head visited_list_link;"
        ]
    
        epitem [
            color = sienna,
            label = "<epitem> struct \n epitem  |
                <rb>struct rb_node rbn;\nstruct rcu_head rcu; |
                <rdllink> struct list_head rdllink; |
                <next> struct epitem *next; |
                <ffd> struct epoll_filefd ffd; |
                <nwait> int nwait; |
                <pwqlist> struct list_head pwqlist; |
                <ep> struct eventpoll *ep; |
                <fllink> struct list_head fllink; |
                <ws> struct wakeup_source __rcu *ws; |
                <event> struct epoll_event event;"
        ]
    
        epitem2 [
            color = sienna,
            label = "<epitem> struct \n epitem |
                <rb>struct rb_node rbn;\nstruct rcu_head rcu; |
                <rdllink> struct list_head rdllink; |
                <next> struct epitem *next; |
                <ep> struct eventpoll *ep; |
                 ··· |
                 ··· "
        ]
    
        eppoll_entry [
            color = darkviolet,
            label = "<entry> struct \n eppoll_entry |
                <llink> struct list_head llink; |
                <base> struct epitem *base; |
                <wait> wait_queue_entry_t wait; |
                <whead> wait_queue_head_t *whead;"
        ]
    
        epitem:ep -> eventpoll:se [color = sienna]
        epitem2:ep -> eventpoll:se [color = sienna]
        eventpoll:ovflist -> epitem:next -> epitem2:next [color = cornflowerblue]
        eventpoll:rdllist -> epitem:rdllink -> epitem2:rdllink [dir = both]
        eppoll_entry:llink -> epitem:pwqlist [color = darkviolet]
        eppoll_entry:base -> epitem:nw  [color = darkviolet]
    }

    遗留问题

    1. 在以上TCP/IP 状态变迁图中,尝试增加主动关闭方的区域边框

    2. 尝试增加 TCP/IP 的时序图

    使用 VSCode 进行预览生成

    1. 在官网下载graphviz安装包

    2. 安装 vscode 插件 Graphviz Preview

    3. 在 settings.json 中添加 "graphvizPreview.dotPath": "graphviz_path\graphviz-2.38\\release\\bin\\dot.exe", graphviz_path 为所在路径,这些修改一下既可

    4. 新建一个 dot 文件,右上角就会有预览生成的按钮了

    12/05 更新,用了一圈发现并没有那么好用,自动排版是优势,但有的时候也是劣势,需要固定位置的作图时还是手动控制比较好一些,ProcessOn 用了几次觉得很不错,推荐!

    参考

    1. graphviz官方文档


    转载本站文章《数据可视化Graphviz程序化绘图—学习资料收藏整理笔记》,
    请注明出处:https://www.zhoulujun.cn/html/Operation/UML/2016_0713_8291.html