
告别手动画图的痛苦,拥抱声明式绘图
如果你用Python做过数据分析,肯定对Matplotlib、Seaborn不陌生——它们帮你把数据变成漂亮的折线图、柱状图。但当你需要画流程图、系统架构、决策树、知识图谱这些“关系图”时,是不是瞬间头大?
手动拖拽节点、调整连线、对齐布局……光是想想就让人崩溃。更别提后续修改了,动一个节点,整个图都得重调。
这时候,Graphviz就该登场了。它不是什么“另一个绘图库”,而是一个声明式图形可视化工具。你只需要告诉它:“我有这些节点,它们之间这样连接”,至于“怎么画得好看、不交叉、布局合理”这个最头疼的问题,交给Graphviz的自动布局引擎就行了。
想象一下:你设计电路图(逻辑),它生成PCB布线图(布局)。这就是Graphviz的哲学。
Graphviz的世界由三个基本元素构成:
图(Graph/Digraph):画布。Graph是无向图,Digraph是有向图(更常用)。
节点(Node):图中的实体,比如“用户”、“数据库”、“处理函数”。
边(Edge):节点之间的关系,带箭头表示方向。
Graphviz使用一种叫DOT的声明式语言。在Python中,graphviz库帮你生成DOT代码,但你了解下它的语法没坏处:
digraph 我的流程图 {
rankdir=LR; // 布局方向:从左到右
数据源 -> 清洗 -> 分析 -> 报告;
}
是的,就这么简单!但真正的威力藏在参数里。下面我们进入重头戏。
graph_attr控制整个图的全局行为,这是你首先要设置的。
from graphviz import Digraph
# 创建图时设置全局参数
dag = Digraph(
name='数据处理流水线',
graph_attr={
# 布局方向:TB(上到下,默认), LR(左到右), RL, BT
'rankdir': 'LR',
# 连线样式:解决交叉问题的神器!
# line(直线), spline(曲线), ortho(直角折线)
'splines': 'ortho', # ★ 强烈推荐!消除连线交叉
# 节点重叠处理:大型图必备
'overlap': 'vpsc', # 或 'prism',防止节点重叠
# 字体:中文显示的生命线
'fontname': 'Microsoft YaHei, SimSun, sans-serif',
'fontsize': '12',
# 背景和边距
'bgcolor': 'white',
'margin': '0.3,0.3',
# 字符编码:中文用户必设
'charset': 'UTF-8'
}
)
关键参数解析:
splines='ortho':这是让你的图从“乱麻”变“清晰”的最重要参数!它强制连线走直角,避免交叉。但需要Graphviz 2.30+版本。
overlap='vpsc':当节点很多时,这个参数能自动防止节点重叠。部分Linux系统可能需要额外安装libvpsc库。
fontname:一定要设置中文字体回退链!"Microsoft YaHei, SimSun, WenQuanYi Zen Hei, sans-serif"这样写,确保跨平台显示正常。
节点是信息的载体,好的节点设计能极大提升信息密度。
# 设置节点默认样式
dag.node_attr.update({
# 形状:用形状传递语义
'shape': 'box', # box=实体, ellipse=概念, diamond=决策
'style': 'filled,rounded', # 填充+圆角
'fillcolor': 'lightblue',
# 文字:中文优化关键
'fontname': 'Microsoft YaHei',
'fontsize': '11',
'fontcolor': 'black',
# 边距:防止文字贴边
'margin': '0.25,0.15',
# 尺寸:让Graphviz自动计算,别固定!
# 'fixedsize': 'false' # 默认就是false,别改!
})
# 添加具体节点(可覆盖默认属性)
dag.node('raw_data', '原始数据\n(Polars加载)',
shape='cylinder', fillcolor='wheat')
dag.node('model', 'LSTM预测模型',
shape='ellipse', fillcolor='plum', peripheries='2') # 双线框突出显示
高级技巧:HTML-like标签
当节点内容复杂时,用HTML-like标签实现表格布局:
dag.node('smart_node', label='''<
<TABLEBORDER="0"CELLBORDER="1"CELLSPACING="0"CELLPADDING="4">
<TR>
<TDCOLSPAN="2"BGCOLOR="#4a86e8">
<FONTCOLOR="white"><B>贾宝玉</B></FONT>
</TD>
</TR>
<TR>
<TDALIGN="LEFT">特点</TD>
<TD>叛逆、重情</TD>
</TR>
<TR>
<TDALIGN="LEFT">关键事件</TD>
<TDPORT="event">摔玉</TD>
</TR>
</TABLE>>''', shape='none')
这样你可以:
PORT="event")供边连接边不只是连线,它能承载丰富的信息。
# 边默认样式
dag.edge_attr.update({
# 基础视觉
'color': '#4a5568', # 深灰蓝,专业感
'penwidth': '1.3',
'style': 'solid',
# 箭头系统
'arrowhead': 'vee',
'arrowsize': '0.9',
# 标签系统:中文优化关键!
'fontname': 'Microsoft YaHei',
'fontsize': '10',
'fontcolor': '#2d3748',
'labelangle': '45', # ★ 防遮挡黄金角度
'labeldistance': '1.4', # 让标签远离连线
# 布局权重:让重要关系更直更短
'weight': '5'
})
# 添加边(可覆盖默认)
dag.edge('raw_data', 'clean', label='CSV文件')
dag.edge('model', 'output',
color='red',
style='bold',
label='预测概率\n(0.87)')
边的语义化编码:
# 定义关系类型样式映射
RELATION_STYLES = {
'数据流': {'color': '#3498db', 'style': 'solid', 'weight': 8},
'控制流': {'color': '#e74c3c', 'style': 'dashed', 'weight': 3},
'依赖': {'color': '#9b59b6', 'style': 'dotted', 'arrowhead': 'dot'},
}
# 应用样式
for src, rel_type, dst in relationships:
style = RELATION_STYLES.get(rel_type, {})
dag.edge(src, dst, label=rel_type, **style)
import polars as pl
from graphviz import Digraph
# 创建示例数据(Polars 1.36)
df = pl.DataFrame({
'date': pl.date_range(
start='2024-01-01',
end='2024-01-10',
interval='1d',
eager=True
),
'symbol': ['AAPL', 'GOOGL', 'MSFT'] * 4,
'price': (pl.arange(0, 12, eager=True) * 10 + 100).cast(pl.Float64),
'volume': pl.arange(1000, 13000, 1000, eager=True)
})
# 构建一个复杂查询
lazy_df = df.lazy().filter(
pl.col('price') > 120
).group_by('symbol').agg([
pl.col('price').mean().alias('avg_price'),
pl.col('volume').sum().alias('total_vol')
])
# 模拟查询计划可视化
plan_viz = Digraph('PolarsQueryPlan', format='svg')
plan_viz.graph_attr.update({
'rankdir': 'TB',
'splines': 'ortho',
'fontname': 'Microsoft YaHei',
'nodesep': '0.4',
'ranksep': '0.6'
})
plan_viz.node_attr.update({
'shape': 'box',
'style': 'filled,rounded',
'fillcolor': '#E8F4F8',
'fontname': 'Microsoft YaHei'
})
# 模拟查询计划节点
plan_viz.node('scan', '扫描CSV文件\n(并行读取)')
plan_viz.node('filter', '过滤: price > 120\n(谓词下推)')
plan_viz.node('agg', '聚合: avg(price), sum(volume)')
plan_viz.node('project', '投影: symbol, avg_price, total_vol')
plan_viz.node('result', '结果DataFrame')
# 连接边
plan_viz.edges([
('scan', 'filter'),
('filter', 'agg'),
('agg', 'project'),
('project', 'result')
])
# 保存
plan_viz.render('polars_query_plan', view=False, cleanup=True)
print("查询计划图已生成:polars_query_plan.svg")
import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from graphviz import Source
# 加载数据(Pandas 3.0)
iris = load_iris()
df = pd.DataFrame(
data=np.column_stack([iris.data, iris.target]),
columns=['sepal_len', 'sepal_wid', 'petal_len', 'petal_wid', 'target']
)
# 训练决策树
X = df[['petal_len', 'petal_wid']].values
y = df['target'].values
tree_clf = DecisionTreeClassifier(max_depth=3, random_state=42)
tree_clf.fit(X, y)
# 导出为Graphviz DOT格式
export_graphviz(
tree_clf,
out_file='iris_tree.dot',
feature_names=['petal_len', 'petal_wid'],
class_names=iris.target_names,
rounded=True,
filled=True, # 填充颜色表示类别
special_characters=True
)
# 在Python中直接渲染
with open('iris_tree.dot', 'r', encoding='utf-8') as f:
dot_data = f.read()
# 添加中文支持(原始export_graphviz不支持中文,我们手动增强)
dot_data = dot_data.replace(
'node [shape=box',
'node [shape=box, fontname="Microsoft YaHei"'
)
# 保存修改后的DOT文件
with open('iris_tree_enhanced.dot', 'w', encoding='utf-8') as f:
f.write(dot_data)
# 渲染查看
src = Source.from_file('iris_tree_enhanced.dot')
src.render('iris_decision_tree', format='png', view=False)
print("决策树已生成:iris_decision_tree.png")
import pandas as pd
import numpy as np
from graphviz import Digraph
from typing import Dict, List
# 模拟包依赖数据(用Pandas处理)
deps_data = {
'package': ['main_app', 'data_processor', 'ml_model', 'utils',
'pandas', 'numpy', 'graphviz', 'scipy'],
'dependencies': [
'data_processor,ml_model,utils',
'pandas,numpy',
'scipy,numpy',
'numpy',
'numpy',
'',
'',
'numpy'
]
}
df_deps = pd.DataFrame(deps_data)
# 构建依赖图
dep_graph = Digraph('PackageDependencies', format='svg')
dep_graph.graph_attr.update({
'rankdir': 'LR',
'splines': 'ortho',
'fontname': 'Microsoft YaHei',
'bgcolor': '#f8f9fa'
})
# 定义节点颜色映射
package_types = {
'main_app': {'fillcolor': '#4a86e8', 'shape': 'ellipse'},
'data_processor': {'fillcolor': '#6aa84f', 'shape': 'box'},
'ml_model': {'fillcolor': '#e69138', 'shape': 'box'},
'utils': {'fillcolor': '#cccccc', 'shape': 'box'},
'第三方库': {'fillcolor': '#fff2cc', 'shape': 'cylinder'}
}
# 添加节点
for _, row in df_deps.iterrows():
pkg = row['package']
# 确定包类型
if pkg in ['pandas', 'numpy', 'graphviz', 'scipy']:
pkg_type = '第三方库'
elif pkg == 'main_app':
pkg_type = 'main_app'
else:
pkg_type = pkg
attrs = package_types.get(pkg_type, {})
dep_graph.node(
pkg,
pkg,
shape=attrs.get('shape', 'box'),
style='filled,rounded',
fillcolor=attrs.get('fillcolor', 'lightgray'),
fontname='Microsoft YaHei'
)
# 添加依赖边
for _, row in df_deps.iterrows():
source = row['package']
if pd.notna(row['dependencies']) and row['dependencies']:
for dep in row['dependencies'].split(','):
if dep: # 非空字符串
dep_graph.edge(dep, source) # 依赖指向被依赖方
# 添加图例
with dep_graph.subgraph(name='cluster_legend') as legend:
legend.attr(
label='图例',
style='filled',
fillcolor='white',
fontname='Microsoft YaHei'
)
legend.node_attr.update({'fontsize': '9', 'width': '1.2'})
legend.node('legend_app', '主应用', shape='ellipse', fillcolor='#4a86e8')
legend.node('legend_module', '业务模块', shape='box', fillcolor='#6aa84f')
legend.node('legend_lib', '第三方库', shape='cylinder', fillcolor='#fff2cc')
dep_graph.render('package_deps', view=False, cleanup=True)
print(f"包依赖图已生成,共{len(df_deps)}个包")
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.csgraph import shortest_path
from graphviz import Digraph
# 创建邻接矩阵(NumPy 2.4)
adj_matrix = np.array([
[0, 3, 0, 0, 2], # A到B=3, A到E=2
[0, 0, 1, 0, 0], # B到C=1
[0, 0, 0, 4, 0], # C到D=4
[0, 0, 0, 0, 0], # D没有出边
[0, 1, 0, 7, 0] # E到B=1, E到D=7
], dtype=np.float64)
# 计算最短路径
graph = csr_matrix(adj_matrix)
dist_matrix, predecessors = shortest_path(
csgraph=graph,
directed=True,
return_predecessors=True,
indices=0# 从节点0(A)开始
)
# 可视化路径
path_viz = Digraph('ShortestPaths', format='svg')
path_viz.graph_attr.update({
'rankdir': 'LR',
'splines': 'ortho',
'fontname': 'Microsoft YaHei'
})
# 节点名称
nodes = ['A', 'B', 'C', 'D', 'E']
# 添加所有节点
for i, node in enumerate(nodes):
distance = dist_matrix[i]
dist_label = f'{distance:.1f}'if np.isfinite(distance) else'∞'
path_viz.node(
node,
f'{node}\n距离: {dist_label}',
shape='circle',
style='filled',
fillcolor='lightblue'if i == 0else'lightyellow',
fontname='Microsoft YaHei'
)
# 添加原始边(灰色)
for i in range(len(nodes)):
for j in range(len(nodes)):
weight = adj_matrix[i, j]
if weight > 0:
path_viz.edge(
nodes[i], nodes[j],
label=f'{weight}',
color='gray',
style='dashed',
fontcolor='gray'
)
# 高亮最短路径边(从A出发)
for target in range(1, len(nodes)):
pred = predecessors[target]
if pred >= 0:
path_viz.edge(
nodes[pred], nodes[target],
color='red',
penwidth='2.5',
label=f'{adj_matrix[pred, target]}',
fontcolor='red'
)
path_viz.render('shortest_path', view=False, cleanup=True)
print(f"最短路径已计算并可视化")
print(f"从A到各点距离: {dist_matrix}")
# 正确做法:三位一体
dot = Digraph(
encoding='utf-8', # 1. 设置编码
graph_attr={
'charset': 'UTF-8', # 2. DOT文件编码
'fontname': 'Microsoft YaHei, SimSun, sans-serif'# 3. 字体回退链
},
node_attr={'fontname': 'Microsoft YaHei'},
edge_attr={'fontname': 'Microsoft YaHei'}
)
先开ortho,再调其他:splines='ortho'解决80%的连线交叉问题
权重比线宽更重要:weight参数影响布局引擎,penwidth只影响视觉
让Graphviz自动计算尺寸:除非特殊需求,别设fixedsize=true
标签角度45°:labelangle=45是边标签清晰度的生命线
from graphviz import Digraph
import os
defrender_graph(dot: Digraph, output_format: str = None) -> str:
"""智能渲染函数,支持动态格式选择"""
# 格式白名单
VALID_FORMATS = {'png', 'svg', 'pdf', 'jpg', 'json', 'dot'}
# 1. 优先使用传入格式
if output_format and output_format.lower() in VALID_FORMATS:
format_choice = output_format.lower()
# 2. 检查环境变量
elif'GRAPHVIZ_OUTPUT_FORMAT'in os.environ:
env_format = os.environ['GRAPHVIZ_OUTPUT_FORMAT'].lower()
format_choice = env_format if env_format in VALID_FORMATS else'svg'
# 3. 默认SVG(矢量图,推荐)
else:
format_choice = 'svg'
# 设置格式
dot.format = format_choice
# 生成文件名
filename = f'diagram.{format_choice}'
# 渲染(不自动打开)
dot.render(filename.replace(f'.{format_choice}', ''),
view=False,
cleanup=True)
print(f"已生成: {filename} (格式: {format_choice})")
return filename
# 使用示例
dot = Digraph()
dot.node('A', '开始')
dot.node('B', '结束')
dot.edge('A', 'B')
# 根据需求生成不同格式
render_graph(dot, 'svg') # 网页用
render_graph(dot, 'pdf') # 打印用
render_graph(dot, 'png') # 通用嵌入
当节点超过100个时:
# 大型图优化配置
large_graph = Digraph()
large_graph.graph_attr.update({
'splines': 'ortho',
'overlap': 'vpsc', # 或 'prism'
'pack': 'true', # 紧凑打包
'packmode': 'clust',
'concentrate': 'true', # 合并平行边
'newrank': 'true', # 新层级算法
'fontsize': '10', # 调小字体
})
通过上面的实战,你应该感受到了:Graphviz不是要替代Matplotlib或Plotly,而是填补关系可视化的空白。
✅ 系统架构图:微服务、数据流水线
✅ 算法流程图:决策树、状态机、工作流
✅ 知识图谱:实体关系、社交网络
✅ 依赖分析:包依赖、任务依赖
✅ 组织结构:公司架构、项目分工
❌ 数据分布可视化:用Matplotlib/Seaborn
❌ 交互式数据探索:用Plotly/Altair
❌ 地理信息可视化:用GeoPandas/Leaflet
❌ 实时数据仪表盘:用Dash/Streamlit
# 完整工作流示例:从数据到关系图
import polars as pl
import pandas as pd
import numpy as np
from scipy.sparse.csgraph import connected_components
from graphviz import Digraph
# 1. 用Polars处理大数据
df_large = pl.read_csv('big_data.csv')
relationships = df_large.filter(
pl.col('strength') > 0.5
).select(['source', 'target', 'relation_type']).to_pandas()
# 2. 用Pandas/Numpy分析
adj_matrix = pd.crosstab(
relationships['source'],
relationships['target']
).values
# 3. 用SciPy分析图结构
n_components, labels = connected_components(
csgraph=adj_matrix,
directed=False
)
# 4. 用Graphviz可视化
viz = Digraph()
# ... 构建可视化代码
print(f"发现{n_components}个连通组件")
Graphviz的学习曲线有点陡,主要就是因为它的参数系统太丰富。但一旦掌握,你就能:
记住这个学习路径:
先开ortho:解决连线交叉
再设字体:解决中文显示
然后调布局:rankdir、ranksep、nodesep
最后美化:颜色、形状、标签
Graphviz就像绘图界的"
AutoLayout"——你描述关系,它负责美观。在数据越来越复杂、系统越来越庞大的今天,这种声明式的可视化方式,会成为你工具箱中不可或缺的利器。