首页
/ diagrams 中 Custom 节点深度实践:用本地图标与远程图标构建自定义架构图

diagrams 中 Custom 节点深度实践:用本地图标与远程图标构建自定义架构图

2026-09-05 12:55:30作者:董斯意

本文为 diagrams 项目中「Custom 自定义节点」能力的技术指南,基于官方文档 docs/nodes/custom.md 并结合 diagrams/custom/init.py 的源码实现展开。读完本文,你将掌握如何在 diagrams 中引用本地或远程下载的图片作为节点图标,理解 Custom 类如何覆写图标加载逻辑并注入 Graphviz 节点属性,以及如何在 Cluster、列表扇出等组合模式下产出可复现的自定义架构图。

diagrams 自定义节点本地图标示例的渲染结果

diagrams 自定义节点远程图标示例的渲染结果

Custom 节点是什么:源码视角的能力定位

Custom 是 diagrams 提供的一个基础节点类,其设计目标在模块 docstring 中写得很明确:允许加载一张图片作为节点呈现,从而弥补内置云厂商节点图标库没有覆盖到的组件。完整实现见 diagrams/custom/init.py

"""
Custom provides the possibility of load an image to be presented as a node.
"""

from diagrams import Node


class Custom(Node):
    _provider = "custom"
    _type = "custom"
    _icon_dir = None

    fontcolor = "#ffffff"

    def _load_icon(self):
        return self._icon

    def __init__(self, label, icon_path, *args, **kwargs):
        self._icon = icon_path
        super().__init__(label, *args, **kwargs)

与内置节点(如 AWS、GCP 的节点)相比,Custom 有两处关键差异:

  1. 图标路径直接透传。基类 Node._load_icon() 会把图标拼接到包内的 resources/ 资源目录下(见 diagrams/init.py):

    def _load_icon(self):
        basedir = Path(os.path.abspath(os.path.dirname(__file__)))
        return os.path.join(basedir.parent, self._icon_dir, self._icon)
    

    Custom 覆写了该方法,直接返回构造时传入的 icon_pathreturn self._icon),并将 _icon_dir 置为 None。因此你传给 Custom 的任意本地路径(相对或绝对)都会被原样交给 Graphviz 使用,这也是本地图标必须相对于「脚本运行目录」存在的原因。

  2. 节点属性注入。在 Node.__init__ 中(diagrams/init.py),只要节点带有图标,就会附加如下属性:

    # If a node has an icon, increase the height slightly to avoid
    # that label being spanned between icon image and white space.
    # Increase the height by the number of new lines included in the label.
    padding = 0.4 * (self.label.count('\n'))
    self._attrs = {
        "shape": "none",
        "height": str(self._height + padding),
        "image": self._load_icon(),
    } if self._icon else {}
    

    节点基础高度为 _height = 1.9,标签中每出现一个换行 \n 高度额外增加 0.4,避免图标与文字重叠——这就是为什么文档示例中的标签普遍使用 "\n" 换行(如 "Adaptations must be shared\n under the same terms")。

此外,Node.__init__ 会在 diagrams/init.py 中检查全局 Diagram 上下文,Custom 同样必须创建在 with Diagram(...) 块内,否则抛出 EnvironmentError("Global diagrams context not set up")。连接能力则来自 Node 上重载的操作符(diagrams/init.py):

  • node >> other:前向有向边(forward=True);
  • node - other:无方向边;
  • 右侧为列表时(如 cc_heart >> non_commercial)会对列表中每个节点分别建边,即「一对多扇出」;
  • 列表左侧同理支持 non_commercial >> x 的一对一聚合。

边(Edge)的方向属性由 attrs 属性映射为 Graphviz 的 dirforward/back/both/none),见 diagrams/init.py

模式一:使用本地图片图标(Custom with local icons)

官方文档给出的本地图标场景是一个「Creative Commons 许可证关系图」。假设工作目录结构如下(与 docs/nodes/custom.md 一致):

.
├── custom_local.py
├── my_resources
│   ├── cc_heart.black.png
│   ├── cc_attribution.png
│   ├──...

custom_local.py 的完整内容为:

from diagrams import Diagram, Cluster
from diagrams.custom import Custom


with Diagram("Custom with local icons\n Can be downloaded here: \nhttps://creativecommons.org/about/downloads/", show=False, filename="custom_local", direction="LR"):
  cc_heart = Custom("Creative Commons", "./my_resources/cc_heart.black.png")
  cc_attribution = Custom("Credit must be given to the creator", "./my_resources/cc_attribution.png")

  cc_sa = Custom("Adaptations must be shared\n under the same terms", "./my_resources/cc_sa.png")
  cc_nd = Custom("No derivatives or adaptations\n of the work are permitted", "./my_resources/cc_nd.png")
  cc_zero = Custom("Public Domain Dedication", "./my_resources/cc_zero.png")

  with Cluster("Non Commercial"):
    non_commercial = [Custom("Y", "./my_resources/cc_nc-jp.png") - Custom("E", "./my_resources/cc_nc-eu.png") - Custom("S", "./my_resources/cc_nc.png")]

  cc_heart >> cc_attribution
  cc_heart >> non_commercial
  cc_heart >> cc_sa
  cc_heart >> cc_nd
  cc_heart >> cc_zero

要点解析:

  • Diagram 关键参数show=False 表示渲染后只保存不打开预览;filename="custom_local" 指定输出文件名为 custom_local.png(不写扩展名,默认格式为 png);direction="LR" 指定从左到右的布局方向。合法取值在 diagrams/init.py 中定义:方向为 TB/BT/LR/RL,输出格式为 png/jpg/svg/pdf/dot,非法值会抛出 ValueErrordiagrams/init.py)。
  • 图标题即 name:示例把一段含 URL 的多行文本作为图标题,说明 name 只是 Graphviz 的图级 label(见 diagrams/init.py),可承载任何提示文本。
  • Custom(label, icon_path) 签名:第一个参数是节点标签,第二个参数是图片路径;由源码可见路径会原样写入节点 image 属性,因此必须保证 Graphviz 渲染时该文件可达。
  • Cluster 内链式节点with Cluster("Non Commercial"): 块内用 - 将三个单字母节点串联,并用列表变量 non_commercial 承接,外层再通过 cc_heart >> non_commercial 一条语句完成扇出。Cluster 在退出 with 块时以 subgraph 形式挂到父图上(diagrams/init.py)。
  • 渲染时机:图片并不在创建节点时读取,而是在 Diagram.__exit__ 中调用 render() 交给 Graphviz 生成图片(diagrams/init.py)。所以只要图片文件在 with 块结束前存在于磁盘上即可。

运行 python custom_local.py 后即得到文档中展示的渲染结果:

Custom 节点本地图标示例输出

模式二:使用远程图片图标(Custom with remote icons)

当图标托管在网络上、且在生成图表时可访问时,可以在脚本中先把图片下载到本地再交给 Custom。文档给出的完整示例(节选自 docs/nodes/custom.md):

from diagrams import Diagram, Cluster
from diagrams.custom import Custom
from urllib.request import urlretrieve

with Diagram("Custom with remote icons", show=False, filename="custom_remote", direction="LR"):

  # download the icon image file
  diagrams_url = "https://github.com/mingrammer/diagrams/raw/master/assets/img/diagrams.png"
  diagrams_icon = "diagrams.png"
  urlretrieve(diagrams_url, diagrams_icon)

  diagrams = Custom("Diagrams", diagrams_icon)

  with Cluster("Some Providers"):

    openstack_url = "https://github.com/mingrammer/diagrams/raw/master/resources/openstack/openstack.png"
    openstack_icon = "openstack.png"
    urlretrieve(openstack_url, openstack_icon)

    openstack = Custom("OpenStack", openstack_icon)

    elastic_url = "https://github.com/mingrammer/diagrams/raw/master/resources/elastic/saas/elastic.png"
    elastic_icon = "elastic.png"
    urlretrieve(elastic_url, elastic_icon)

    elastic = Custom("Elastic", elastic_icon)

  diagrams >> openstack
  diagrams >> elastic

该模式有几个值得注意的实现细节:

  • 下载必须在渲染之前完成。从源码看,urlretrieve 写在 with Diagram(...) 块内、render() 触发之前,此时磁盘文件已就位,Graphviz 才能通过节点 image 属性找到它。若在 with 块结束后才下载,渲染时文件尚不存在,输出会缺少图标。
  • 下载的只是「本地化」手段Custom 本身并不发起网络请求——它只是把 urlretrieve 保存下来的本地文件名传给 Graphviz。这一结论可直接从 diagrams/custom/init.py 得到:类中没有任何网络相关代码。
  • 混合嵌套:示例把两个远程图标节点放入 Cluster("Some Providers"),并让簇外的 diagrams 节点分别指向它们,展示了簇内/簇间连线的组合方式(Node.connect 始终通过全局 Diagram 上下文建边,见 diagrams/init.py)。

渲染结果如下:

Custom 节点远程图标示例输出

进阶组合:Custom 节点与内置节点混用

官方文档末尾指向的另一个示例(现位于仓库内 docs/getting-started/examples.md 的 "RabbitMQ Consumers with Custom Nodes" 一节)演示了 Custom 与 Kubernetes、AWS 内置节点混用的典型做法:

from urllib.request import urlretrieve

from diagrams import Cluster, Diagram
from diagrams.aws.database import Aurora
from diagrams.custom import Custom
from diagrams.k8s.compute import Pod

# Download an image to be used into a Custom Node class
rabbitmq_url = "https://jpadilla.github.io/rabbitmqapp/assets/img/icon.png"
rabbitmq_icon = "rabbitmq.png"
urlretrieve(rabbitmq_url, rabbitmq_icon)

with Diagram("Broker Consumers", show=False):
    with Cluster("Consumers"):
        consumers = [
            Pod("worker"),
            Pod("worker"),
            Pod("worker")]

    queue = Custom("Message queue", rabbitmq_icon)

    queue >> consumers >> Aurora("Database")

这个例子说明了三件实事:其一,CustomPodAurora 一样继承自 Node,可以无缝参与 >>- 等运算符链;其二,queue >> consumers 借助列表扇出一次连出三条边,consumers >> Aurora 再聚合连回数据库,一条表达式完成「扇出-聚合」拓扑;其三,urlretrieve 也可以在 with 块之外提前下载,只要文件在渲染前就位即可。对应的渲染效果可参考仓库中的 rabbitmq_consumers_diagram.png

环境前提、参数速查与常见问题

运行环境。按 docs/getting-started/installation.md 的说明,diagrams 需要 Python 3.7 以上(当前仓库 pyproject.toml 中 Poetry 声明为 python = "^3.9",且依赖 graphviz >=0.13.2,<0.21.0jinja2),并且必须先安装 Graphviz 系统程序(macOS 可用 Homebrew、Windows 可用 Chocolatey 安装),随后通过 pip install diagramspoetry add diagrams 安装库本体。本文所有示例均以「Graphviz 已安装 + 库已安装」为前提。

DiagramCustom 相关参数速查

参数 位置 取值/默认值 说明
name Diagram 字符串,默认 "" 图标题;未给 filename 时会用它生成文件名(空格转下划线、转小写,见 diagrams/init.py
filename Diagram 无扩展名,默认由 name 生成 输出文件名,如 custom_local 生成 custom_local.png
direction Diagram TB/BT/LR/RL,默认 LR 数据流方向,映射为 Graphviz rankdir
show Diagram 默认 True False 时仅保存不打开图像
outformat Diagram png/jpg/svg/pdf/dot,默认 png,支持列表 多格式输出时逐个渲染(diagrams/init.py
label Custom 字符串 节点标签,支持 \n 换行,每换行一次节点高度 +0.4
icon_path Custom 本地图片路径 原样作为 Graphviz 节点 image 属性,相对路径相对于运行目录

常见问题排查

  • 图标不显示:先确认 icon_path 相对于「脚本执行目录」而非脚本文件位置是否可达(Custom 不会做任何路径修正,直接透传给 Graphviz)。
  • 渲染报 EnvironmentError: Global diagrams context not set upCustom(...) 必须在 with Diagram(...) 块内创建。
  • 布局方向报错 ValueError: "xxx" is not a valid directiondirection 只接受 TB/BT/LR/RL 四种取值。
  • 生成后目录中找不到 .dot 文件:属预期行为,Diagram.__exit__ 在渲染完成后会删除中间 Graphviz 文件,仅保留图片(diagrams/init.py)。

小结

Custom 节点是 diagrams「Diagram as Code」体系中扩展图标库的标准入口:它通过覆写 _load_icon 把用户提供的图片路径直接注入 Graphviz 节点,配合 Node 的操作符语法与 Cluster 子图机制,既能绘制纯自定义组件关系图(本地图标模式),也能把网络上托管的图标临时下载后混入标准云架构图(远程图标模式)。实现入口在 diagrams/custom/init.py,上下文、节点与边的核心逻辑在 diagrams/init.py,更多组合用法可继续参考 docs/getting-started/examples.md

登录后查看全文
热门项目推荐
相关项目推荐