Graphviz 畫圖的一些總結


Graphviz

Graphviz 是一個自動排版的作圖軟件,可以生成 png pdf 等格式。

dot 語言

Graphviz 構建組件為 圖,節點,邊,用屬性對其進行描述。

以下是定義DOT語言的抽象語法,約束的規則如下:

  • 元素的終止以 粗體 顯示
  • 文字字符用單引號 '' 引起來
  • 圓括號 () 的內容為必選項
  • 方括號 [] 為可選項目
  • 豎杠 | 為擇一選擇
聲明 結構
graph [ strict ] (graph | digraph) [ ID ] '{' stmt_list '}'
stmt_list [ stmt [ ';' ] stmt_list ]
stmt node_stmt | edge_stmt | attr_stmt | ID '=' ID | subgraph
attr_stmt (graph | node | edge) attr_list
attr_list '[' [ a_list ] ']' [ attr_list ]
a_list ID '=' ID [ (';' | ',') ] [ a_list ]
edge_stmt (node_id | subgraph) edgeRHS [ attr_list ]
edgeRHS edgeop (node_id | subgraph) [ edgeRHS ]
node_stmt node_id [ attr_list ]
node_id ID [ 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. 純數字 $[-]^?(.[0-9]^+ | [0-9]^+(.[0-9]*)6? $
  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. 后半部分為可選的屬性列表
方向 說明
n north 北
ne north east
e east 東
se south east 東南
s south 南
sw south west 西南
w west 西
nw north west 西北
c center 中部
_ 任意

一個方向的示例

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官方文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM