强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Graphviz 图形可视化教程 / 10 - 编程语言绑定

第 10 章 · 编程语言绑定

10.1 为什么需要编程绑定

直接编写 DOT 文件适用于静态图形,但在以下场景中需要编程绑定:

场景 说明
动态数据 从数据库/API 获取数据生成图
批量生成 自动化生成大量图形
条件逻辑 根据条件改变图形结构
模板引擎 图形模板与数据分离
交互应用 Web 应用中动态渲染
数据可视化 从分析结果自动生成图

10.2 Python — graphviz

安装

pip install graphviz

⚠️ 需要系统已安装 Graphviz(dot 命令可用)。

基本用法

from graphviz import Digraph

# 创建有向图
dot = Digraph(comment='示例图', format='png')
dot.attr(rankdir='LR', bgcolor='#FAFAFA')
dot.attr('node', shape='box', style='filled,rounded',
         fillcolor='#E3F2FD', color='#1976D2',
         fontname='Microsoft YaHei', fontsize='11')
dot.attr('edge', color='#666666', fontname='Microsoft YaHei')

# 添加节点
dot.node('A', '开始')
dot.node('B', '处理')
dot.node('C', '结束')

# 添加边
dot.edge('A', 'B', label='请求')
dot.edge('B', 'C', label='响应')

# 渲染输出
dot.render('output/demo', cleanup=True)
print("已生成 output/demo.png")

无向图

from graphviz import Graph

g = Graph(comment='无向图', format='svg')
g.node('A')
g.node('B')
g.edge('A', 'B')
g.render('output/undirected', cleanup=True)

子图与集群

from graphviz import Digraph

dot = Digraph()
dot.attr(rankdir='TB')

# 集群(cluster_ 前缀)
with dot.subgraph(name='cluster_frontend') as c:
    c.attr(label='前端', style='filled', fillcolor='#E3F2FD', color='#1976D2')
    c.node('React', 'React')
    c.node('Vue', 'Vue')

with dot.subgraph(name='cluster_backend') as c:
    c.attr(label='后端', style='filled', fillcolor='#E8F5E9', color='#388E3C')
    c.node('Go', 'Go')
    c.node('Python', 'Python')

dot.edge('React', 'Go', label='API')
dot.edge('Vue', 'Python', label='REST')

dot.render('output/clusters', cleanup=True)

高级特性

from graphviz import Digraph

dot = Digraph(format='svg')

# HTML 标签
dot.node('table', label='''<
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
    <TR>
        <TD COLSPAN="2" BGCOLOR="#1976D2">
            <FONT COLOR="white"><B>用户信息</B></FONT>
        </TD>
    </TR>
    <TR><TD>姓名</TD><TD>张三</TD></TR>
    <TR><TD>年龄</TD><TD>28</TD></TR>
</TABLE>
>''', shape='plain')

# Record 形状
dot.node('struct', shape='record',
         label='{<f0> 输入|<f1> 处理|<f2> 输出}')

# 端口连接
dot.edge('struct:f0', 'struct:f2', style='dashed')

dot.render('output/html_table', cleanup=True)

10.3 Python — pydot

安装

pip install pydot

基本用法

import pydot

# 创建图
graph = pydot.Dot(graph_type='digraph', rankdir='LR')

# 添加节点
node_a = pydot.Node('A', label='开始', shape='circle',
                     style='filled', fillcolor='#C8E6C9')
node_b = pydot.Node('B', label='处理', shape='box',
                     style='filled', fillcolor='#E3F2FD')
node_c = pydot.Node('C', label='结束', shape='doublecircle',
                     style='filled', fillcolor='#FFCDD2')

graph.add_node(node_a)
graph.add_node(node_b)
graph.add_node(node_c)

# 添加边
graph.add_edge(pydot.Edge(node_a, node_b, label='请求'))
graph.add_edge(pydot.Edge(node_b, node_c, label='响应'))

# 输出
graph.write_png('output/pydot_demo.png')
graph.write_svg('output/pydot_demo.svg')

从 DOT 字符串解析

import pydot

dot_string = '''
digraph G {
    A -> B -> C
    A -> C
}
'''

# 解析 DOT 字符串
graphs = pydot.graph_from_dot_data(dot_string)
graph = graphs[0]
graph.write_png('output/parsed.png')

10.4 Python — pygraphviz

安装

# 需要先安装 graphviz-dev
# Ubuntu: sudo apt-get install graphviz libgraphviz-dev
pip install pygraphviz

基本用法

import pygraphviz as pgv

# 创建图
G = pgv.AGraph(directed=True, rankdir='LR')
G.node_attr['shape'] = 'box'
G.node_attr['style'] = 'filled,rounded'
G.node_attr['fontname'] = 'Microsoft YaHei'

# 添加节点和边
G.add_node('A', label='开始', fillcolor='#C8E6C9')
G.add_node('B', label='处理', fillcolor='#E3F2FD')
G.add_node('C', label='结束', fillcolor='#FFCDD2')
G.add_edge('A', 'B', label='请求')
G.add_edge('B', 'C', label='响应')

# 布局和渲染
G.layout(prog='dot')
G.draw('output/pygraphviz.png')
G.draw('output/pygraphviz.svg')

10.5 Python 库对比

特性 graphviz pydot pygraphviz
安装难度 简单 简单 需要 dev 库
API 风格 链式 命令式 对象式
DOT 解析 不支持 支持 支持
布局控制 命令行 命令行 内置
性能 中等 中等
维护状态 活跃 维护中 活跃
推荐场景 新建项目 解析 DOT 高性能需求

10.6 Go 语言绑定

使用 gonum 生态

package main

import (
    "fmt"
    "os"
    "os/exec"
    "strings"
)

func main() {
    // 构建 DOT 字符串
    dot := `digraph G {
    rankdir=LR
    node [shape=box style="filled,rounded" fillcolor="#E3F2FD" color="#1976D2" fontname="Microsoft YaHei"]
    edge [color="#666666"]

    A [label="开始"]
    B [label="处理"]
    C [label="结束"]

    A -> B [label="请求"]
    B -> C [label="响应"]
}`

    // 写入临时文件
    tmpFile := "/tmp/graph.dot"
    os.WriteFile(tmpFile, []byte(dot), 0644)

    // 调用 dot 命令
    cmd := exec.Command("dot", "-Tpng", tmpFile, "-o", "output.png")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("已生成 output.png")
}

使用 gographviz

package main

import (
    "fmt"
    "os"

    github.com/awalterschulze/gographviz
)

func main() {
    g := gographviz.NewGraph()
    g.SetName("G")
    g.SetDir(true) // 有向图
    g.AddAttr("G", "rankdir", "LR")

    // 添加节点
    g.AddNode("G", "A", map[string]string{
        "label": `"开始"`,
        "shape": "box",
    })
    g.AddNode("G", "B", map[string]string{
        "label": `"处理"`,
        "shape": "box",
    })
    g.AddNode("G", "C", map[string]string{
        "label": `"结束"`,
        "shape": "box",
    })

    // 添加边
    g.AddEdge("A", "B", true, nil)
    g.AddEdge("B", "C", true, nil)

    // 输出 DOT
    dot := g.String()
    fmt.Println(dot)
    os.WriteFile("output.dot", []byte(dot), 0644)
}

10.7 Node.js 绑定

使用 @viz-js/viz(推荐)

npm install @viz-js/viz
import { instance } from "@viz-js/viz";

async function main() {
    const viz = await instance();

    const dot = `
    digraph G {
        rankdir=LR
        node [shape=box style="filled,rounded" fillcolor="#E3F2FD"]
        A -> B -> C
        A -> C
    }
    `;

    // 渲染为 SVG
    const svg = viz.renderSVGElement(dot);
    console.log(svg.outerHTML);

    // 渲染为 JSON
    const json = viz.renderJSON(dot);
    console.log(JSON.stringify(json, null, 2));

    // 渲染为 PNG(返回 ArrayBuffer)
    const png = viz.renderString(dot, { format: "png" });
    console.log("PNG 数据长度:", png.length);
}

main().catch(console.error);

在浏览器中使用

<!DOCTYPE html>
<html>
<head>
    <title>Graphviz in Browser</title>
    <script type="module">
        import { instance } from "https://cdn.jsdelivr.net/npm/@viz-js/viz/+esm";

        document.addEventListener("DOMContentLoaded", async () => {
            const viz = await instance();

            const dot = document.getElementById("dot-input").value;
            const svg = viz.renderSVGElement(dot);
            document.getElementById("output").appendChild(svg);
        });
    </script>
</head>
<body>
    <textarea id="dot-input" rows="10" cols="50">
digraph G {
    A -> B -> C
    A -> C
}
    </textarea>
    <div id="output"></div>
</body>
</html>

使用 graphviz npm 包

const graphviz = require('graphviz');

// 创建图
const g = graphviz.digraph("G");
g.set("rankdir", "LR");

// 添加节点
const a = g.addNode("A", { label: "开始", shape: "box" });
const b = g.addNode("B", { label: "处理", shape: "box" });
const c = g.addNode("C", { label: "结束", shape: "box" });

// 添加边
g.addEdge(a, b, { label: "请求" });
g.addEdge(b, c, { label: "响应" });

// 输出 DOT
console.log(g.to_dot());

// 渲染(需要本地安装 Graphviz)
g.output("png", "output/node_graph.png", (err) => {
    if (err) console.error(err);
    else console.log("已生成 output/node_graph.png");
});

10.8 Rust 绑定

使用 graphviz-rust

# Cargo.toml
[dependencies]
graphviz-rust = "0.9"
use graphviz_rust::dot_generator::*;
use graphviz_rust::dot_structures::*;
use graphviz_rust::print;
use graphviz_rust::cmd::{CommandArg, Format};
use graphviz_rust::exec;

fn main() {
    // 构建图
    let g = graph!(id!("G");
        attr!("rankdir", "LR"),
        node!("A"; attr!("label", esc "开始"), attr!("shape", "box")),
        node!("B"; attr!("label", esc "处理"), attr!("shape", "box")),
        node!("C"; attr!("label", esc "结束"), attr!("shape", "box")),
        edge!(node_id!("A") => node_id!("B";  port!("e")),
              attr!("label", esc "请求")),
        edge!(node_id!("B") => node_id!("C"; port!("w")),
              attr!("label", esc "响应"))
    );

    // 输出 DOT
    let dot = print(g.clone());
    println!("{}", dot);

    // 渲染为 PNG(需要系统安装 Graphviz)
    let svg = exec(
        g,
        &mut vec![Format::Svg.into()],
    ).expect("渲染失败");
    println!("{}", svg);
}

10.9 动态生成模式

从 JSON 数据生成图

import json
from graphviz import Digraph

# 模拟数据
data = {
    "services": [
        {"name": "用户服务", "type": "api", "deps": ["数据库", "缓存"]},
        {"name": "订单服务", "type": "api", "deps": ["用户服务", "消息队列"]},
        {"name": "支付服务", "type": "api", "deps": ["订单服务"]},
        {"name": "数据库", "type": "db", "deps": []},
        {"name": "缓存", "type": "db", "deps": []},
        {"name": "消息队列", "type": "infra", "deps": []},
    ]
}

def generate_architecture_diagram(data):
    dot = Digraph(comment='服务架构', format='svg')
    dot.attr(rankdir='TB')
    dot.attr('node', fontname='Microsoft YaHei', fontsize='11')
    dot.attr('edge', fontname='Microsoft YaHei', fontsize='9', color='#666666')

    # 样式映射
    styles = {
        'api': {'shape': 'component', 'fillcolor': '#E3F2FD', 'color': '#1976D2'},
        'db':  {'shape': 'cylinder', 'fillcolor': '#F3E5F5', 'color': '#7B1FA2'},
        'infra': {'shape': 'box3d', 'fillcolor': '#FFF3E0', 'color': '#FF9800'},
    }

    for svc in data['services']:
        style = styles.get(svc['type'], {})
        dot.node(svc['name'], label=svc['name'],
                 style='filled', **style)

    for svc in data['services']:
        for dep in svc['deps']:
            dot.edge(svc['name'], dep)

    return dot

dot = generate_architecture_diagram(data)
dot.render('output/architecture', cleanup=True)

从 CSV 生成关系图

import csv
from graphviz import Digraph

def generate_from_csv(csv_path, output_path):
    dot = Digraph(format='svg')
    dot.attr(rankdir='LR')
    dot.attr('node', shape='box', style='filled,rounded',
             fillcolor='#E3F2FD', fontname='Microsoft YaHei')

    with open(csv_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        nodes = set()
        for row in reader:
            src = row['source']
            dst = row['target']
            label = row.get('label', '')

            if src not in nodes:
                dot.node(src, src)
                nodes.add(src)
            if dst not in nodes:
                dot.node(dst, dst)
                nodes.add(dst)

            dot.edge(src, dst, label=label)

    dot.render(output_path, cleanup=True)
    return dot

# CSV 格式:source,target,label
# generate_from_csv('relations.csv', 'output/relations')

注意事项

⚠️ 系统依赖:所有编程绑定都需要系统安装 Graphviz(dot 命令)。

⚠️ 中文编码:确保 Python 文件使用 UTF-8 编码,标签中的中文才能正确渲染。

⚠️ HTML 标签转义:在 Python 字符串中嵌入 HTML 标签时注意转义 <>

⚠️ 临时文件:某些库会生成临时文件,注意清理。

⚠️ viz.js 的 WASM:Node.js 中的 @viz-js/viz 使用 WebAssembly,不需要系统安装 Graphviz。

⚠️ pygraphviz 编译pygraphviz 需要 C 编译器和 graphviz-dev 包。


扩展阅读


下一章11 - Docker 与自动化 — 在容器中使用 Graphviz。