Drawlib クイックスタートガイド

純粋 Python による「Illustration as Code」&「Illustrated Documentation as Code」

Drawlib は、宣言的な Python 作図コードと Markdown 技術文書を単一の Git リポジトリ(Single Source of Truth)に統合し、ドキュメントサイト(site)・検索可能な API リファレンス辞書(catalog)・技術白書(doc)・16:9 プレゼンテーション(slide)・単体画像/アニメーション群(images)・ベクター PDF・編集可能な Microsoft Office 文書(.docx / .pptx)を一括生成するライブラリです。

00_cover_1
図 0.1: Drawlib の Single Source of Truth — Python と Markdown から多様な成果物を一括生成

本書の構成とロードマップ

本書は、ソフトウェアエンジニア・テクニカルライター・AI コーディングエージェント向けに Drawlib の基礎から実践的なドキュメント出版までを全 16 章で解説する入門ハンドブックです。

ドキュメント情報 内容
対象バージョン 0.3.7
ビジュアルテーマ GoogleStyles & GoogleColors (style = "google")
対応言語 ([i18n]) 英語 (en)・日本語 (ja)
公式サイト https://www.drawlib.com/
ソースリポジトリ https://github.com/yuichi110/drawlib
ライセンス Apache License 2.0

1. なぜ Drawlib なのか?

現代のソフトウェア開発では、プルリクエスト・CI/CD パイプライン・AI コーディング支援によって実装コードが日々高速に進化し続ける一方、アーキテクチャ図や技術仕様書はいまだに手作業の分断された運用に取り残されがちです。

Drawlib は**純粋 Python による宣言的な作図(Illustration as Code)とマルチ形式の技術文書出版(Illustrated Documentation as Code)**を単一の Git リポジトリに統合し、エンジニアと AI エージェントがコードとまったく同じ開発体験で作図・差分レビュー・視覚検証・自動出版を行える基盤を提供します。

01_about_1
図 1.1: 分断された従来の作図運用と Drawlib の Git 統合ワークフローの比較
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles

setup(width=148, height=66)

left_panel = Panel(
    "従来の分断された作図運用",
    style=Styles.Neutral.patch(shape_r=2.2),
    header_style=Styles.MutedNeutral,
    text_style=Styles.DarkBold.patch(text_size=12.0),
    icon=phosphor.warning,
    icon_style=Styles.DangerBold,
    header_height=9.0,
)
left_panel.draw((5.0, 5.0), width=65.0, height=56.0)

legacy_cards = CardList(
    style=Styles.White.patch(shape_r=1.6),
    text_style=Styles.DarkBold.patch(text_size=11.0),
    description_style=Styles.Dark.patch(text_size=10.0),
    badge_style=Styles.MutedNeutral,
    badge_text_style=Styles.DarkBold.patch(text_size=10.5),
    icon_style=Styles.DarkBold,
    spacing=2.2,
    show_arrows=True,
    arrow_style=Styles.MutedBold,
)
legacy_cards.add(
    "GUI ツールでの手作業編集",
    "バイナリや巨大 XML によりコードレビュー困難",
    badge_text="1",
    icon=phosphor.wrench,
)
legacy_cards.add(
    "PNG 書き出し・手動貼り付け",
    "Wiki・設計書・スライドへ毎回手作業で転記",
    badge_text="2",
    icon=phosphor.images,
)
legacy_cards.add(
    "設計図の陳腐化と実装乖離",
    "PR 差分が追えず本番システムと図が不一致に",
    badge_text="3",
    icon=phosphor.warning,
    style=Styles.DangerNeutral.patch(shape_r=1.6),
    icon_style=Styles.DangerBold,
)
legacy_cards.draw((8.5, 8.0), width=58.0, height=41.0)

line((71.2, 33.0), (76.8, 33.0), arrow_head="->", style=Styles.DarkBold)

right_panel = Panel(
    "Drawlib の Git 統合ワークフロー",
    style=Styles.PrimaryNeutral.patch(shape_r=2.2),
    header_style=Styles.SecondaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=12.0),
    icon=phosphor.check_circle,
    icon_style=Styles.PrimaryBold,
    header_height=9.0,
)
right_panel.draw((78.0, 5.0), width=65.0, height=56.0)

drawlib_cards = CardList(
    style=Styles.White.patch(shape_r=1.6),
    text_style=Styles.DarkBold.patch(text_size=11.0),
    description_style=Styles.Dark.patch(text_size=10.0),
    badge_style=Styles.SecondaryNeutral,
    badge_text_style=Styles.DarkBold.patch(text_size=10.5),
    icon_style=Styles.DarkBold,
    spacing=2.2,
    show_arrows=True,
    arrow_style=Styles.DarkBold,
)
drawlib_cards.add(
    "Git 管理の Python & Markdown",
    "明快な PR 差分と単一の信頼できる情報源",
    badge_text="1",
    icon=phosphor.git_branch,
)
drawlib_cards.add(
    "Drawlib コンパイラ & AI 検証",
    "決定論的座標と -g グリッドによる自律検証",
    badge_text="2",
    icon=phosphor.cpu,
    style=Styles.PrimaryFlat.patch(shape_r=1.6),
    text_style=Styles.WhiteBold.patch(text_size=11.0),
    description_style=Styles.White.patch(text_size=10.0),
    badge_style=Styles.White,
    badge_text_style=Styles.PrimaryBold.patch(text_size=10.5),
    icon_style=Styles.PrimaryBold,
)
drawlib_cards.add(
    "マルチ形式への一括ビルド出版",
    "HTML・辞書・PDF・DOCX・PPTX を自動生成",
    badge_text="3",
    icon=phosphor.globe,
    style=Styles.SecondaryNeutral.patch(shape_r=1.6),
)
drawlib_cards.draw((81.5, 8.0), width=58.0, height=41.0)

save()

1.1 既存ツールの限界と課題

これまでソフトウェア開発現場では主に 3 種類の作図・可視化ツールが使い分けられてきましたが、実運用においてそれぞれ深刻な課題を抱えています。

評価軸 GUI ツール (draw.io / Visio) テキスト DSL (Mermaid / PlantUML) 数値プロット (Matplotlib) Drawlib (v0.3.7)
Git PR 差分の可読性 低(バイナリ・冗長な XML) 高(簡潔なテキスト) 中(冗長な命令コード) 高(宣言的な Python コード)
レイアウト制御性 手動マウス操作のみ ブラックボックス自動配置のみ 軸座標の手動計算 精密なデカルト座標 + 5種自動ソルバー
表現領域の広さ 構成図・ワイヤーフレーム 基本的なフロー・シーケンス 数値グラフのみ アイコン・SmartArts・チャート・構成図・アニメ
ドキュメント出版先 画像の手動書き出し Web 上の埋め込み表示のみ 単体画像ファイルのみ HTML・辞書・PDF・.docx・スライド・.pptx
AI 自律修正との親和性 コーディング AI から操作不可 描画後の重なりを微調整困難 ボイラープレートが多く破綻しやすい -g グリッドによるマルチモーダル自律修正

1.2 Drawlib を支える 3 つの柱

Drawlib v0.3.7 は、技術的なビジュアル表現を再現可能なソフトウェア工学へと昇華させる 3 つの柱を中心に設計されています。

01_about_2
図 1.2: Drawlib の3層アーキテクチャ — 基本図形・高レベル描画エンジンからマルチ形式出版まで
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.shapes import rectangle
from drawlib.smartarts import ChevronProcess, Panel
from drawlib.styles import Styles
from drawlib.text import text

setup(width=148, height=66)

stages = ChevronProcess(
    style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=11.2),
    description_style=Styles.Dark.patch(text_size=10.0),
    spacing=2.0,
    flat_left_end=True,
)
stages.add("1. 基本図形とアイコン")
stages.add(
    "2. 高レベル描画エンジン",
    style=Styles.PrimaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=11.2),
)
stages.add("3. マルチ形式ドキュメント出版", style=Styles.SecondaryNeutral)
stages.draw((6.0, 49.0), width=136.0, height=11.5)

p1 = Panel(
    "基盤プリミティブ層",
    style=Styles.White.patch(shape_r=1.8),
    header_style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=11.0),
    icon=phosphor.cube,
    icon_style=Styles.DarkBold,
    header_height=7.8,
)
p1.draw((6.0, 18.0), width=43.0, height=28.0)
text(
    (10.0, 28.0),
    "• 43種の基本幾何図形\n• 曲線・直交ルーティング線\n• 日本語/欧文フォント管理\n• 34種ベクターアイコン群",
    style=Styles.Dark.patch(text_size=10.2, halign="left", text_line_spacing=1.45),
)

p2 = Panel(
    "宣言的ビジュアル層",
    style=Styles.PrimaryNeutral.patch(shape_r=1.8),
    header_style=Styles.SecondaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.0),
    icon=phosphor.stack,
    icon_style=Styles.PrimaryBold,
    header_height=7.8,
)
p2.draw((52.5, 18.0), width=43.0, height=28.0)
text(
    (56.5, 28.0),
    "• 18種 SmartArts 部品\n• 10種の定量データチャート\n• 6種の専門ダイアグラム\n• 5種の自動グラフソルバー",
    style=Styles.Dark.patch(text_size=10.2, halign="left", text_line_spacing=1.45),
)

p3 = Panel(
    "統合パブリッシング層",
    style=Styles.White.patch(shape_r=1.8),
    header_style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=11.0),
    icon=phosphor.export,
    icon_style=Styles.DarkBold,
    header_height=7.8,
)
p3.draw((99.0, 18.0), width=43.0, height=28.0)
text(
    (103.0, 28.0),
    "• Web サイト & API 辞書\n• 単一 HTML & GitHub MD\n• ベクター PDF 出力\n• 編集可能な .docx / .pptx",
    style=Styles.Dark.patch(text_size=10.2, halign="left", text_line_spacing=1.45),
)

rectangle((74.0, 9.5), width=136.0, height=9.5, style=Styles.MutedDashed.patch(shape_r=1.8))
phosphor.grid_four((13.0, 9.5), width=5.0, style=Styles.PrimaryBold)
text(
    (18.5, 9.5),
    "第3の柱 — AI マルチモーダル自律ループ: 決定論的 (x, y) 座標系 + CLI グリッド検証 (drawlib show -g)",
    style=Styles.DarkBold.patch(text_size=10.5, halign="left"),
)

save()

1.2.1 第1の柱:Illustration as Code(コードとしての作図)

Drawlib のすべての図解は、Pydantic による厳格な型検証と統一テーマプリセット(GoogleStyles・DefaultStyles・MonochromeStyles)を備えた純粋な Python コードとして記述されます。

1.2.2 第2の柱:Illustrated Documentation as Code(図解ドキュメントのコード化)

Drawlib は単体画像の生成にとどまらず、Markdown 本文と埋め込み ```drawlib コードブロックを単一のコンパイル単位として統合します。

1.2.3 第3の柱:AI マルチモーダル自律検証ループ

Drawlib は、人間のエンジニアとマルチモーダル AI コーディングエージェントの協業を前提としてゼロから設計されています。

2. インストールとゼロ設定ツールチェイン

Drawlib v0.3.7 は Python 3.11+(3.11・3.12・3.13) で動作する自己完結型の純粋 Python ツールチェインであり、Linux・macOS・Windows のいずれの環境でも Graphviz 本体や LaTeX 環境、Node.js バンドラを別途インストールする必要はありません。

既存の Python プロジェクトへ uv add drawlib で依存追加する運用はもちろん、Go・Rust・TypeScript・Java などの多言語リポジトリで設定ファイルを一切作らず uvx drawlib から即時実行する運用にも同じ CLI とキャッシュ機構で対応します。

02_install_1
図 2.1: 2つの導入ワークフロー — Python プロジェクト依存 (uv add) と多言語ゼロインストール実行 (uvx)
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import ArchitectureDiagram, Node, NodeGroup, PhosphorIcon
from drawlib.styles import Styles

setup(width=148, height=62)

d = ArchitectureDiagram(
    node_style=Styles.PrimaryBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.8),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
)

g_py = d.add(
    NodeGroup(
        title="1. Python リポジトリ (uv add)",
        width=50.0,
        height=23.0,
        style=Styles.PrimaryNeutral.patch(shape_r=2.0),
        text_style=Styles.DarkBold.patch(text_size=10.8, halign="left"),
    ),
    xy=(3.0, 31.0),
)
g_py.add(
    Node(
        "pyproject.toml + .venv\nuv run drawlib ...",
        width=44.0,
        height=12.0,
        icon=PhosphorIcon.PACKAGE,
        icon_size=5.0,
        card_style=Styles.White.patch(shape_r=1.6),
    ),
    xy=(25.0, 8.8),
)

g_uvx = d.add(
    NodeGroup(
        title="2. 多言語・文書リポジトリ (uvx)",
        width=50.0,
        height=23.0,
        style=Styles.SecondaryNeutral.patch(shape_r=2.0),
        text_style=Styles.DarkBold.patch(text_size=10.8, halign="left"),
    ),
    xy=(3.0, 4.0),
)
g_uvx.add(
    Node(
        "Go / Rust / TS / Java\nuvx drawlib ... (設定不要)",
        width=44.0,
        height=12.0,
        icon=PhosphorIcon.TERMINAL_WINDOW,
        icon_size=5.0,
        card_style=Styles.White.patch(shape_r=1.6),
    ),
    xy=(25.0, 8.8),
)

cli_node = d.add(
    Node(
        "Drawlib v0.3.7\nCLI & コンパイラ",
        width=31.0,
        height=20.0,
        icon=PhosphorIcon.CPU,
        icon_size=5.8,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=11.2),
        card_style=Styles.PrimaryFlat.patch(shape_r=2.2),
    ),
    xy=(78.5, 29.0),
)

cache_node = d.add(
    Node(
        "自動アセットキャッシュ\n(フォント・アイコン・ブラウザ)",
        width=39.0,
        height=15.0,
        icon=PhosphorIcon.DATABASE,
        icon_size=5.0,
        card_style=Styles.Neutral.patch(shape_r=1.8),
    ),
    xy=(122.5, 42.5),
)

out_node = d.add(
    Node(
        "マルチ形式の成果物\n(HTML / PDF / DOCX / PPTX)",
        width=39.0,
        height=15.0,
        icon=PhosphorIcon.EXPORT,
        icon_size=5.0,
        card_style=Styles.SecondaryNeutral.patch(shape_r=1.8),
    ),
    xy=(122.5, 15.5),
)

j_in_top = d.junction((58.0, 42.5))
j_in_mid = d.junction((58.0, 29.0))
j_in_bot = d.junction((58.0, 15.5))
d.connect(g_py, j_in_top, arrow="-", routing="direct", padding=(0.8, 0.0))
d.connect(g_uvx, j_in_bot, arrow="-", routing="direct", padding=(0.8, 0.0))
d.connect(j_in_top, j_in_mid, arrow="-", routing="direct")
d.connect(j_in_bot, j_in_mid, arrow="-", routing="direct")
d.connect(j_in_mid, cli_node, arrow="->", routing="direct", padding=(0.0, 0.8))

j_out_mid = d.junction((98.5, 29.0))
j_out_top = d.junction((98.5, 42.5))
j_out_bot = d.junction((98.5, 15.5))
d.connect(cli_node, j_out_mid, arrow="-", routing="direct", padding=(0.8, 0.0))
d.connect(j_out_mid, j_out_top, arrow="-", routing="direct")
d.connect(j_out_mid, j_out_bot, arrow="-", routing="direct")
d.connect(j_out_top, cache_node, arrow="->", routing="direct", padding=(0.0, 0.8))
d.connect(j_out_bot, out_node, arrow="->", routing="direct", padding=(0.0, 0.8))

d.draw(xy=(2.0, 2.0))
save()

2.1 動作要件と 2 つの実行モード

Drawlib は現行の CPython ランタイムをサポートしており、高速なパッケージマネージャである uv と組み合わせることでロックファイル管理もゼロインストール実行もシームレスに行えます。

要件項目 サポート仕様 備考
Python バージョン Python 3.11+(3.11・3.12・3.13) Linux(x86_64 / aarch64)・macOS・Windows 対応
推奨ランナー uv(uv add / uv run / uvx) 仮想環境の高速構築および多言語リポジトリでの即時実行に対応
標準代替手段 pip(venv + pip install drawlib) 追加の extra 指定なしで全機能が標準インストール可能

2.1.1 方法 1:Python プロジェクトへの導入(uv add drawlib)

Python アプリケーションやデータ分析基盤、あるいは pyproject.toml と uv.lock でバージョンを厳密に固定したいドキュメント専用リポジトリでは、uv add drawlib による導入を推奨します。

# 新規 uv プロジェクトの作成と drawlib の追加(既存 Python リポジトリ内でも同様)
uv init my_project
uv add drawlib
uv run drawlib --version

# uv を使用しない環境での標準 venv + pip フォールバック
python3 -m venv .venv
.venv/bin/pip install drawlib
.venv/bin/drawlib --version

プロジェクト依存として追加すると、エディタ(Pyright・Pylance・Ruff)上で drawlib.shapes・drawlib.smartarts・drawlib.charts・drawlib.diagrams の型補完が完全に効き、CI でも uv sync --frozen で再現性の高いビルドが可能になります。

2.1.2 方法 2:多言語リポジトリでのゼロインストール実行(uvx drawlib)

Go・Rust・TypeScript・Java・C++ リポジトリや純粋な Markdown 文書リポジトリでは、ルート直下に pyproject.toml や .venv/ を作る必要はありません。uvx を付けて実行するだけで、隔離されたツールキャッシュ環境から Drawlib CLI を即座に呼び出せます。

# ローカル設定ファイル一切不要でバージョン確認
uvx drawlib --version

# Go / Rust / TS リポジトリ内に多言語ドキュメント基盤を初期化して HTML ビルド
uvx drawlib init project site docs -l en,ja -s google
uvx drawlib build html docs_src/ -o docs_html/

2.2 同梱ランタイム基盤と自動アセットキャッシュ

図表描画・PDF 変換・Word/PowerPoint 出力ごとに別々のプラグイン導入を強いる従来のツールとは異なり、drawlib v0.3.7 は単一パッケージにすべての描画・出版エンジンを標準同梱しています(drawlib[pdf] のようなオプション指定は不要です)。

02_install_2
図 2.2: Drawlib v0.3.7 のゼロ設定ランタイム構成と自動キャッシュ基盤
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.shapes import rectangle
from drawlib.smartarts import KpiCards
from drawlib.styles import Styles
from drawlib.text import text

setup(width=148, height=66)

rectangle((74.0, 56.5), width=136.0, height=8.6, style=Styles.PrimaryFlat.patch(shape_r=2.0))
phosphor.package((13.5, 56.5), width=5.0, style=Styles.WhiteBold)
text(
    (18.5, 56.5),
    "drawlib v0.3.7 — 単一パッケージに全機能を標準同梱 (Python 3.11 / 3.12 / 3.13)",
    style=Styles.WhiteBold.patch(text_size=11.5, halign="left"),
)

stack = KpiCards(
    style=Styles.White.patch(shape_r=1.8),
    metric_style=Styles.DarkBold.patch(text_size=13.0),
    text_style=Styles.DarkBold.patch(text_size=11.2),
    description_style=Styles.Dark.patch(text_size=10.0),
    accent_style=Styles.Neutral,
    icon_style=Styles.PrimaryBold,
    accent_position="left_pill",
    metric_layout="left_column",
    metric_width_ratio=0.22,
    spacing=3.5,
)
stack.add(
    "Core",
    "純粋 Python 宣言的 API",
    "基本図形・18種 SmartArts・10種チャート・\n6種ダイアグラム・5種グラフソルバー",
    icon=phosphor.code,
    style=Styles.PrimaryNeutral.patch(shape_r=1.8),
)
stack.add(
    "Pillow",
    "ラスター画像 & アニメ基盤",
    "高精細 PNG/WebP 生成・Dimage 変換・\nAPNG / WebP / GIF フレーム合成",
    icon=phosphor.images,
    style=Styles.Neutral.patch(shape_r=1.8),
)
stack.add(
    "Cache",
    "Google/CJK フォント & アイコン",
    "Noto CJK フォント・34種アイコン自動取得\n& SQLite 差分キャッシュ (.drawlib)",
    icon=phosphor.database,
    style=Styles.Neutral.patch(shape_r=1.8),
)
stack.add(
    "Export",
    "Chromium & Office 出力器",
    "Playwright Chromium 自動取得 (PDF)\n+ python-docx / python-pptx 標準搭載",
    icon=phosphor.file_pdf,
    style=Styles.SecondaryNeutral.patch(shape_r=1.8),
)
stack.draw((6.0, 5.5), width=136.0, height=43.0, columns=2)

save()

drawlib をインストールすると、以下の 4 つのランタイム基盤が追加設定なしで連携して動作します。

オフライン環境や CI ランナー、および共有ライブラリを含まない最小構成の Linux コンテナ(python:3.12-slim 等)では、以下のコマンドで事前取得を行えます。

# ローカルのフォント・アイコン・地図・ブラウザのキャッシュ状態を確認
uv run drawlib cache list

# 日本語/欧文フォント・34種アイコン・地図・ヘッドレス Chromium を一括事前取得
uv run drawlib cache download --all

# PDF / PPTX ビルド用のヘッドレス Chromium のみを事前取得
uv run drawlib cache download --browser

# 最小 Linux コンテナ等で OS 共有ライブラリ (libnss3, libgbm1 等) が不足している場合
uv run playwright install-deps chromium

2.3 クイックテスト:最初のプロジェクト作成

Drawlib では裸の .py スクリプトを手作業で作るのではなく、必ず drawlib init project でプロジェクト雛形を生成してから執筆を始めます。これにより drawlib.toml・styles.py・utils.py・_assets/・各種 build*.sh が一括で構成されます。

# 1. CLI バージョンの確認
uv run drawlib --version

# 2. 日英バイリンガル (en,ja) かつ Google テーマの技術文書プロジェクトを初期化
uv run drawlib init project doc my_docs -l en,ja -s google

# 3. HTML・ベクター PDF・GitHub Markdown・単体画像を一括ビルド
./my_docs_src/build.sh

drawlib init project doc my_docs -l en,ja -s google を実行すると、my_docs_src/ 配下に drawlib.toml([project] と [i18n] 設定)・styles.py(GoogleStyles)・ja/styles.py(FontJapanese オーバーライド)・utils.py・_assets/・en/ と ja/ の初期章ファイル、そして用途別のビルドスクリプト(build.sh・build_html.sh・build_pdf.sh・build_docx.sh・serve.sh)が即座に生成されます。

3. デカルト座標キャンバスとアンカー設計

Drawlib のすべての図解は、左下隅を原点 (0, 0) とし、X 軸が右方向(0 -> width)、Y 軸が上方向(0 -> height)へ増加する決定論的な 2 次元デカルト座標平面上に描画されます。

左下原点のデカルト座標系・720pt タイポグラフィ計算式・コンポーネント別の基準アンカー規則の 3 点を押さえることで、人間のエンジニアも AI コーディングエージェントも文字重なりや配置ズレのない美しい図解を一発で設計できるようになります。

03_canvas_1
図 3.1: Drawlib の左下 (0, 0) デカルト座標系・安全マージン領域・プロット座標
Source Code
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import circle, rectangle
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=148, height=72)

# 仮想キャンバス外枠 [26..126] x [16..64] と外周 6 単位内側の安全マージン領域
rectangle((76.0, 40.0), width=100.0, height=48.0, style=Styles.Neutral.patch(shape_line_width=1.2))
rectangle(
    (76.0, 40.0),
    width=88.0,
    height=36.0,
    style=Styles.PrimaryDashed.patch(shape_fill_color=Colors.White, shape_r=1.4),
)
text(
    (34.5, 55.2),
    "安全マージン領域 (外周から 4–6 単位内側)",
    style=Styles.MutedBold.patch(text_size=10.2, halign="left", valign="top"),
)

# 中心座標の補助クロスヘア(中央バッジの背面に描画)
line((32.0, 40.0), (120.0, 40.0), style=Styles.MutedDotted)
line((76.0, 22.0), (76.0, 50.5), style=Styles.MutedDotted)

# デカルト X 軸(右向き)と Y 軸(上向き)
line((26.0, 16.0), (136.0, 16.0), arrow_head="->", style=Styles.DarkBold)
text((138.0, 16.0), "+X", style=Styles.DarkBold.patch(text_size=11.5, halign="left", valign="center"))
text((76.0, 7.2), "X 軸:右方向へ増加 (0 -> width)", style=Styles.DarkBold.patch(text_size=11.0))

line((26.0, 16.0), (26.0, 67.5), arrow_head="->", style=Styles.DarkBold)
text((23.2, 66.8), "+Y", style=Styles.DarkBold.patch(text_size=11.5, halign="right", valign="center"))
text(
    (9.5, 40.0),
    "Y 軸:上方向へ増加 (0 -> height)",
    style=Styles.DarkBold.patch(text_size=11.0, angle=90),
)

# 四隅のマーカーとオフセット配置ラベル(軸線やドットとの重なりゼロ)
circle((26.0, 16.0), radius=1.25, style=Styles.PrimaryFlat)
text((24.0, 12.2), "原点 (0, 0)", style=Styles.DarkBold.patch(text_size=10.8, halign="right", valign="top"))

circle((126.0, 16.0), radius=0.95, style=Styles.DarkFlat)
text((126.0, 12.2), "(W, 0)", style=Styles.DarkBold.patch(text_size=10.5, halign="center", valign="top"))

circle((26.0, 64.0), radius=0.95, style=Styles.DarkFlat)
text((29.2, 64.8), "(0, H)", style=Styles.DarkBold.patch(text_size=10.5, halign="left", valign="bottom"))

circle((126.0, 64.0), radius=0.95, style=Styles.DarkFlat)
text((128.5, 64.0), "(W, H)", style=Styles.DarkBold.patch(text_size=10.5, halign="left", valign="center"))

# 中心アンカーバッジと 2 つのサンプル座標点
rectangle(
    (76.0, 40.0),
    width=34.0,
    height=9.8,
    style=Styles.PrimaryFlat.patch(shape_r=2.0),
    text="中心 (W/2, H/2)",
    text_style=Styles.WhiteBold.patch(text_size=10.8),
)

circle((45.0, 28.0), radius=1.05, style=Styles.DarkFlat)
text((47.6, 28.0), "P1 (25, 20)", style=Styles.DarkBold.patch(text_size=10.4, halign="left", valign="center"))

circle((108.0, 50.5), radius=1.05, style=Styles.DarkFlat)
text((105.4, 50.5), "P2 (95, 48)", style=Styles.DarkBold.patch(text_size=10.4, halign="right", valign="center"))

save()

3.1 デカルト座標系とブラウザ DOM / SVG 座標系の違い

HTML5 の <canvas> や CSS 配置、生の <svg> に慣れているエンジニアは、Y 座標の上下反転を防ぐために以下の違いを最初に確認してください。

座標特性 Drawlib デカルト座標系 ブラウザ DOM / HTML <svg>
原点 (0, 0) の位置 左下隅(Bottom-Left) 左上隅(Top-Left)
水平 +X 方向 左から右へ増加(0 -> width) 左から右へ増加(0 -> width)
垂直 +Y 方向 下から上へ増加(0 -> height) 上から下へ増加(0 -> height)
上部タイトルの配置 大きい Y 値(height - 6 付近) 小さい Y 値(16px 付近)
外周の安全マージン [5, W - 5] × [5, H - 5] の内側に配置 CSS padding / viewBox

数学のグラフや建築立面図と同じく +Y が上を向いているため、**「Y の値が大きいほど画面の上方に配置される」**という直感的なメンタルモデルで統一されています。図タイトルは height - 6 付近、メインの構成要素は height / 2 前後、注釈や凡例は y = 6..10 付近に置きます。また、枠線や角丸の見切れを防ぐため、キャンバス四辺には必ず 4〜6 単位の外周マージンを確保してください。


3.2 キャンバスのライフサイクル:setup()・描画・save()

単体スクリプトでも Markdown 埋め込みブロックでも、Drawlib の作図コードは常に 3 ステップのライフサイクルで構成されます。

  1. setup(width=..., height=..., dpi=..., background_color=..., grid=...): キャンバスの幅と高さを抽象的な仮想座標単位(width / height、既定値 100 × 100)で定義し、出力画像のピクセル解像度を dpi(既定値 100)で指定します。座標計算と解像度が分離されているため、dpi=100 を dpi=200 に引き上げても (x, y) 座標を一切書き換えることなく出力ピクセル数だけを 2 倍に高精細化できます。また、配色テーマやフォント設定(from drawlib.styles import Colors, Styles)はプロジェクトの styles.py(drawlib.toml の style = "google" 等)から自動適用されます。
  2. 宣言的な描画呼び出し: 背面の境界枠や接続線から前面のノード・ラベルへと奥から手前(Back-to-Front)の順で基本図形・アイコン・SmartArts・チャートを配置します。
  3. save(): キャンバスの描画内容を確定して出力します。単体 .py スクリプトでも Markdown 内の ```drawlib ブロックでも、必ず末尾を save() で締めくくってください。単体スクリプトでは <スクリプト名>.png(または指定パス)への書き出しを実行し、Markdown 埋め込みブロックでも save() を明記することでそのままコピー&ペーストして単体実行できる自己完結性が保たれます(※1 つの .py ファイル内で複数画像を連続生成する場合は、各 save(...) の直後に clear() を呼び出してキャンバスを初期化します)。
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles

# 1. 仮想座標 136x50 単位・解像度 150 DPI(出力サイズ 2040x750 px)で初期化
setup(width=136, height=50, dpi=150)

# 2. 奥から手前の順で描画(ニュートラル基調 + 主役ノード 1 個のみ PrimaryFlat)
line((44, 25), (54, 25), arrow_head="->", style=Styles.DarkBold)
line((82, 25), (92, 25), arrow_head="->", style=Styles.DarkBold)

rectangle((26, 25), width=36, height=18, style=Styles.Neutral, text="クライアント")
rectangle(
    (68, 25),
    width=28,
    height=18,
    style=Styles.PrimaryFlat,
    text="API コア",
    text_style=Styles.WhiteBold,
)
rectangle((110, 25), width=36, height=18, style=Styles.SecondaryNeutral, text="PostgreSQL")

# 3. ライフサイクル確定のため必ず save() を呼び出す
save()

3.3 720pt タイポグラフィ計算式とキャンバスサイズ設計

Drawlib の内部レンダラは図全体の基準幅を 720pt 固定としてフォントサイズを正規化します。そのため、dpi の値にかかわらず、HTML・PDF・スライド上で画像が Display Width(表示幅ピクセル)で描画された際の見た目上の文字サイズは以下の式で厳密に決まります。

Rendered Font Size (px) = text_size × (Display Width (px) / 720)

この比例関係を理解しておくと、ドキュメントに埋め込んだ際に図中の文字だけ極端に小さくなる問題を防げます。

用途・出力先 推奨 setup(width, height) 標準表示幅 推奨 text_size 画面上の実表示文字サイズ
技術文書・Web サイトの図解 140 × 60(幅 120..150) ~840px 10.5〜12.0(見出し 12.0〜14.0) ~12.5px〜16.5px(16px 本文と調和)
情報密度の高い全体構成図 148 × 72 ~840px 10.0〜11.5(見出し 12.0〜13.5) ~11.7px〜15.8px
16:9 フル幅スライド図解 160 × 80 ~1760px 11.0〜13.0(見出し 14.0〜16.0) ~27px〜39px(26px スライド本文と調和)

3.4 テキスト配置(halign / valign)とコンポーネントのアンカー設計

単体テキスト text(xy, label, style=...) の基準点 xy に対する揃え位置は、既存の Styles.* トークンに .patch(halign=..., valign=...) を適用して制御します(Style(...) を直接インスタンス化せず、常に .patch(...) を使用します)。

さらに、Drawlib の全コンポーネントは以下の 3 種類の座標アンカー規則のいずれかに明確に分類されます。

03_canvas_2
図 3.2: テキスト配置(halign / valign)と 3 種のコンポーネント基準アンカー
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import circle, rectangle
from drawlib.smartarts import Panel
from drawlib.styles import Styles
from drawlib.text import text

setup(width=148, height=68)

# 左パネル: halign / valign の基準点デモ(アンカードットと文字の重なりゼロ)
p_left = Panel(
    "テキスト配置 (halign & valign)",
    style=Styles.Neutral.patch(shape_r=2.0),
    header_style=Styles.PrimaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.5),
    icon=phosphor.text_t,
    icon_style=Styles.PrimaryBold,
    header_height=8.5,
)
p_left.draw((5.0, 5.0), width=66.0, height=58.0)

# 1段目: halign="left", valign="center"
rectangle((38.0, 45.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
line((13.0, 41.2), (13.0, 48.8), style=Styles.MutedDashed)
circle((13.0, 45.0), radius=1.05, style=Styles.PrimaryFlat)
text(
    (16.0, 45.0),
    'halign="left", valign="center"',
    style=Styles.DarkBold.patch(text_size=10.5, halign="left", valign="center"),
)

# 2段目: halign="center", valign="bottom"
rectangle((38.0, 30.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
line((16.0, 26.5), (60.0, 26.5), style=Styles.MutedDashed)
circle((38.0, 26.5), radius=1.05, style=Styles.PrimaryFlat)
text(
    (38.0, 28.5),
    'halign="center", valign="bottom"',
    style=Styles.DarkBold.patch(text_size=10.5, halign="center", valign="bottom"),
)

# 3段目: halign="right", valign="top"
rectangle((38.0, 15.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
line((22.0, 18.5), (63.0, 18.5), style=Styles.MutedDashed)
circle((63.0, 18.5), radius=1.05, style=Styles.PrimaryFlat)
text(
    (60.2, 16.5),
    'halign="right", valign="top"',
    style=Styles.DarkBold.patch(text_size=10.5, halign="right", valign="top"),
)

# 右パネル: コンポーネントの 3 大基準アンカーモデル
p_right = Panel(
    "コンポーネントの基準アンカー設計",
    style=Styles.Neutral.patch(shape_r=2.0),
    header_style=Styles.SecondaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.5),
    icon=phosphor.crosshair,
    icon_style=Styles.DarkBold,
    header_height=8.5,
)
p_right.draw((77.0, 5.0), width=66.0, height=58.0)

# 分類 1: 中央基準 Center (cx, cy) — 主役ノード PrimaryFlat 1個
rectangle((110.0, 45.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
rectangle((90.5, 45.0), width=14.5, height=7.8, style=Styles.PrimaryFlat.patch(shape_r=1.2))
circle((90.5, 45.0), radius=1.05, style=Styles.WhiteFlat)
text((100.8, 47.0), "中央基準 (cx, cy)", style=Styles.DarkBold.patch(text_size=10.8, halign="left"))
text((100.8, 42.6), "基本図形・アイコン・画像・text()", style=Styles.Dark.patch(text_size=10.0, halign="left"))

# 分類 2: 左下基準 Bottom-Left (x0, y0)
rectangle((110.0, 30.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
rectangle((90.5, 30.0), width=14.5, height=7.8, style=Styles.PrimaryNeutral.patch(shape_r=1.2))
circle((83.25, 26.1), radius=1.05, style=Styles.PrimaryFlat)
text((100.8, 32.0), "左下基準 (x0, y0)", style=Styles.DarkBold.patch(text_size=10.8, halign="left"))
text((100.8, 27.6), "チャート・構成図・大半の SmartArts", style=Styles.Dark.patch(text_size=10.0, halign="left"))

# 分類 3: 左上基準 Top-Left (x0, y_top)
rectangle((110.0, 15.0), width=59.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
rectangle((90.5, 15.0), width=14.5, height=7.8, style=Styles.SecondaryNeutral.patch(shape_r=1.2))
circle((83.25, 18.9), radius=1.05, style=Styles.PrimaryFlat)
text((100.8, 17.0), "左上基準 (x0, y_top)", style=Styles.DarkBold.patch(text_size=10.8, halign="left"))
text((100.8, 12.6), "Table・TreeNode・SourceCode", style=Styles.Dark.patch(text_size=10.0, halign="left"))

save()

基本プリミティブと複合コンポーネント(SmartArts・Charts・Diagrams)を組み合わせる際は、常に以下の 3 分類を意識してください。

4. 基本図形・接続線・幾何計算ヘルパー

複数ノードの全体配置を自動化する高レベル描画エンジン(ArchitectureDiagram・SmartArts・Charts)に対し、drawlib.shapes・drawlib.lines・drawlib.math はカスタムバッジや吹き出し、3D コンテナ、滑らかな曲線コネクタをミリ単位の精度で自在に制御するためのベクター描画プリミティブを提供します。

04_shapes_and_lines_1
図 4.1: Drawlib が提供する全 43 種の基本図形から抜粋した代表プリミティブ一覧
Source Code
from drawlib.canvas import save, setup
from drawlib.shapes import (
    bubblespeech,
    chevron,
    circle,
    cloud,
    cylinder,
    document,
    rectangle,
    regularpolygon,
)
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=144, height=68)

xs = [21.0, 55.0, 89.0, 123.0]
ys = [49.5, 18.5]
slot_style = Styles.White.patch(
    shape_r=2.0,
    shape_line_color=Colors.Gray3,
    shape_line_width=1.0,
)

for ry in ys:
    for cx in xs:
        rectangle((cx, ry), width=31.0, height=26.5, style=slot_style)

label_style = Styles.DarkBold.patch(text_size=10.0)
body_dark = Styles.DarkBold.patch(text_size=10.5)

# 1段目: 角丸矩形・中抜きリング・正多角形・シェブロン矢印
rectangle(
    (xs[0], 52.8),
    width=23.0,
    height=12.0,
    style=Styles.PrimaryFlat.patch(shape_r=2.5),
    text="注目カード",
    text_style=Styles.WhiteBold.patch(text_size=10.8),
)
text((xs[0], 40.2), "rectangle (shape_r)", style=label_style)

circle(
    (xs[1], 52.8),
    radius=6.8,
    hole_ratio=0.48,
    style=Styles.PrimaryNeutral,
)
text((xs[1], 40.2), "circle (hole_ratio)", style=label_style)

regularpolygon(
    (xs[2], 52.8),
    6,
    7.2,
    style=Styles.SecondaryNeutral.patch(shape_r=0.9),
    text="六角形",
    text_style=body_dark,
)
text((xs[2], 40.2), "regularpolygon", style=label_style)

chevron(
    (xs[3], 52.8),
    width=20.5,
    height=11.0,
    corner_angle=48,
    style=Styles.Neutral,
    text="工程 1",
    text_style=body_dark,
)
text((xs[3], 40.2), "chevron", style=label_style)

# 2段目: 円柱ストレージ・クラウド境界・吹き出し・積層ドキュメント
cylinder(
    (xs[0], 22.0),
    width=18.5,
    height=13.0,
    disks=1,
    style=Styles.PrimaryNeutral,
    text="永続DB",
    text_style=Styles.DarkBold.patch(text_size=10.2, xy_abs_shift=(0, -1.5)),
)
text((xs[0], 9.2), "cylinder", style=label_style)

cloud(
    (xs[1], 22.0),
    width=23.5,
    height=13.5,
    style=Styles.Neutral,
    text="VPC 境界",
    text_style=Styles.DarkBold.patch(text_size=10.2),
)
text((xs[1], 9.2), "cloud", style=label_style)

bubblespeech(
    (xs[2] - 10.5, 19.2),
    width=21.0,
    height=9.5,
    tail_edge="bottom",
    tail_start_ratio=0.32,
    tail_vertex_xy=(85.0, 14.5),
    tail_end_ratio=0.55,
    style=Styles.SecondaryNeutral,
    text="応答 < 5ms",
    text_style=Styles.DarkBold.patch(text_size=10.0),
)
text((xs[2], 9.2), "bubblespeech", style=label_style)

document(
    (xs[3], 22.0),
    width=19.5,
    height=13.0,
    stacked=2,
    style=Styles.Neutral,
    text="設計仕様",
    text_style=Styles.DarkBold.patch(text_size=10.2, xy_abs_shift=(0, 0.8)),
)
text((xs[3], 9.2), "document", style=label_style)

save()

4.1 drawlib.shapes が提供する全 43 種の基本図形

drawlib.shapes は用途別に整理された 6 ファミリー・全 43 種のベクター図形関数を提供します。すべての図形関数はキーワード専用の必須引数 *, style: Style を受け取り、座標ジオメトリと視覚スタイルを明確に分離します。

ファミリー 数 drawlib.shapes の関数名 主なエンジニアリング用途
基本図形 (Basic) 6 rectangle, circle, ellipse, arc, fan, wedge サービスカード(rectangle + shape_r)、ノード、ドーナツ/リング(circle の hole_ratio や wedge の width)、扇形
多角形・自由パス (Polygons) 9 triangle, rhombus, parallelogram, trapezoid, regularpolygon, star, plaque, polygon, shape 条件分岐(rhombus)・入出力(parallelogram)、五角形/六角形/八角形(regularpolygon(xy, n, r))、任意の中抜き多角形(polygon + holes)
ブロック矢印・シェブロン (Arrows) 6 arrow, arrow_polyline, arrow_arc, arrow_l, arrow_u, chevron フェーズ見出し(chevron)、片方向/双方向ブロック矢印(arrow(..., head="<->"))、L 字・U 字ターン矢印
3D コンテナ・境界 (Containers) 6 cylinder, cube, document, funnel, cloud, brace データベース(cylinder の disks=2)、3D ノード(cube)、波形/折り返し文書(document)、クラウド境界、波括弧(brace)
バッジ・吹き出し (Badges) 8 bubblespeech, ribbon, shield, tag, pin, checkmark, forbidden, explosion 注釈コールアウト(bubblespeech)、セキュリティ盾(shield)、リリースタグ(tag・ribbon)、位置ピン(pin)、合否ステータス
記号・フロー演算子 (Symbols) 8 clock, face, gear, heart, lightning, math_symbol, moon, sun スケジューラ/遅延(clock)、ワーカー処理(gear)、十字/四則演算記号(math_symbol)、イベント発火(lightning)

システム構成図や業務フローチャートで頻出する標準記号も、これらのプリミティブにそのまま対応します(分岐判定は rhombus、データ入出力は parallelogram、帳票・仕様書は document、永続ストレージは cylinder、処理ステップは角丸 rectangle)。大規模なフロー図は drawlib.diagrams.flow.FlowDiagram で自動レイアウトすることも可能です。


4.2 中心アンカー (cx, cy)・インラインテキスト・角丸・回転

HTML/DOM の左上基準やチャートの左下基準とは異なり、drawlib.shapes の平面図形は幾何学的な中心座標 xy = (cx, cy) をアンカーとします(始点・頂点列を指定する arrow(xy1, xy2, ...)・arrow_polyline(xys, ...)・polygon(xys, ...) および吹き出し本体の左下 (x0, y0) を指定する bubblespeech(xy, ...) が例外です)。幅や高さの半分を引き算する必要がなく、共通の y = 20 を指定するだけで異種図形の中心軸がピタリと揃います。

さらに以下の 3 つの仕組みにより、ボイラープレートのない簡潔な記述が可能です。

from drawlib.canvas import save, setup
from drawlib.shapes import circle, rectangle, regularpolygon
from drawlib.styles import Styles

setup(width=120, height=40)

# すべて y=20 の中心アンカーで一列に整列し、テキスト・角丸・回転を宣言的に適用
rectangle(
    (25, 20),
    width=30,
    height=16,
    style=Styles.PrimaryFlat.patch(shape_r=2.5),
    text="入口GW",
    text_style=Styles.WhiteBold.patch(text_size=11.0),
)
regularpolygon(
    (65, 20),
    6,
    10.0,
    style=Styles.PrimaryNeutral.patch(angle=15, shape_r=1.0),
    text="ルーター",
    text_style=Styles.DarkBold.patch(text_size=10.5, angle=0),
)
circle(
    (100, 20),
    radius=9.5,
    hole_ratio=0.5,
    style=Styles.Neutral,
    text="50%",
    text_style=Styles.DarkBold.patch(text_size=10.5),
)

save()

4.3 ベクター接続線(drawlib.lines)と幾何計算(drawlib.math)

配置した図形同士を美しく結ぶため、drawlib.lines は多彩な 1 次元ストローク接続線を提供し、drawlib.math は三角関数の手計算を不要にする幾何計算ヘルパーを提供します。

04_shapes_and_lines_2
図 4.2: ベクター接続線(line・lines_curved・line_curved・lines_bezier)と幾何補間ヘルパー
Source Code
from drawlib.canvas import save, setup
from drawlib.lines import line, line_curved, lines_bezier, lines_curved
from drawlib.math import get_intermediate_points, rotate_point
from drawlib.shapes import circle, rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=144, height=68)

hdr = Styles.DarkBold.patch(text_size=10.5, halign="left")
note = Styles.Dark.patch(text_size=10.0)
node_light = Styles.White.patch(shape_r=1.8)
node_sec = Styles.SecondaryNeutral.patch(shape_r=1.8)
lbl_dark = Styles.DarkBold.patch(text_size=10.2)

# ゆとりある 4 分割ニュートラルパネル
rectangle((37.5, 49.5), width=64.0, height=26.5, style=Styles.PrimaryNeutral.patch(shape_r=2.2))
rectangle((106.5, 49.5), width=64.0, height=26.5, style=Styles.Neutral.patch(shape_r=2.2))
rectangle((37.5, 18.5), width=64.0, height=26.5, style=Styles.Neutral.patch(shape_r=2.2))
rectangle((106.5, 18.5), width=64.0, height=26.5, style=Styles.SecondaryNeutral.patch(shape_r=2.2))

# パネル 1: 双方向直線 "<->" + get_intermediate_points()
text((9.0, 59.0), '1. line("<->") + get_intermediate_points()', style=hdr)
rectangle(
    (16.0, 48.0),
    width=12.5,
    height=8.2,
    style=Styles.PrimaryFlat.patch(shape_r=1.8),
    text="送信",
    text_style=Styles.WhiteBold.patch(text_size=10.2),
)
rectangle((59.0, 48.0), width=12.5, height=8.2, style=node_light, text="受信", text_style=lbl_dark)
line((22.5, 48.0), (52.5, 48.0), arrow_head="<->", style=Styles.DarkBold)
for pt in get_intermediate_points((22.5, 48.0), (52.5, 48.0), num=3):
    circle(pt, radius=1.3, style=Styles.AccentFlat)
text((37.5, 39.8), "num=3 の等間隔補間点", style=note)

# パネル 2: lines_curved(r=3.5) による角丸マンハッタン経路
text((78.0, 59.0), '2. lines_curved(xys, r=3.5, arrow_head="->")', style=hdr)
rectangle((85.5, 51.8), width=14.0, height=7.8, style=node_light, text="受付GW", text_style=lbl_dark)
rectangle((127.5, 42.2), width=14.0, height=7.8, style=node_sec, text="処理系", text_style=lbl_dark)
route = [(92.5, 51.8), (106.5, 51.8), (106.5, 42.2), (120.5, 42.2)]
lines_curved(route, r=3.5, arrow_head="->", style=Styles.DarkBold)
text((91.5, 41.2), "r=3.5 の角丸フィレット", style=note)

# パネル 3: line_curved(bend=...) による往復円弧スプライン
text((9.0, 28.0), "3. line_curved(bend=-0.22) による往復経路", style=hdr)
rectangle((16.0, 16.0), width=13.0, height=8.2, style=node_light, text="アプリ", text_style=lbl_dark)
rectangle((59.0, 16.0), width=13.0, height=8.2, style=node_sec, text="キャッシュ", text_style=lbl_dark)
line_curved((22.8, 18.2), (52.2, 18.2), bend=-0.22, arrow_head="->", style=Styles.PrimaryBold)
line_curved((52.2, 13.8), (22.8, 13.8), bend=-0.22, arrow_head="->", style=Styles.DarkBold)
text((37.5, 16.0), "bend = -0.22", style=note)

# パネル 4: lines_bezier() の S 字曲線 + rotate_point() の放射配置
text((78.0, 28.0), "4. lines_bezier() + rotate_point()", style=hdr)
rectangle((85.5, 20.8), width=13.5, height=7.6, style=node_light, text="イベント", text_style=lbl_dark)
hub = (125.5, 16.0)
circle(hub, radius=4.8, style=Styles.White, text="ハブ", text_style=lbl_dark)
for deg in [35, 145, 270]:
    sat = rotate_point((hub[0] + 7.2, hub[1]), deg, center=hub)
    circle(sat, radius=1.2, style=Styles.PrimaryFlat)
lines_bezier(
    (92.5, 20.8),
    [((107.0, 20.8), (106.0, 16.0), (120.4, 16.0))],
    arrow_head="->",
    style=Styles.DarkBold,
)
text((92.0, 10.2), "三次ベジェ S 字曲線", style=note)

save()

5. スタイルシステム・デザイントークン・多言語フォント

Drawlib は、イミュータブルな Style / Color モデル、セマンティックなデザイントークン(Styles.*・Colors.*)、およびオンデマンド自動キャッシュ対応の多言語フォントエンジン(drawlib.fonts)によって、座標ロジックと視覚デザインを完全に分離します。

05_colors_and_styles_1
図 5.1: GoogleStyles トークン一覧 — ニュートラル基盤・外枠/基本ロール・強調 Flat・線種バリエーション
Source Code
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=144, height=74)

row_hdr = Styles.DarkBold.patch(text_size=10.2, halign="left")
dark_lbl = Styles.DarkBold.patch(text_size=10.0)
white_lbl = Styles.WhiteBold.patch(text_size=10.0)
xs = [45.5, 72.2, 98.9, 125.6]
sw_w, sw_h = 24.2, 10.8

# 1段目: ニュートラル基盤(全図の 50% 以上を構成)
text((6.0, 61.0), "1. ニュートラル基盤\n   (50%以上原則)", style=row_hdr)
r1 = [
    ("Neutral", Styles.Neutral),
    ("PrimaryNeutral", Styles.PrimaryNeutral),
    ("SecondaryNeutral", Styles.SecondaryNeutral),
    ("White", Styles.White),
]
for cx, (name, st) in zip(xs, r1):
    rectangle((cx, 61.0), width=sw_w, height=sw_h, style=st.patch(shape_r=1.8), text=name, text_style=dark_lbl)

# 2段目: 外枠線(Outline)と基本ロール
text((6.0, 44.5), "2. 外枠・基本ロール\n   (*Outline / *)", style=row_hdr)
r2 = [
    ("PrimaryOutline", Styles.PrimaryOutline, Styles.PrimaryBold.patch(text_size=10.0)),
    ("SecondaryOutline", Styles.SecondaryOutline, Styles.SecondaryBold.patch(text_size=10.0)),
    ("AccentOutline", Styles.AccentOutline, Styles.DarkBold.patch(text_size=10.0)),
    ("Primary", Styles.Primary, white_lbl),
]
for cx, (name, st, ts) in zip(xs, r2):
    rectangle((cx, 44.5), width=sw_w, height=sw_h, style=st.patch(shape_r=1.8), text=name, text_style=ts)

# 3段目: 強調ベタ塗りトークン(*Flat + WhiteBold)
text((6.0, 28.0), "3. 強調ベタ塗り\n   (*Flat + 白文字)", style=row_hdr)
r3 = [
    ("PrimaryFlat", Styles.PrimaryFlat),
    ("AccentFlat", Styles.AccentFlat),
    ("SuccessFlat", Styles.SuccessFlat),
    ("DangerFlat", Styles.DangerFlat),
]
for cx, (name, st) in zip(xs, r3):
    rectangle((cx, 28.0), width=sw_w, height=sw_h, style=st.patch(shape_r=1.8), text=name, text_style=white_lbl)

# 4段目: 接続線・境界線種バリエーション
text((6.0, 11.5), "4. 線種トークン\n   (Bold / Dashed)", style=row_hdr)
r4 = [
    ("DarkBold", Styles.DarkBold),
    ("PrimaryBold", Styles.PrimaryBold),
    ("SecondaryDashed", Styles.SecondaryDashedBold),
    ("MutedDotted", Styles.MutedDottedBold),
]
for cx, (name, st) in zip(xs, r4):
    line((cx - 10.5, 14.2), (cx + 10.5, 14.2), arrow_head="->", style=st)
    text((cx, 8.2), name, style=dark_lbl)

save()

5.1 動的テーマ注入: drawlib.styles と preset_styles の分離

作図スクリプトおよび Markdown の drawlib ブロック内では、常に drawlib.styles から PascalCase の Colors と Styles をインポートします。

from drawlib.styles import Colors, Styles

モジュール名と衝突する小文字の styles / colors は使わず、また各図のコード内で GoogleStyles などの具象プリセットを直接インポートしないのが鉄則です。drawlib.styles はプロジェクト直下の styles.py(および多言語構成における ja/styles.py などの言語別スタイル定義)から動的に注入されるファサードとして機能します。

すべてのテーマが同一のトークン名を備えているため、styles.py を 1 箇所変更するだけで全ページの図表テーマを一括で切り替えられます。


5.2 セマンティックトークン体系と「50%+ ニュートラル」原則

読みやすいアーキテクチャ図は、虹色の乱用を避けた明確な視覚階層から生まれます。各テーマは意味的ロールと直交サフィックスの組み合わせで構成されています。

トークン種別・サフィックス 代表トークン 視覚効果とコントラスト規律
ニュートラル基盤 Neutral, PrimaryNeutral, SecondaryNeutral, White 落ち着いた淡色背景と控えめな枠線。全ノード・カードの 50% 以上をこの層で構成します。 Styles.White 上の文字には必ず text_style=Styles.DarkBold を指定します。
セマンティックロール Primary, Secondary, Accent, Warning, Success, Danger, Muted 青(Primary)・紫/藍(Secondary)・橙(Accent)・緑(Success)・赤(Danger)・灰(Muted)に加え、淡色 Primary1〜濃色 Primary6 の明度段階を提供します。
外枠線(*Outline / *Solid) PrimaryOutline, SecondaryOutline, AccentOutline 塗りつぶしなしの透過内部+色付き実線枠(線幅 1.5)。論理ゾーンや軽量バッジに最適です。
強調ベタ塗り(*Flat) PrimaryFlat, AccentFlat, SuccessFlat, DangerFlat 枠線なしの高彩度ベタ塗り(shape_line_width=0)。図内で 1〜2 個の主役要素に限定し、必ず Styles.WhiteBold の白太字と組み合わせます。
太さ・線種サフィックス *Bold, *Thin, *Dashed, *Dotted 線幅とフォントウェイト(Bold=2.5・Thin=0.75、単体ロール Styles.Light もあり)や破線・点線(MutedDashed・SecondaryDashedBold・PrimaryDotted)を切り替えます。

5.3 イミュータブルな .patch()・.patch_font() と多言語フォント

すべての Style および Color インスタンスは**完全にイミュータブル(不変)**です。角丸・線幅・文字サイズ・揃え位置・不透明度を調整する際は、素の Style(...) をゼロから生成するのではなく、必ず既存の Styles.* トークンに対して .patch(...) を呼び出します。これにより、フォントファミリーやアイコンウェイト、テーマ切り替え時の互換性が安全に継承されます。

from drawlib.canvas import save, setup
from drawlib.fonts import FontFile, FontJapanese, FontMonoSpace
from drawlib.preset_styles import GoogleStyles
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles

# ja/styles.py で用いられる言語別フォント一括差し替えパターン:
JaStyles = GoogleStyles().patch_font(
    regular=FontJapanese.SANSSERIF_REGULAR,
    bold=FontJapanese.SANSSERIF_BOLD,
    thin=FontJapanese.SANSSERIF_THIN,
)
# ローカルの .ttf / .otf ファイルも FontFile("_assets/brand.ttf") で読み込み可能
_ = FontFile

setup(width=120, height=36)

card_style = Styles.PrimaryNeutral.patch(
    shape_r=2.8,
    shape_line_width=2.0,
    alpha=0.95,
)
mono_label = Styles.DarkBold.patch(
    text_font=FontMonoSpace.ROBOTO_MONO_BOLD,
    text_size=11.0,
    halign="center",
    valign="center",
)
glass_tint = Colors.Primary.patch(alpha=0.15)
_ = (JaStyles, glass_tint)

rectangle((60, 18), width=56, height=18, style=card_style, text="api.internal:8443", text_style=mono_label)
save()

drawlib.fonts の内蔵フォント群は初回参照時に自動ダウンロード&ローカルキャッシュされ、OS ごとのフォント差異による文字化け(豆腐化)を根絶します(オフライン環境では uv run drawlib cache download --all で事前取得可能)。

05_colors_and_styles_2
図 5.2: .patch() による段階的スタイル派生と多言語フォント見本
Source Code
from drawlib.canvas import save, setup
from drawlib.fonts import FontJapanese, FontMonoSpace, FontRoboto, FontSerif
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=144, height=66)

sec_hdr = Styles.DarkBold.patch(text_size=10.8, halign="left")
xs = [26.0, 72.0, 118.0]

# 上段: .patch() による段階的スタイル変換
text((8.5, 59.5), "A. イミュータブルな Style.patch() 派生チェーン", style=sec_hdr)

rectangle(
    (xs[0], 46.5),
    width=35.0,
    height=15.0,
    style=Styles.Neutral,
    text="Styles.Neutral\n(基本矩形カード)",
    text_style=Styles.DarkBold.patch(text_size=10.2),
)
line((44.5, 46.5), (53.5, 46.5), arrow_head="->", style=Styles.DarkBold)

rectangle(
    (xs[1], 46.5),
    width=35.0,
    height=15.0,
    style=Styles.PrimaryNeutral.patch(shape_r=3.0),
    text=".patch(shape_r=3.0)\n(角丸フィレット適用)",
    text_style=Styles.DarkBold.patch(text_size=10.2),
)
line((90.5, 46.5), (99.5, 46.5), arrow_head="->", style=Styles.DarkBold)

rectangle(
    (xs[2], 46.5),
    width=35.0,
    height=15.0,
    style=Styles.SecondaryNeutral.patch(
        shape_r=3.0,
        shape_line_style="dashed",
        shape_line_width=2.0,
    ),
    text='+ line_style="dashed"\n(破線ゾーン境界)',
    text_style=Styles.DarkBold.patch(text_size=10.2),
)

# 下段: 内蔵フォント見本
text((8.5, 29.0), "B. 自動キャッシュ対応の多言語フォント見本 (drawlib.fonts)", style=sec_hdr)

rectangle((xs[0], 15.5), width=35.0, height=16.0, style=Styles.White.patch(shape_r=2.2))
text((xs[0], 18.8), "FontSerif", style=Styles.DarkBold.patch(text_font=FontSerif.MERRIWEATHER_BOLD, text_size=11.0))
text((xs[0], 12.2), "Merriweather & Roboto", style=Styles.Dark.patch(text_font=FontRoboto.ROBOTO_REGULAR, text_size=10.0))

rectangle((xs[1], 15.5), width=35.0, height=16.0, style=Styles.Neutral.patch(shape_r=2.2))
text((xs[1], 18.8), "FontMonoSpace", style=Styles.DarkBold.patch(text_font=FontMonoSpace.ROBOTO_MONO_BOLD, text_size=10.8))
text((xs[1], 12.2), "10.0.4.12:8443", style=Styles.Dark.patch(text_font=FontMonoSpace.ROBOTO_MONO_REGULAR, text_size=10.0))

rectangle((xs[2], 15.5), width=35.0, height=16.0, style=Styles.PrimaryFlat.patch(shape_r=2.2))
text((xs[2], 18.8), "FontJapanese", style=Styles.WhiteBold.patch(text_font=FontJapanese.SANSSERIF_BOLD, text_size=11.0))
text((xs[2], 12.2), "日本語・CJK ゴシック", style=Styles.White.patch(text_font=FontJapanese.SANSSERIF_REGULAR, text_size=10.2))

save()

6. 34種のアイコンスイートと画像アセット処理

システム構成図や技術ドキュメントに直感的な UI アイコン・公式クラウドサービスロゴ・鮮明なラスター画像を組み合わせることで、図解の情報伝達力は飛躍的に高まります。Drawlib はオンデマンド自動キャッシュ対応の全 34 種の内蔵アイコンスイート(drawlib.icons)と、非破壊画像変換およびサブキャンバス合成エンジン(drawlib.images)を標準搭載しています。

06_icons_and_images_1
図 6.1: UI グリフ・主要クラウド・15 領域ブランド・Original 2D スイートから成る全 34 種の内蔵アイコン
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import (
    ai,
    aws,
    azure,
    cnative,
    data,
    gcp,
    original_ai,
    original_cnative,
    original_data,
    original_security,
    phosphor,
)
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=144, height=68)

hdr = Styles.DarkBold.patch(text_size=10.5, halign="left")
lbl = Styles.DarkBold.patch(text_size=10.0)
xs_l = [15.5, 30.2, 44.8, 59.5]
xs_r = [84.5, 99.2, 113.8, 128.5]

# 4 つのニュートラル分類カード
rectangle((37.5, 49.5), width=64.0, height=26.5, style=Styles.PrimaryNeutral.patch(shape_r=2.2))
rectangle((106.5, 49.5), width=64.0, height=26.5, style=Styles.Neutral.patch(shape_r=2.2))
rectangle((37.5, 18.5), width=64.0, height=26.5, style=Styles.Neutral.patch(shape_r=2.2))
rectangle((106.5, 18.5), width=64.0, height=26.5, style=Styles.SecondaryNeutral.patch(shape_r=2.2))

# 1. Phosphor 汎用 UI ベクターグリフ
text((9.0, 59.0), "1. 汎用 UI ベクターグリフ (phosphor)", style=hdr)
phosphor.cpu((xs_l[0], 50.2), width=7.2, style=Styles.Dark)
phosphor.database((xs_l[1], 50.2), width=7.2, style=Styles.PrimaryBold)
phosphor.shield_check((xs_l[2], 50.2), width=7.2, style=Styles.Success.patch(icon_style="fill"))
phosphor.git_branch((xs_l[3], 50.2), width=7.2, style=Styles.SecondaryBold)
for cx, name in zip(xs_l, ["cpu", "database", "shield", "git_branch"]):
    text((cx, 41.0), name, style=lbl)

# 2. 公式クラウドスイート (gcp, aws, azure)
text((78.0, 59.0), "2. 公式クラウド (gcp, aws, azure)", style=hdr)
gcp.cloud_run((xs_r[0], 50.2), width=7.8, style=Styles.Primary)
gcp.bigquery((xs_r[1], 50.2), width=7.8, style=Styles.Primary)
aws.aws_lambda((xs_r[2], 50.2), width=7.8, style=Styles.Primary)
azure.cosmos_db((xs_r[3], 50.2), width=7.8, style=Styles.Primary)
for cx, name in zip(xs_r, ["Cloud Run", "BigQuery", "Lambda", "Cosmos DB"]):
    text((cx, 41.0), name, style=lbl)

# 3. 15 領域のマルチブランドスイート (ai .. workspace)
text((9.0, 28.0), "3. 15 領域ブランド (ai, cnative, data...)", style=hdr)
ai.gemini((xs_l[0], 19.2), width=7.8, style=Styles.Primary)
cnative.kubernetes((xs_l[1], 19.2), width=7.8, style=Styles.Primary)
cnative.docker((xs_l[2], 19.2), width=7.8, style=Styles.Primary)
data.postgresql((xs_l[3], 19.2), width=7.8, style=Styles.Primary)
for cx, name in zip(xs_l, ["gemini", "k8s", "docker", "postgres"]):
    text((cx, 10.0), name, style=lbl)

# 4. style= による背景ティント&枠線付き 15 種 original_* スイート
text((78.0, 28.0), "4. 15 種 original_* (style= ティント枠)", style=hdr)
tinted = Styles.Secondary.patch(
    image_tint_color=Colors.White,
    image_border_color=Colors.Secondary,
    image_border_width=1.0,
)
original_ai.ai_agent((xs_r[0], 19.2), width=7.8, style=tinted)
original_data.data_lakehouse((xs_r[1], 19.2), width=7.8, style=tinted)
original_security.zero_trust((xs_r[2], 19.2), width=7.8, style=tinted)
original_cnative.argo((xs_r[3], 19.2), width=7.8, style=tinted)
for cx, name in zip(xs_r, ["ai_agent", "lakehouse", "zero_trust", "argo"]):
    text((cx, 10.0), name, style=lbl)

save()

6.1 全 34 種の内蔵アイコンスイート体系

drawlib.icons のすべてのアイコン関数は、基本図形と共通の中心アンカー xy = (cx, cy)・1:1 正方形サイズ width=・必須キーワード引数 *, style: Style に統一されています。各スイートは初回使用時に自動ダウンロード&キャッシュされます(オフライン CI 用に uv run drawlib cache download --all で一括事前取得も可能)。

ティア スイート構成(全 34 種) 描画方式と style= カスタマイズ
1. 汎用 UI ベクターアイコン (1) phosphor(1,500 種以上+低レベル font_icon) style.icon_style("thin", "light", "regular", "bold", "fill" または Styles.*Bold / *Flat)の 5 ウェイトに対応する鮮明なベクターフォントグリフ。style.icon_color で直接着色可能。
2. 公式クラウドスイート (3) gcp, aws, azure Google Cloud・AWS・Microsoft Azure の公式サービスアイコン(512x512 RGBA)。
3. 領域別マルチブランド (15) ai, business, client, cloud, cnative, code, data, devops, facility, itservice, network, security, server, social, workspace 生成 AI・CI/CD&DevOps・コンテナ/Kubernetes(cnative)・DB/データ基盤(data)・フロントエンド(client)・サーバー/設備・ネットワーク・セキュリティ等の主要製品ロゴを網羅。
4. original_* 2D スイート (15) original_ai 〜 original_workspace 各ブランドを統一トーンのフラット 2D パステル調に揃えたツイン版に加え、ベンダー中立な概念アイコン(original_ai.ai_agent・original_data.data_lakehouse・original_security.zero_trust 等)を収録。style= で背景ティント(image_tint_color)・外枠(image_border_*)・回転・透過を指定でき、Dimage.colorize() で単色リカラーも可能。

6.2 単体アイコン描画関数と高レベル Enum トークンの使い分け

アイコンの利用方法は、自由配置キャンバスか高レベル構成図かに応じて 2 通りのイディオムを使い分けます。

  1. 単体関数の直接呼び出し(drawlib.icons.<suite>.<name>): phosphor.cpu((25, 20), width=8.0, style=Styles.PrimaryBold)(または Styles.Accent.patch(icon_style="fill"))や gcp.cloud_run((60, 20), width=9.0, style=Styles.Primary) のように、キャンバス上の中心座標 (cx, cy) へ直接描画します。
  2. 型安全な Enum トークン(PhosphorIcon・GcpIcon・AwsIcon・CnativeIcon 等): 高レベル構成図モジュール(drawlib.diagrams.architecture・drawlib.diagrams.sequence)やアイコン対応 SmartArts では、StrEnum トークンを Node(..., icon=GcpIcon.CLOUD_RUN) のように渡すだけでアイコンとラベルの余白・整列が自動計算されます。
from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import ArchitectureDiagram, GcpIcon, Node, PhosphorIcon
from drawlib.icons import phosphor
from drawlib.styles import Styles

setup(width=120, height=40)

# 1. 中心座標 (cx, cy) を指定する単体アイコン描画
phosphor.sparkle((16, 20), width=7.5, style=Styles.Accent.patch(icon_style="fill"))

# 2. 高レベル ArchitectureDiagram への Enum トークン指定
arch = ArchitectureDiagram(
    node_style=Styles.PrimaryBold,
    node_text_style=Styles.DarkBold.patch(text_size=11.0),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
)
gw = arch.add(Node("境界ゲートウェイ", width=34, height=16, icon=PhosphorIcon.SHIELD_CHECK), xy=(52, 20))
svc = arch.add(
    Node(
        "Cloud Run API",
        width=34,
        height=16,
        icon=GcpIcon.CLOUD_RUN,
        card_style=Styles.PrimaryNeutral,
    ),
    xy=(98, 20),
)
arch.connect(gw, svc, arrow="->")
arch.draw()

save()

6.3 ラスター画像(image)・Dimage 変換・サブキャンバス合成

画面キャプチャやマスコット画像、入れ子のミニ図解を扱うために、drawlib.images は 3 つの機能を提供します。

06_icons_and_images_2
図 6.2: Dimage("_assets/linux.png") の非破壊フィルタパイプラインとスタイル付きカードへの埋め込み
Source Code
from drawlib.canvas import save, setup
from drawlib.images import Dimage, get_dimage_from_code, image
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=144, height=62)

base = Dimage("_assets/linux.png")
xs = [20.5, 54.8, 89.2, 123.5]
card_w, card_h, cy = 28.5, 50.0, 31.0

title_st = Styles.DarkBold.patch(text_size=10.5)
code_st = Styles.Dark.patch(text_size=10.0)

# ステップ 1: 元のラスター画像アセット
rectangle((xs[0], cy), width=card_w, height=card_h, style=Styles.Neutral.patch(shape_r=2.2))
text((xs[0], 51.0), "1. 元画像", style=title_st)
image((xs[0], 31.8), width=14.5, image=base)
text((xs[0], 12.0), "Dimage(\n'_assets/linux.png')", style=code_st)

# ステップ 2: 左下基準 crop + 左右反転 mirror + 角度付き配置
rectangle((xs[1], cy), width=card_w, height=card_h, style=Styles.PrimaryNeutral.patch(shape_r=2.2))
text((xs[1], 51.0), "2. 切り抜き・回転", style=title_st)
cropped = base.crop(110, 340, 455, 440).mirror()
image(
    (xs[1], 31.8),
    width=14.5,
    image=cropped,
    style=Styles.Primary.patch(
        angle=10,
        image_border_width=1.2,
        image_border_color=Colors.Primary,
    ),
)
text((xs[1], 12.0), ".crop().mirror()\n+ angle=10", style=code_st)

# ステップ 3: グレースケール等の色調フィルタ
rectangle((xs[2], cy), width=card_w, height=card_h, style=Styles.Neutral.patch(shape_r=2.2))
text((xs[2], 51.0), "3. フィルタ変換", style=title_st)
image((xs[2], 31.8), width=14.5, image=base.grayscale())
text((xs[2], 12.0), ".grayscale()\n(.sepia / .blur)", style=code_st)

# ステップ 4: スタイル付きカードへの埋め込み + サブキャンバス合成
rectangle((xs[3], cy), width=card_w, height=card_h, style=Styles.SecondaryNeutral.patch(shape_r=2.2))
text((xs[3], 51.0), "4. カード合成", style=title_st)
rectangle(
    (xs[3], 43.8),
    width=22.5,
    height=5.6,
    style=Styles.PrimaryFlat.patch(shape_r=1.4),
    text="Linux ノード",
    text_style=Styles.WhiteBold.patch(text_size=10.0),
)
framed = base.resize(270, 320).fill(Colors.White)
image(
    (xs[3], 29.2),
    width=12.2,
    image=framed,
    style=Styles.Primary.patch(
        image_border_width=1.5,
        image_border_color=Colors.Primary,
    ),
)
sub_badge = get_dimage_from_code("""
from drawlib.canvas import setup
from drawlib.shapes import checkmark, circle
from drawlib.styles import Styles
setup(width=36, height=36)
circle((18, 18), radius=15, style=Styles.SuccessFlat)
checkmark((18, 18), width=15, style=Styles.WhiteFlat)
""").make_transparent(color="auto", tolerance=15)
image((xs[3] + 6.5, 22.8), width=5.6, image=sub_badge)
text((xs[3], 12.0), ".fill() + サブ描画\nget_dimage_from_code", style=code_st)

# 各ステージ間の矢印コネクタ
for i in range(3):
    line((xs[i] + 14.8, cy), (xs[i + 1] - 14.8, cy), arrow_head="->", style=Styles.DarkBold)

save()

7. SmartArts:18種の構造化ビジュアルコンポーネント

業務フロー図や比較表のたびに矩形やテキストの座標を手計算する必要はありません。drawlib.smartarts は、余白・整列・枠線・タイポグラフィを自動計算する 全18種の高レベル構造化コンポーネント を提供します。すべての SmartArt が GoogleStyles のデザイントークンと同一キャンバスを共有するため、KPI サマリー・パイプライン・SLA 比較表・ディレクトリツリーを1枚の図内で自在に組み合わせられます。

07_smartarts_1
図 7.1: KpiCards と ChevronProcess を組み合わせたリリースメトリクス・ダッシュボード
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import ChevronProcess, KpiCards
from drawlib.styles import Styles
from drawlib.text import text

setup(width=148, height=66)

text(
    (74.0, 60.5),
    "v0.3.7 リリース品質指標とドキュメント公開パイプライン",
    style=Styles.DarkBold.patch(text_size=12.5),
)

kpi = KpiCards(
    style=Styles.Neutral.patch(shape_r=1.8),
    metric_style=Styles.DarkBold.patch(text_size=15.5),
    text_style=Styles.DarkBold.patch(text_size=11.0),
    description_style=Styles.Muted.patch(text_size=10.0),
    accent_style=Styles.SecondaryFlat,
    icon_style=Styles.PrimaryBold,
    accent_position="top_bar",
    metric_layout="top_stack",
    spacing=3.5,
)
kpi.add(
    "99.99%",
    "稼働率 SLA",
    "マルチリージョン冗長構成",
    icon=phosphor.shield_check,
)
kpi.add(
    "11.4 ms",
    "エッジ P99 遅延",
    "v0.3.7 ゼロコピールーター",
    icon=phosphor.lightning,
    style=Styles.PrimaryFlat.patch(shape_r=1.8),
    metric_style=Styles.WhiteBold.patch(text_size=15.5),
    text_style=Styles.WhiteBold.patch(text_size=11.0),
    description_style=Styles.White.patch(text_size=10.0),
    accent_style=Styles.WhiteFlat,
    icon_style=Styles.WhiteBold,
)
kpi.add(
    "4.2x",
    "ビルド処理速度",
    "差分 SQLite キャッシュ",
    icon=phosphor.cpu,
    style=Styles.PrimaryNeutral.patch(shape_r=1.8),
    accent_style=Styles.PrimaryFlat,
)
kpi.draw(xy=(6.0, 29.0), width=136.0, height=26.0, columns=3)

pipeline = ChevronProcess(
    style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=10.8),
    description_style=Styles.Muted.patch(text_size=10.0),
    corner_angle=60.0,
    spacing=2.0,
    flat_left_end=True,
)
pipeline.add("1. 原稿執筆", description="Python + Markdown")
pipeline.add("2. グリッド検証", description="drawlib show -g")
pipeline.add(
    "3. 一括ビルド",
    description="HTML / PDF / Office",
    style=Styles.PrimaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=10.8),
    description_style=Styles.White.patch(text_size=10.0),
)
pipeline.add(
    "4. 公開配信",
    description="CI / 静的ポータル",
    style=Styles.SecondaryNeutral,
)
pipeline.draw(xy=(6.0, 7.0), width=136.0, height=16.5)

save()

7.1 全18種 SmartArts カタログと基準座標(アンカー)

Drawlib は左下 (0, 0)・上向き +Y のデカルト座標系を採用しているため、各 SmartArt の .draw(xy=...) がどの基準点(アンカー)を受け取るかを把握しておけば、試行錯誤なしで一発で正確な位置に配置できます。

7.1.1 プロセス・サイクル・タイムライン

コンポーネント 要素・補助モデル .draw(xy=...) 既定アンカー 主なエンジニアリング用途
ChevronProcess ChevronItem Bottom-Left (align="bottom_left") CI/CD パイプライン、ETL 処理段階、段階的移行フロー(flat_left_end=True)
Cycle CycleItem, CycleCenter Center (align="center") PDCA 改善ループ、トークン更新サイクル、SRE 運用フィードバック
Roadmap RoadmapItem Bottom-Left (align="bottom_left") 四半期リリースマイルストーン、移行ロードマップ、障害時系列の記録

7.1.2 カード・グリッド・サマリー

コンポーネント 要素・補助モデル .draw(xy=...) 既定アンカー 主なエンジニアリング用途
KpiCards (KpiCard) KpiCardItem Bottom-Left (align="bottom_left") SLA 目標値、レイテンシ指標、ベンチマーク結果のエグゼクティブサマリー
CardList CardListItem Bottom-Left (align="bottom_left") 縦並び・横並びの機能カード一覧(オプションで -> 接続矢印付き)
BoxList BoxListItem Bottom-Left (align="bottom_left") 密着配置のサービス群、プロトコルヘッダ構造、ステータスバッジ列
GridLayout GridItem Bottom-Left (align="bottom_left") 行・列結合に対応した多層アーキテクチャマトリクス(row=0 が最下段)
Pyramid PyramidItem Bottom-Left (align="bottom_left") テストピラミッド、記憶階層モデル、多層防御セキュリティ構造
Panel — Bottom-Left (align="bottom_left") タイトル付き枠コンテナ("filled" / "underline" / "floating" / "window")

7.1.3 テーブル・箇条書き・ソースコード

コンポーネント 要素・補助モデル .draw(xy=...) 既定アンカー 主なエンジニアリング用途
Table — Top-Left (align="top_left") サービス SLA 一覧表、機能比較マトリクス、クォータ仕様表(draw_flexible)
BulletPoints BulletPointItem Top-Left (固定 (x, y)) 設計レビュー要点、チェックリスト、スライド内の構造化箇条書き
SourceCode SourceCodeStyles, sourcecode Top-Left (align="top_left") 図内に直接埋め込む構文ハイライト付き Python / YAML / JSON コード枠

7.1.4 ツリー階層・マインドマップ

コンポーネント 要素・補助モデル .draw(xy=...) 既定アンカー 主なエンジニアリング用途
TreeNode — Top-Left (固定 (x, y)) モノレポのディレクトリ構成、パッケージ階層、CLI サブコマンド体系
MindMapNode — Center (固定 (cx, cy)) 上下左右4方向に展開するシステム分類図、ドメイン分割、組織図

7.1.5 地図・カレンダースケジュール

コンポーネント 要素・補助モデル .draw(xy=...) 既定アンカー 主なエンジニアリング用途
GeoMap — Bottom-Left (align="bottom_left") マルチリージョン拠点マップ(World / Countries / Cities、lonlat_to_xy)
MonthCalendar CalendarItem Bottom-Left (align="bottom_left") 7列の月間・複数週リリース計画カレンダー(ISO 8601 "YYYY-MM-DD")
WeekCalendar CalendarItem Bottom-Left (align="bottom_left") 時間軸付きのスプリント計画・メンテナンス枠("YYYY-MM-DD HH:MM")
DayCalendar CalendarItem Bottom-Left (align="bottom_left") 1日単位のタイムライン、障害対応・切り替え作業ランブック

ヒント — Top-Left 系と Bottom-Left 系を横並びで揃える計算式: 高さ H の Table や TreeNode を、下端 y_bottom の Bottom-Left コンポーネントと上端揃えにするには、xy=(x0, y_bottom + H) を渡します(Table と SourceCode では align="bottom_left" の直接指定も可能です)。

7.2 左上アンカーの組み合わせ:Table と TreeNode の並列配置

Table と TreeNode はどちらも 左上 Top-Left (x0, y_top) を起点とするため、仕様表とディレクトリ構成を横並びに配置する設計図で抜群の相性を発揮します。

07_smartarts_2
図 7.2: 左上アンカー(Top-Left)で揃えた Table と TreeNode の並列レイアウト
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.shapes import rectangle
from drawlib.smartarts import Table, TreeNode
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=148, height=64)

text(
    (43.0, 56.5),
    "プロジェクト種別と出力マトリクス (Table)",
    style=Styles.DarkBold.patch(text_size=11.5),
)
text(
    (114.5, 56.5),
    "自動生成されるディレクトリ構成 (TreeNode)",
    style=Styles.DarkBold.patch(text_size=11.5),
)

table = Table(
    cell_style=Styles.White,
    text_style=Styles.Dark.patch(text_size=10.2),
    header_cell_style=Styles.DarkFlat,
    header_text_style=Styles.WhiteBold.patch(text_size=10.5),
    border_style=Styles.MutedThin,
)
table.set_style_cell_evenodd(
    even_color=Colors.Gray1,
    even_text_style=Styles.Dark.patch(text_size=10.2),
    odd_color=Colors.White,
    odd_text_style=Styles.Dark.patch(text_size=10.2),
)
table.set_style_cell_header(
    background_color=Colors.Gray8,
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)
table.set_style_cell(
    background_color=Colors.Primary,
    text_style=Styles.WhiteBold.patch(text_size=10.2),
    rows=[1],
    columns=[2],
)
table.set_style_border(
    top=Styles.DarkBold.patch(line_width=1.4),
    top2=Styles.DarkBold.patch(line_width=1.0),
    bottom=Styles.DarkBold.patch(line_width=1.4),
    between_rows=Styles.MutedThin.patch(line_width=0.6),
)

matrix = [
    ["種別", "主な Web 出力", "Office / 印刷出力"],
    ["doc", "章構成 HTML", "PDF + DOCX"],
    ["site", "多階層ドキュメント", "llms.txt + SEO"],
    ["catalog", "検索付き API 辞書", "JSON インデックス"],
    ["slide", "16:9 Web スライド", "PDF + PPTX"],
]
table.draw_flexible(
    xy=(7.0, 51.5),
    column_widths=[18.0, 29.0, 25.0],
    row_heights=[8.6, 8.6, 8.6, 8.6, 8.6],
    data=matrix,
    align="top_left",
)

rectangle(
    (114.5, 30.0),
    width=53.0,
    height=43.0,
    style=Styles.Neutral.patch(shape_r=2.0),
)

TreeNode.register_drawing_item(
    name="ch07_dir_ja",
    location="before",
    padding_width=4.4,
    function=phosphor.folder,
    style=Styles.PrimaryBold,
    args={"width": 3.2},
)
TreeNode.register_drawing_item(
    name="ch07_py_ja",
    location="before",
    padding_width=4.4,
    function=phosphor.file_py,
    style=Styles.SecondaryBold,
    args={"width": 3.2},
)
TreeNode.register_drawing_item(
    name="ch07_md_ja",
    location="before",
    padding_width=4.4,
    function=phosphor.file_text,
    style=Styles.DarkBold,
    args={"width": 3.2},
)

root = TreeNode(
    "quickstart/",
    text_style=Styles.DarkBold.patch(text_size=10.2),
    line_style=Styles.Muted.patch(line_width=1.1),
    line_horizontal_margin=2.8,
    line_horizontal_length=2.8,
    line_vertical_margin=5.4,
    children=[
        TreeNode(
            "en/",
            text_style=Styles.DarkBold.patch(text_size=10.2),
            children=[
                TreeNode("00_cover.md", text_style=Styles.Dark.patch(text_size=10.0)).set_drawing_item("ch07_md_ja"),
                TreeNode("07_smartarts.md", text_style=Styles.PrimaryBold.patch(text_size=10.0)).set_drawing_item("ch07_md_ja"),
            ],
        ).set_drawing_item("ch07_dir_ja"),
        TreeNode(
            "ja/",
            text_style=Styles.DarkBold.patch(text_size=10.2),
            children=[
                TreeNode("07_smartarts.md", text_style=Styles.Dark.patch(text_size=10.0)).set_drawing_item("ch07_md_ja"),
            ],
        ).set_drawing_item("ch07_dir_ja"),
    ],
).set_drawing_item("ch07_dir_ja")
root.add("styles.py", text_style=Styles.Dark.patch(text_size=10.0)).set_drawing_item("ch07_py_ja")

root.draw(xy=(91.5, 47.0))

save()

7.3 show=False による段階的ビルドとアニメーション対応

状態を持つすべての SmartArt は4段階のライフサイクル(インスタンス生成 -> .add() で要素登録 -> .items / .children の状態変更 -> .draw() で描画)に従います。登録された各アイテムおよびノード(ChevronItem、KpiCardItem、CardListItem、BoxListItem、CycleItem、GridItem、PyramidItem、RoadmapItem、BulletPointItem、CalendarItem、TreeNode、MindMapNode)は共通して show: bool = True 属性を備えています。

item.show = False を設定(または .add(..., show=False) を指定)すると、他の兄弟要素の幅・列位置・円周角度・行間隔を一切変化させずに該当要素だけを非表示にできます。これにより、16:9 スライドの段階表示や drawlib.anim によるマルチフレームアニメーションでも、前段の要素がガタつくことなく滑らかにステップを追加できます。

from drawlib.canvas import save, setup
from drawlib.smartarts import ChevronProcess
from drawlib.styles import Styles

setup(width=120, height=32)

flow = ChevronProcess(
    style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=10.5),
    description_style=Styles.Muted.patch(text_size=10.0),
    flat_left_end=True,
)
flow.add("1. 収集", description="Parquet ストリーム")
flow.add(
    "2. 検証",
    description="スキーマ検査",
    style=Styles.PrimaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=10.5),
    description_style=Styles.White.patch(text_size=10.0),
)
# フレーム1では非表示にしても、ステップ1・2は4列分の正確な幅と座標を維持
step3 = flow.add("3. 変換", description="ベクトル結合", show=False)
step4 = flow.add("4. 配信", description="低遅延 API", show=False)

flow.draw(xy=(8.0, 8.0), width=104.0, height=16.0)
save()

各 SmartArt の詳細なコンストラクタ引数は、uv run drawlib rules show lib-smartarts-overview(または lib-smartarts-calendar、lib-smartarts-geomap、lib-smartarts-source-code などの個別トピック)でいつでも参照できます。

8. 定量チャート:10種の純粋 Python グラフ

技術白書・ベンチマークレポート・設計レビューでは、構成図だけでなく定量的なデータによる裏付けが不可欠です。drawlib.charts は Drawlib の描画エンジン上に構築された 全10種の純粋 Python チャート を提供します。すべてのチャートが 左下アンカー Bottom-Left (x0, y0)(align="bottom_left")を既定としているため、SmartArts・アイコン・注釈矢印と同一キャンバス上で精密に組み合わせて配置できます。

08_charts_1
図 8.1: 定量ダッシュボード — BarChart によるスループット比較と LineChart による遅延推移
Source Code
from drawlib.canvas import save, setup
from drawlib.charts.bar import BarChart
from drawlib.charts.line import LineChart
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles

setup(width=152, height=62)

card_bg = Styles.White.patch(shape_r=2.0, shape_line_color=Colors.Gray3)
rectangle((40.0, 31.0), width=68.0, height=50.0, style=card_bg)
rectangle((112.0, 31.0), width=68.0, height=50.0, style=card_bg)

bar = BarChart(
    axis_line_style=Styles.MutedDashed,
    categories=["白書", "サイト", "API辞書", "スライド"],
    width=62.0,
    height=38.0,
    bar_mode="group",
    bar_width_ratio=0.68,
    bar_r=0.8,
    axis_text_style=Styles.Muted.patch(text_size=10.0),
    grid_style=Styles.MutedThin,
    value_text_style=Styles.DarkBold.patch(text_size=10.0),
    value_format="{:.0f}",
    title="ビルド処理性能 (ページ / 秒)",
    title_style=Styles.DarkBold.patch(text_size=11.5),
)
bar.add_series("v0.3.0 従来版", [18.0, 24.0, 21.0, 29.0], style=Styles.SecondaryNeutral)
bar.add_series("v0.3.7 高速版", [42.0, 56.0, 49.0, 64.0], style=Styles.PrimaryFlat)
bar.configure_y_axis(min_value=0.0, max_value=80.0, tick_step=20.0)
bar.draw(xy=(9.0, 14.0))
bar.draw_legend(
    xy=(15.0, 10.0),
    text_style=Styles.DarkBold.patch(text_size=10.0),
    orientation="horizontal",
    item_gap=4.5,
)

line = LineChart(
    axis_line_style=Styles.MutedDashed,
    categories=["10頁", "25頁", "50頁", "100頁", "200頁"],
    width=62.0,
    height=38.0,
    smooth=False,
    show_points=True,
    point_size=0.75,
    axis_text_style=Styles.Muted.patch(text_size=10.0),
    grid_style=Styles.MutedThin,
    title="差分レンダリング遅延 (ms)",
    title_style=Styles.DarkBold.patch(text_size=11.5),
)
line.add_series(
    "初回",
    [14.0, 22.0, 34.0, 52.0, 72.0],
    style=Styles.Muted6Flat,
    line_width=1.9,
    line_style="dashed",
    point_shape="square",
)
line.add_series(
    "v0.3.7",
    [6.0, 8.0, 11.0, 15.0, 21.0],
    style=Styles.PrimaryFlat,
    line_width=2.4,
    point_shape="circle",
)
line.add_series(
    "SLO上限",
    [60.0, 60.0, 60.0, 60.0, 60.0],
    style=Styles.Danger,
    line_width=1.5,
    line_style="dotted",
    point_shape="none",
)
line.configure_y_axis(min_value=0.0, max_value=80.0, tick_step=20.0, format="{:.0f}", unit="ms")
line.draw(xy=(81.0, 14.0))
line.draw_legend(
    xy=(84.5, 10.0),
    text_style=Styles.DarkBold.patch(text_size=10.0),
    orientation="horizontal",
    item_gap=3.5,
)

save()

8.1 技術文書において drawlib.charts が Matplotlib に勝る理由

多言語対応の技術文書に外部の Matplotlib スクリプトを持ち込むと、フォントの不統一・煩雑な rcParams ボイラープレート・日本語の文字化け(豆腐 □□□)・他の構成図と組み合わせられない単体画像化といった問題が発生します。drawlib.charts はこれら4つの課題を根本から解決します。

8.2 全10種チャートカタログと使い分け

9つのサブモジュール(drawlib.charts.*)で提供される 全10種のチャートクラス はすべて 左下アンカー Bottom-Left (x0, y0) を既定とし、align="center" や比例スケーリング scale: float = 1.0 にも対応しています。

# チャートクラス サブモジュール .draw(xy=...) 既定アンカー 主なモードと機能
1 BarChart drawlib.charts.bar Bottom-Left (x0, y0) 縦棒/横棒、"group"/"stack"、角丸 bar_r、数値ラベル、"log" 対数軸
2 BoxChart drawlib.charts.box Bottom-Left (x0, y0) 生データ配列または BoxStats、"iqr"/"percentile" ひげ、show_points ジッター点群
3 LineChart drawlib.charts.line Bottom-Left (x0, y0) 複数系列折れ線・smooth=True スプライン、頂点マーカー、point_shape="none" 閾値線
4 AreaChart drawlib.charts.area Bottom-Left (x0, y0) "overlap" 重ね合わせ/"stack" 積み上げ面グラフ、透過度 fill_alpha 制御
5 PieChart drawlib.charts.pie Bottom-Left (x0, y0) 円グラフ(hole_ratio=0.0)とドーナツ型(hole_ratio=0.5..0.7 + center_text 中央指標)
6 RadarChart drawlib.charts.radar Bottom-Left (x0, y0) 多軸 "polygon"/"circle" レーダーチャート(levels、能力比較マトリクス)
7 ScatterChart drawlib.charts.scatter Bottom-Left (x0, y0) 2次元 (x, y) 相関プロットおよび Point(x, y, radius=...) によるバブルチャート
8 GanttChart drawlib.charts.gantt Bottom-Left (x0, y0) Section・進捗付き Task・Milestone・Marker・Dependency 矢印を備えた工程表
9 HeatmapChart drawlib.charts.heatmap Bottom-Left (x0, y0) 2次元強度マトリクス(min_style..max_style)、自動文字コントラスト、draw_colorbar
10 HeatmapOverlay drawlib.charts.heatmap Bottom-Left (x0, y0) 地図や構成図上に重ねる空間密度 Spot オーバーレイ("smooth"/"contour" 等高線)

8.2.1 カテゴリ・統計分布・2次元マトリクス系チャート

8.2.2 時系列推移・構成比・スケジュール系チャート

8.3 BoxChart による統計分布とデータストーリーテリング

説得力のある定量チャートを描く鉄則は 配色のストーリーテリング規律 です。5本のデータ系列に5色の原色を割り当てる虹色グラフは避け、比較対象の旧バージョン・ベースライン・周辺ティアには落ち着いたニュートラル系(Styles.Neutral、Styles.PrimaryNeutral、Styles.SecondaryNeutral、破線の Styles.Muted)を使い、強調したいターゲット系列1〜2本だけに Styles.PrimaryFlat や Styles.AccentFlat を割り当てます。

平均値だけの棒グラフでは見逃されがちなテールレイテンシのばらつきも、BoxChart を使えば中央値・四分位範囲(Q1〜Q3)・ひげ・外れ値を一目で比較できます。生のサンプル配列(list[float])と集計済み統計モデル(BoxStats)の両方を直接渡すことができ、濃色ボックス上の白太字中央値ラインも自動導出されます。

08_charts_2
図 8.2: BoxChart と BoxStats によるサービス層別のレイテンシ統計分布比較
Source Code
from drawlib.canvas import save, setup
from drawlib.charts.box import BoxChart, BoxStats
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles

setup(width=148, height=62)

rectangle(
    (74.0, 31.0),
    width=136.0,
    height=50.0,
    style=Styles.White.patch(shape_r=2.0, shape_line_color=Colors.Gray3),
)

box = BoxChart(
    axis_line_style=Styles.MutedDashed,
    categories=["エッジ CDN", "認証 API", "注文エンジン", "ベクトル検索"],
    width=124.0,
    height=38.0,
    box_width_ratio=0.62,
    box_r=0.6,
    show_outliers=True,
    axis_text_style=Styles.Muted.patch(text_size=10.0),
    grid_style=Styles.MutedThin,
    title="サービス層別レスポンス時間分布 (ms) — 従来プール vs. v0.3.7 非同期ランタイム",
    title_style=Styles.DarkBold.patch(text_size=11.5),
)
box.add_series(
    "従来プール (p5–p95 + p99外れ値)",
    [
        BoxStats(min=5.0, q1=8.0, median=11.0, q3=15.0, max=24.0, outliers=[34.0]),
        BoxStats(min=12.0, q1=18.0, median=24.0, q3=33.0, max=48.0, outliers=[66.0]),
        BoxStats(min=16.0, q1=24.0, median=32.0, q3=44.0, max=62.0, outliers=[76.0]),
        BoxStats(min=22.0, q1=32.0, median=43.0, q3=56.0, max=70.0, outliers=[78.0]),
    ],
    style=Styles.SecondaryNeutral,
)
box.add_series(
    "v0.3.7 非同期ランタイム (注目対象)",
    [
        BoxStats(min=3.0, q1=4.5, median=6.0, q3=8.0, max=12.0, outliers=[16.0]),
        BoxStats(min=6.0, q1=9.0, median=12.0, q3=16.0, max=22.0, outliers=[29.0]),
        BoxStats(min=8.0, q1=12.0, median=15.5, q3=20.0, max=28.0, outliers=[36.0]),
        BoxStats(min=11.0, q1=16.0, median=21.0, q3=27.0, max=38.0, outliers=[46.0]),
    ],
    style=Styles.PrimaryFlat,
)
box.configure_y_axis(
    min_value=0.0,
    max_value=85.0,
    tick_step=20.0,
    format="{:.0f}",
    unit="ms",
)
box.draw(xy=(12.0, 14.0))
box.draw_legend(
    xy=(36.0, 9.8),
    text_style=Styles.DarkBold.patch(text_size=10.0),
    orientation="horizontal",
    item_gap=6.0,
)

save()

試行回数 N = 8..25 程度の実測ベンチマークでは、生の list[float] とともに show_box=True, show_points=True, jitter=0.42 を指定すれば決定論的シードによるストリップ点群を重ね描きできます。また、LineChart や BarChart の draw_ratio(0.0〜1.0)を制御すれば、drawlib.anim による段階的アニメーションも簡単に構築できます。

from drawlib.canvas import save, setup
from drawlib.charts.line import LineChart
from drawlib.styles import Styles

setup(width=110, height=56)

trend = LineChart(
    axis_line_style=Styles.MutedDashed,
    categories=["T1", "T2", "T3", "T4", "T5"],
    width=90.0,
    height=38.0,
    smooth=True,
    axis_text_style=Styles.Muted.patch(text_size=10.0),
    grid_style=Styles.MutedThin,
    title="カナリアロールアウト処理性能推移 (60% 描画フレーム)",
    title_style=Styles.DarkBold.patch(text_size=11.5),
)
trend.add_series("従来版", [40, 42, 41, 43, 42], style=Styles.SecondaryNeutral, line_style="dashed")
# T1..T5 の全体軸スケールを固定したまま、左から60%地点まで曲線をスイープ描画
focal = trend.add_series("v0.3.7 カナリア", [42, 58, 74, 89, 96], style=Styles.PrimaryFlat, draw_ratio=0.6)

trend.configure_y_axis(min_value=0, max_value=100, tick_step=25, unit="k rps")
trend.draw(xy=(10.0, 10.0))
save()

各チャートの軸設定・凡例・カラーバー・コンストラクタ引数の詳細は、uv run drawlib rules show lib-charts-overview(または lib-charts-bar、lib-charts-box、lib-charts-line、lib-charts-pie、lib-charts-gantt、lib-charts-heatmap)で確認できます。

9. クラウド・システムアーキテクチャ構成図

分散システムの設計図は、信頼境界・ネットワーク階層・サービス間の依存関係を正確に伝える必要があります。drawlib.diagrams.architecture は、階層型の境界コンテナ NodeGroup・33種のクラウド/技術アイコン列挙型(GcpIcon、AwsIcon、AzureIcon、PhosphorIcon など)・境界認識エッジルーティング・交差のない junction() バス分岐を純粋な Python コードとして統合します。

09_diagrams_cloud_1
図 9.1: 外部クライアント・API Gateway・Junction 分岐・データ層を備えた本番クラウド VPC 構成図
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import (
    ArchitectureDiagram,
    GcpIcon,
    Node,
    NodeGroup,
    PhosphorIcon,
)
from drawlib.styles import Styles

setup(width=160, height=74)

d = ArchitectureDiagram(
    node_style=Styles.PrimaryBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.White,
)

# 外部アクターは必ずクラウドリージョン/VPC 境界の「外側」に配置する
client = d.add(
    Node(
        "Web・モバイル\nクライアント",
        width=25.0,
        height=18.5,
        icon=PhosphorIcon.DEVICES,
        icon_size=6.0,
        card_style=Styles.Neutral.patch(shape_r=2.0),
    ),
    xy=(14.5, 30.0),
)

# クラウドリージョン/VPC 境界(図全体座標系で左下 (41, 2))
vpc = d.add(
    NodeGroup(
        title="Google Cloud リージョン — 本番 VPC (us-central1)",
        width=109.0,
        height=62.0,
        style=Styles.PrimaryNeutral.patch(shape_r=2.4),
        text_style=Styles.DarkBold.patch(text_size=11.0, halign="left"),
    ),
    xy=(41.0, 2.0),
)

# 注目の入口ノード API Gateway(vpc 左下を原点とするローカル中心座標)
gateway = vpc.add(
    Node(
        "API Gateway\n(Ingress 境界)",
        width=23.0,
        height=19.0,
        icon=PhosphorIcon.SHIELD_CHECK,
        icon_size=6.0,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.5),
        card_style=Styles.PrimaryFlat.patch(shape_r=2.0),
    ),
    xy=(16.5, 28.0),
)

# VPC 内のアプリケーション層(White カード + 公式 GcpIcon)
auth_svc = vpc.add(
    Node(
        "認証サービス\n(Cloud Run)",
        width=24.0,
        height=16.5,
        icon=GcpIcon.CLOUD_RUN,
        icon_size=6.2,
        card_style=Styles.White.patch(shape_r=1.8),
    ),
    xy=(52.5, 40.5),
)
order_svc = vpc.add(
    Node(
        "注文 API\n(Cloud Run)",
        width=24.0,
        height=16.5,
        icon=GcpIcon.CLOUD_RUN,
        icon_size=6.2,
        card_style=Styles.White.patch(shape_r=1.8),
    ),
    xy=(52.5, 15.5),
)

# ネストされたデータ層 NodeGroup(vpc 内のローカル左下座標)
data_tier = vpc.add(
    NodeGroup(
        title="データ層 (Private)",
        width=34.0,
        height=52.5,
        style=Styles.SecondaryNeutral.patch(shape_r=2.0),
        text_style=Styles.DarkBold.patch(text_size=10.5, halign="left"),
    ),
    xy=(71.0, 3.5),
)
sql_db = data_tier.add(
    Node(
        "Cloud SQL\n(メイン DB)",
        width=26.0,
        height=16.5,
        icon=GcpIcon.CLOUD_SQL,
        icon_size=6.2,
        card_style=Styles.White.patch(shape_r=1.8),
    ),
    xy=(17.0, 37.0),
)
cache_db = data_tier.add(
    Node(
        "Memorystore\n(Redis キャッシュ)",
        width=26.0,
        height=16.5,
        icon=GcpIcon.MEMORYSTORE,
        icon_size=6.2,
        card_style=Styles.White.patch(shape_r=1.8),
    ),
    xy=(17.0, 12.0),
)

# 外部 HTTPS 接続 + Junction による整然としたバス分岐
d.connect(
    client,
    gateway,
    label="HTTPS",
    routing="direct",
    padding=1.0,
    text_style=Styles.DarkBold.patch(text_size=10.0, xy_shift=(-2.0, 2.6)),
)

j_mid = d.junction((75.0, 30.0))
j_top = d.junction((75.0, 42.5))
j_bot = d.junction((75.0, 17.5))
d.connect(gateway, j_mid, arrow="-", routing="direct", padding=(1.0, 0.0))
d.connect(j_mid, j_top, arrow="-", routing="direct")
d.connect(j_mid, j_bot, arrow="-", routing="direct")
d.connect(j_top, auth_svc, arrow="->", routing="direct", padding=(0.0, 1.0))
d.connect(j_bot, order_svc, arrow="->", routing="direct", padding=(0.0, 1.0))

d.connect(auth_svc, sql_db, arrow="->", routing="direct", padding=1.0)
d.connect(order_svc, cache_db, arrow="<->", routing="direct", padding=1.0)

d.draw(xy=(4.0, 4.0))
save()

9.1 コンテナ既定スタイル・Node・ローカル/グローバル座標系

ArchitectureDiagram は図全体のルートコンテナであり、3段階のスタイル継承(個別要素の上書き -> 図全体の既定値 -> 組み込み初期値)を提供します。コンストラクタの4つの必須キーワード引数(node_style、node_text_style、edge_style、edge_text_style)とオプションの node_card_style を定義しておけば、多数のマイクロサービス間で文字サイズや線幅を統一できます。

d.add(..., xy=...) と group.add(..., xy=...) では、対象の型と親コンテナによって xy の意味(中心か左下か・グローバルかローカルか)が明確に定められています。

要素登録メソッド xy の基準点(アンカー) 座標空間の原点 図全体のグローバル座標への換算式
d.add(Node(...), xy=(cx, cy)) 中央 Center (cx, cy) 図全体のグローバル座標 (cx, cy)
d.add(NodeGroup(...), xy=(x0, y0)) 左下 Bottom-Left (x0, y0) 図全体のグローバル座標 左下が (x0, y0)
group.add(Node(...), xy=(lx, ly)) 中央 Center (lx, ly) group 左下を (0, 0) とする ローカル座標 (x0 + lx, y0 + ly)
group.add(NodeGroup(...), xy=(lx0, ly0)) 左下 Bottom-Left (lx0, ly0) group 左下を (0, 0) とする ローカル座標 左下が (x0 + lx0, y0 + ly0)
d.junction((jx, jy)) 正確な1点 (jx, jy) 図全体のグローバル座標 (jx, jy)

9.1.1 公式クラウドアイコンとカード背景コントラストの設計原則

9.2 NodeGroup による境界設計と外部アクター配置ルール

NodeGroup はクラウドリージョン・VPC・アベイラビリティゾーン・Kubernetes クラスタ・サブネット・セキュリティゾーンといった階層的な境界を表現します。width=None, height=None のまま padding: float = 5.0 で子要素を自動包囲させることも、width と height を明示指定して複数ティアの枠サイズを美しく揃えることも可能です。

アーキテクチャ境界の鉄則 — 外部アクターは必ずクラウド NodeGroup の外側に置く: エンドユーザー・ブラウザ・モバイル端末・外部 Webhook 送信元を、クラウドリージョンや VPC の NodeGroup の 内側 に配置してはいけません。外部アクターは必ず d.add(Node(...), xy=...) で外側のグローバル領域に配置し、CDN・ロードバランサ・API Gateway などの入口ノードを経由して境界線を跨がせます。

9.3 エッジルーティング・双方向矢印・junction() バス分岐

d.connect(start, end, ...)(および node.connect(...) / Edge(...))は、Node・NodeGroup・Junction の外周境界で自動的にクリッピングされた接続線を描画します。

09_diagrams_cloud_2
図 9.2: エッジ接続パターン比較 — Direct・Orthogonal・Curved と Junction バス分岐
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import (
    ArchitectureDiagram,
    Node,
    NodeGroup,
    PhosphorIcon,
)
from drawlib.styles import Styles

setup(width=160, height=70)

d = ArchitectureDiagram(
    node_style=Styles.PrimaryBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.2),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.White,
)

# 左パネル:3種のエッジルーティング比較
left = d.add(
    NodeGroup(
        title="3種のルーティング (direct / orthogonal / curved)",
        width=72.0,
        height=58.0,
        style=Styles.Neutral.patch(shape_r=2.2),
        text_style=Styles.DarkBold.patch(text_size=10.8, halign="left"),
    ),
    xy=(2.0, 2.0),
)
a1 = left.add(Node("Edge LB", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(13.5, 43.0))
b1 = left.add(Node("認証 Pod", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(58.5, 43.0))
d.connect(
    a1,
    b1,
    label='direct ("<->")',
    arrow="<->",
    routing="direct",
    padding=1.0,
    text_style=Styles.DarkBold.patch(text_size=10.0, text_bg_fill_color=None, xy_shift=(0.0, 2.8)),
)

a2 = left.add(Node("API Pod", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(13.5, 28.5))
b2 = left.add(Node("SQL 参照", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(58.5, 22.5))
d.connect(
    a2,
    b2,
    label="orthogonal",
    arrow="->",
    routing="orthogonal",
    padding=1.0,
    text_style=Styles.DarkBold.patch(text_size=10.0, text_bg_fill_color=None, xy_shift=(6.5, 5.8)),
)

a3 = left.add(Node("送信元", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(13.5, 10.5))
b3 = left.add(Node("購読ワーカー", width=19.0, height=10.5, card_style=Styles.White.patch(shape_r=1.6)), xy=(58.5, 10.5))
e_curved = d.connect(
    a3,
    b3,
    label="curved (非同期)",
    arrow="->",
    routing="curved",
    style=Styles.SecondaryBold.patch(line_style="dashed"),
    padding=1.0,
    text_style=Styles.DarkBold.patch(text_size=10.0, text_bg_fill_color=None, xy_shift=(2.5, 5.2)),
)
e_curved.via((28.0, 10.4), (33.0, 8.6), (38.0, 7.8), (43.0, 8.6), (48.0, 10.4))

# 右パネル:Junction による1対3のバス分岐
right = d.add(
    NodeGroup(
        title="junction() による交差ゼロのバス分岐",
        width=72.0,
        height=58.0,
        style=Styles.PrimaryNeutral.patch(shape_r=2.2),
        text_style=Styles.DarkBold.patch(text_size=10.8, halign="left"),
    ),
    xy=(78.0, 2.0),
)
router = right.add(
    Node(
        "イベント\nルーター",
        width=21.0,
        height=17.0,
        icon=PhosphorIcon.GIT_BRANCH,
        icon_size=5.6,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.5),
        card_style=Styles.PrimaryFlat.patch(shape_r=1.8),
    ),
    xy=(14.5, 26.5),
)
w1 = right.add(
    Node("課金ワーカー", width=25.0, height=11.5, card_style=Styles.White.patch(shape_r=1.6)),
    xy=(55.0, 42.5),
)
w2 = right.add(
    Node("検索インデクサ", width=25.0, height=11.5, card_style=Styles.White.patch(shape_r=1.6)),
    xy=(55.0, 26.5),
)
w3 = right.add(
    Node("監査アーカイブ", width=25.0, height=11.5, card_style=Styles.SecondaryNeutral.patch(shape_r=1.6)),
    xy=(55.0, 10.5),
)

jb_mid = d.junction((112.0, 28.5))
jb_top = d.junction((112.0, 44.5))
jb_bot = d.junction((112.0, 12.5))
d.connect(router, jb_mid, arrow="-", routing="direct", padding=(1.0, 0.0))
d.connect(jb_mid, jb_top, arrow="-", routing="direct")
d.connect(jb_mid, jb_bot, arrow="-", routing="direct")
d.connect(jb_top, w1, arrow="->", routing="direct", padding=(0.0, 1.0))
d.connect(jb_mid, w2, arrow="->", routing="direct", padding=(0.0, 1.0))
d.connect(jb_bot, w3, arrow="->", routing="direct", padding=(0.0, 1.0))

d.draw(xy=(4.0, 4.0))
save()

定型的な1対Nの直交バス分岐や既存エッジ途中からの分岐(Dead Letter Queue への退避など)には、node.fork(targets, at_x=...) や edge.add_point((x, y)) を使うことで1メソッドで簡潔に記述できます。

from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import ArchitectureDiagram, GcpIcon, Node, PhosphorIcon
from drawlib.styles import Styles

setup(width=124, height=54)

d = ArchitectureDiagram(
    node_style=Styles.PrimaryBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.2),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
)

lb = d.add(
    Node(
        "Ingress LB",
        width=24.0,
        height=16.0,
        icon=PhosphorIcon.SHIELD_CHECK,
        icon_size=5.8,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.5),
        card_style=Styles.PrimaryFlat.patch(shape_r=1.8),
    ),
    xy=(20.0, 25.0),
)
pod_a = d.add(Node("ワーカー A", width=24.0, height=15.0, icon=GcpIcon.CLOUD_RUN, icon_size=5.8), xy=(84.0, 36.0))
pod_b = d.add(Node("ワーカー B", width=24.0, height=15.0, icon=GcpIcon.CLOUD_RUN, icon_size=5.8), xy=(84.0, 14.0))

# x=52.0 に共有 Junction を自動生成し、上下2つの Pod へ整然と分岐配線
lb.fork([pod_a, pod_b], at_x=52.0, padding=1.2)

d.draw(xy=(6.0, 2.0))
save()

全33種のアイコン列挙型やウェイポイント制御の詳細は、uv run drawlib rules show lib-diagrams-architecture および uv run drawlib rules show lib-icons-overview で確認できます。

10. ソフトウェア設計図:シーケンス・フロー・状態遷移・クラス・ER図

第 9 章で扱ったクラウド構成図(ArchitectureDiagram)に加え、drawlib.diagrams にはソフトウェア詳細設計向けの 5 つの専用エンジン(SequenceDiagram・FlowDiagram・StateDiagram・ClassDiagram・ERDiagram)が用意されており、UML 2.x・ISO 5807(JIS X 0121)・Crow's Foot(IE 記法)に準拠した設計図を宣言的な Python コードから描画できます。

10_diagrams_software_1
図 10.1: ParticipantGroup・実行区間・alt/else フレームを用いた OAuth 2.0 トークン検証シーケンス
Source Code
import math
from drawlib.canvas import save, setup
from drawlib.diagrams.sequence import (
    GcpIcon,
    Participant,
    ParticipantGroup,
    PhosphorIcon,
    SequenceDiagram,
)
from drawlib.styles import Styles

d = SequenceDiagram(
    node_style=Styles.DarkBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
    autonumber=True,
    col_width=36.0,
    step_y=5.6,
    margin=4.0,
)

client = d.add(
    Participant(
        "クライアント",
        width=24.0,
        height=12.5,
        icon=PhosphorIcon.DEVICES,
        icon_size=5.0,
        card_style=Styles.Neutral,
    )
)

platform = d.add(
    ParticipantGroup(
        title="認証・API 基盤",
        padding=4.0,
        style=Styles.PrimaryNeutral.patch(shape_r=2.0),
        text_style=Styles.DarkBold.patch(text_size=10.5, halign="left", valign="bottom"),
    )
)
gateway = platform.add(
    Participant(
        "API ゲートウェイ",
        width=26.0,
        height=12.5,
        icon=PhosphorIcon.CLOUD_CHECK,
        icon_size=5.0,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.5),
        card_style=Styles.PrimaryFlat,
    )
)
auth = platform.add(
    Participant(
        "認証サービス",
        width=26.0,
        height=12.5,
        icon=PhosphorIcon.SHIELD_CHECK,
        icon_size=5.0,
        card_style=Styles.White,
    )
)
store = platform.add(
    Participant(
        "セッション DB",
        width=26.0,
        height=12.5,
        icon=GcpIcon.MEMORYSTORE,
        icon_size=5.0,
        card_style=Styles.SecondaryNeutral,
    )
)

client.request(gateway, "POST /v1/orders (Bearer JWT)")
gateway.activate()
gateway.request(auth, "署名検証リクエスト")
auth.activate()
auth.request(store, "GET session:jti")
store.reply(auth, "有効クレーム返却")

with d.alt("") as alt_blk:
    alt_blk.style = Styles.MutedDashed
    alt_blk.text_style = Styles.DarkBold.patch(text_size=10.0)
    auth.reply(gateway, "200 検証済みクレーム", style=Styles.SuccessBold)
    gateway.request(store, "XADD 監査ログ送信", is_async=True, style=Styles.SecondaryBold)
    gateway.reply(client, "200 OK (注文受付)", style=Styles.SuccessBold)
    d.note("300 秒キャッシュ", over=[gateway, auth], style=Styles.PrimaryNeutral.patch(text_size=10.0))

d.space(2.0)
with d.else_("") as else_blk:
    else_blk.style = Styles.MutedDashed
    else_blk.text_style = Styles.DarkBold.patch(text_size=10.0)
    auth.reply(
        gateway,
        "401 無効トークン",
        style=Styles.DangerBold,
        text_style=Styles.DangerBold.patch(text_size=10.0),
    )
    gateway.reply(
        client,
        "401 Unauthorized",
        style=Styles.DangerBold,
        text_style=Styles.DangerBold.patch(text_size=10.0),
    )

auth.deactivate()
gateway.deactivate()

w, h = d.get_size()
setup(width=math.ceil(w) + 8, height=math.ceil(h) + 8)
d.draw(xy=(4.0, 4.0))
save()

10.1 5 つのソフトウェア設計図エンジンの概要

drawlib.diagrams の各エンジンは共通のビルダーライフサイクルを備えています。コンストラクタで基本スタイルを定義し、.add() で要素を登録して関係線を接続し、必要に応じて .get_size() で外寸を取得したうえで .draw(xy=(x0, y0))(既定は左下アンカー)でキャンバスへ描画します。

モジュール 主クラス 主な構成要素 座標指定方式 主な用途
drawlib.diagrams.sequence SequenceDiagram Participant, ParticipantGroup, Message, Note, Block col_width(X)と step_y(Y)で自動進行 RPC/REST 通信手順、OAuth 認可、非同期イベント連鎖
drawlib.diagrams.flow FlowDiagram Start, End, Process, Decision, Data, Lane, Junction 共通/スイムレーン空間上の中心 xy=(cx, cy) CI/CD ゲート判定、業務フロー、部門横断スイムレーン
drawlib.diagrams.state StateDiagram State, InitialState, FinalState, ChoiceState, ForkJoinState 中心 xy=(cx, cy) と曲線/直交遷移パス プロトコル状態機械、ワーカー寿命管理、並行状態図
drawlib.diagrams.class_diagram ClassDiagram ClassNode, AttributeInfo, MethodInfo, ClassRelationship 高さ自動計算の 3 区画ボックス+中心 xy=(cx, cy) OOP ドメインモデル、インターフェース実装、継承構造
drawlib.diagrams.er ERDiagram Entity, ColumnInfo, Relationship 中心 xy=(cx, cy) と行単位 start_column / end_column リレーショナル DB 設計、PK/FK 制約、Crow's Foot 図

10.2 SequenceDiagram による時系列 API シーケンス

SequenceDiagram(from drawlib.diagrams.sequence import SequenceDiagram, Participant, ParticipantGroup)は、時系列プロトコルにおける縦方向の座標計算を完全に自動化します。


10.3 業務フローと状態遷移:FlowDiagram と StateDiagram

処理分岐や部門間の連携には FlowDiagram、イベント駆動のライフサイクル管理には StateDiagram を使用します。

10_diagrams_software_2
図 10.2: ソフトウェア振る舞い設計の対比 — カナリアリリースの FlowDiagram(左)とワーカー状態遷移の StateDiagram(右)
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.flow import Data, Decision, End, FlowDiagram, Process, Start
from drawlib.diagrams.state import ChoiceState, FinalState, InitialState, State, StateDiagram
from drawlib.icons import phosphor
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=160, height=78)

# 左パネル: FlowDiagram
rectangle((41, 39), width=74, height=68, style=Styles.MutedDashed.patch(shape_r=2.2))
phosphor.git_branch((9.5, 68.0), width=4.2, style=Styles.DarkBold)
text(
    (13.5, 68.0),
    "FlowDiagram: カナリアリリース判定",
    style=Styles.DarkBold.patch(text_size=11.0, halign="left"),
)

flow = FlowDiagram(
    node_style=Styles.Neutral,
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
)
f_start = flow.add(
    Start("PR マージ", width=23, height=9.0, style=Styles.Neutral, text_style=Styles.DarkBold.patch(text_size=10.0)),
    xy=(22, 54),
)
f_canary = flow.add(
    Process(
        "カナリア配備",
        width=26,
        height=10.0,
        style=Styles.PrimaryFlat,
        text_style=Styles.WhiteBold.patch(text_size=10.2),
    ),
    xy=(58, 54),
)
f_check = flow.add(
    Decision(
        "SLO 正常?",
        width=25,
        height=13.0,
        style=Styles.PrimaryNeutral,
        text_style=Styles.DarkBold.patch(text_size=10.0),
    ),
    xy=(58, 35),
)
f_alert = flow.add(
    Data(
        "自動ロールバック",
        width=26,
        height=10.0,
        style=Styles.DangerNeutral,
        text_style=Styles.DarkBold.patch(text_size=10.0),
    ),
    xy=(22, 35),
)
f_prod = flow.add(
    End(
        "本番 100% 展開",
        width=26,
        height=9.5,
        style=Styles.SecondaryNeutral,
        text_style=Styles.DarkBold.patch(text_size=10.0),
    ),
    xy=(58, 14),
)

f_start.connect(f_canary, padding=0.6)
f_canary.connect(f_check, start_side="bottom", end_side="top", padding=0.6)
f_check.connect(
    f_alert,
    label="No",
    start_side="left",
    end_side="right",
    style=Styles.DangerBold,
    text_style=Styles.DangerBold.patch(text_size=10.0),
    padding=0.6,
)
f_check.connect(f_prod, label="Yes", start_side="bottom", end_side="top", padding=0.6)
flow.draw()

# 右パネル: StateDiagram
rectangle((119, 39), width=74, height=68, style=Styles.MutedDashed.patch(shape_r=2.2))
phosphor.arrows_clockwise((87.5, 68.0), width=4.2, style=Styles.DarkBold)
text(
    (91.5, 68.0),
    "StateDiagram: ワーカー状態遷移",
    style=Styles.DarkBold.patch(text_size=11.0, halign="left"),
)

sd = StateDiagram(
    node_style=Styles.Neutral.patch(text_size=10.0),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
)
s_init = sd.add(InitialState(), xy=(89, 53))
s_warm = sd.add(
    State("初期化中", entry="loadModel()", width=28, style=Styles.Neutral.patch(text_size=10.0)),
    xy=(114, 53),
)
s_active = sd.add(
    State(
        "稼働中",
        do="serveGrpc()",
        exit="drainPool()",
        width=28,
        style=Styles.PrimaryNeutral.patch(text_size=10.0),
    ),
    xy=(114, 29),
)
s_choice = sd.add(ChoiceState(), xy=(146, 29))
s_final = sd.add(FinalState(), xy=(114, 11))

sd.connect(s_init, s_warm, start_side="right", end_side="left", routing="direct", padding=0.8)
sd.connect(
    s_warm,
    s_active,
    label="  ready",
    start_side="bottom",
    end_side="top",
    routing="direct",
    text_style=Styles.Dark.patch(text_size=10.0, halign="left"),
    padding=0.8,
)
sd.connect(
    s_active,
    s_choice,
    event="err",
    start_side="right",
    end_side="left",
    routing="direct",
    style=Styles.WarningBold,
    padding=0.8,
)
sd.connect(
    s_choice,
    s_warm,
    label="  [retry]",
    start_side="top",
    end_side="right",
    routing="orthogonal",
    text_style=Styles.Dark.patch(text_size=10.0, halign="left"),
    padding=0.8,
)
sd.connect(
    s_choice,
    s_final,
    label="  [fatal]",
    start_side="bottom",
    end_side="right",
    routing="orthogonal",
    style=Styles.DangerBold,
    text_style=Styles.DangerBold.patch(text_size=10.0, halign="left"),
    padding=0.8,
)
sd.connect(
    s_active,
    s_final,
    label="  stop",
    start_side="bottom",
    end_side="top",
    routing="direct",
    text_style=Styles.Dark.patch(text_size=10.0, halign="left"),
    padding=0.8,
)
sd.draw()

save()

10.4 ClassDiagram によるオブジェクト指向クラス設計

ClassDiagram(drawlib.diagrams.class_diagram)は、ステレオタイプ(stereotype="interface" や is_abstract=True)・属性区画・メソッド区画から成る UML 2.0 の 3 区画クラスボックス(ClassNode)を行数に応じた自動高さで描画します。

from drawlib.canvas import save, setup
from drawlib.diagrams.class_diagram import ClassDiagram, ClassNode
from drawlib.fonts import FontRoboto, FontSourceCode
from drawlib.styles import Styles

setup(width=140, height=64)

cd = ClassDiagram(
    node_style=Styles.Neutral.patch(text_font=FontSourceCode.ROBOTO_MONO, text_size=10.0),
    header_style=Styles.PrimaryNeutral.patch(text_font=FontRoboto.ROBOTO_BOLD, text_size=10.8),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
)

repo = cd.add(
    ClassNode("PaymentGateway", stereotype="interface", width=44, header_style=Styles.SecondaryNeutral)
    .add_method("authorize", "amount: Decimal", "AuthReceipt"),
    xy=(32, 42),
)
stripe_gw = cd.add(
    ClassNode("StripeGateway", width=44)
    .add_attribute("api_client", "HttpClient", visibility="-")
    .add_method("authorize", "amount: Decimal", "AuthReceipt"),
    xy=(32, 16),
)
invoice = cd.add(
    ClassNode(
        "Invoice",
        width=40,
        header_style=Styles.PrimaryFlat.patch(
            text_color=Styles.WhiteBold.text_color,
            text_font=FontRoboto.ROBOTO_BOLD,
            text_size=10.8,
        ),
    )
    .add_attribute("id", "UUID", visibility="+")
    .add_attribute("status", "InvoiceState", visibility="#")
    .add_method("capture", "gw: PaymentGateway", "bool"),
    xy=(106, 29),
)

cd.connect(stripe_gw, repo, "realization", start_side="top", end_side="bottom")
cd.connect(invoice, repo, "dependency", label="<<uses>>", start_side="left", end_side="right")

cd.draw(xy=(4.0, 4.0))
save("class_model.png")

10.5 ERDiagram によるリレーショナルデータベース設計

ERDiagram(drawlib.diagrams.er)は Information Engineering(Crow's Foot / 鳥の足記法)による ER 図エンジンであり、各テーブル(Entity)のカラムを「PK / FK バッジ列」「カラム名列」「SQL 型+(NN) 非 NULL 制約列」の 3 サブカラムに自動整列します。

from drawlib.canvas import save, setup
from drawlib.diagrams.er import Entity, ERDiagram
from drawlib.fonts import FontRoboto, FontSourceCode
from drawlib.styles import Styles

setup(width=136, height=52)

erd = ERDiagram(
    node_style=Styles.Neutral.patch(text_font=FontSourceCode.ROBOTO_MONO, text_size=10.0),
    header_style=Styles.PrimaryNeutral.patch(text_font=FontRoboto.ROBOTO_BOLD, text_size=10.8),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
)

tenants = erd.add(
    Entity("tenants", width=40)
    .add_column("id", "UUID", pk=True, nullable=False)
    .add_column("slug", "VARCHAR(64)", nullable=False)
    .add_column("created_at", "TIMESTAMPTZ", nullable=False),
    xy=(30, 26),
)
api_keys = erd.add(
    Entity(
        "api_keys",
        width=42,
        header_style=Styles.PrimaryFlat.patch(
            text_color=Styles.WhiteBold.text_color,
            text_font=FontRoboto.ROBOTO_BOLD,
            text_size=10.8,
        ),
    )
    .add_column("id", "UUID", pk=True, nullable=False)
    .add_column("tenant_id", "UUID", fk=True, nullable=False)
    .add_column("key_hash", "CHAR(64)", nullable=False)
    .add_column("revoked_at", "TIMESTAMPTZ", nullable=True),
    xy=(102, 26),
)

tenants.connect(
    api_keys,
    cardinality="1:*",
    start_column="id",
    end_column="tenant_id",
    start_side="right",
    end_side="left",
)

erd.draw(xy=(4.0, 4.0))
save("er_schema.png")

11. 自動グラフレイアウトソルバー(drawlib.graph)

ノード数が増えるにつれて (x, y) 座標を手作業で管理するのは煩雑になりますが、ブラックボックス型の自動描画ツールではアイコンやフォント、デザインシステム(Styles)の細かな制御が失われます。drawlib.graph は純粋かつ決定論的な座標計算ソルバーとしてこの課題を解決します。ノード・クラスタ・エッジの接続関係だけを宣言して layout = g.get_layout(...) で座標を算出し、Drawlib の任意の描画コンポーネントへ渡すことで「自動配置」と「完全な表現力」を両立します。

11_auto_layout_graphs_1
図 11.1: LayerGraph.get_layout() で座標解決しスタイル付きカードへ描画したデータ基盤 DAG パイプライン
Source Code
from drawlib.canvas import save, setup
from drawlib.graph import LayerGraph
from drawlib.icons import phosphor
from drawlib.lines import lines
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=78)

# ステップ 1: DAG トポロジーを宣言し決定論的な座標を算出
g = LayerGraph(direction="LR", default_node_width=34.0, default_node_height=14.5)
g.node("ingest", layer=0)
g.tier("processing", ["transform", "validate"], layer=1)
g.cluster("etl_stage", ["transform", "validate"], padding=5.0, header_height=4.5)
g.tier("serving", ["feature", "warehouse"], layer=2)

g.edge("ingest", "transform")
g.edge("ingest", "validate")
g.edge("transform", "feature")
g.edge("transform", "warehouse")
g.edge("validate", "warehouse")

layout = g.get_layout(margin=13.0)

# ステップ 2: 算出されたクラスタ境界・直交エッジ・ノードカードを描画
stage = layout["etl_stage"]
rectangle(stage.xy, width=stage.width, height=stage.height, style=Styles.MutedDashed.patch(shape_r=2.2))
text(
    (stage.left + 2.5, stage.top - 2.8),
    "ストリーム処理層",
    style=Styles.DarkBold.patch(halign="left", text_size=10.2),
)

for edge in layout.edges:
    lines(edge.points, arrow_head="->", style=Styles.DarkBold)

cards = [
    ("ingest", "イベント収集", "Kafka / PubSub", Styles.Neutral, Styles.DarkBold, Styles.Dark, phosphor.cloud_arrow_up),
    ("transform", "ストリーム変換", "Stateful Flink", Styles.PrimaryFlat, Styles.WhiteBold, Styles.White, phosphor.lightning),
    ("validate", "スキーマ検証", "品質ゲート", Styles.PrimaryNeutral, Styles.DarkBold, Styles.Dark, phosphor.shield_check),
    ("feature", "特徴量ストア", "低遅延 KV 配信", Styles.SecondaryNeutral, Styles.DarkBold, Styles.Dark, phosphor.cpu),
    ("warehouse", "分析用 DWH", "列指向 SQL", Styles.Neutral, Styles.DarkBold, Styles.Dark, phosphor.database),
]

for nid, title, sub, card_st, title_st, sub_st, icon_fn in cards:
    n = layout[nid]
    rectangle(n.xy, width=n.width, height=n.height, style=card_st.patch(shape_r=1.8))
    icon_fn((n.x - 11.5, n.y), width=4.8, style=title_st)
    text((n.x + 2.2, n.y + 2.4), title, style=title_st.patch(text_size=10.5))
    text((n.x + 2.2, n.y - 2.8), sub, style=sub_st.patch(text_size=10.0))

save()

11.1 2 段階レイアウト設計と座標取得(get_layout)

drawlib.graph はそれ自体には .draw() メソッドや色・フォント属性を持たず、「幾何計算」と「視覚レンダリング」を明確な 2 段階に分離します。

  1. トポロジー宣言と座標解決(layout = g.get_layout(...)): ノード(g.node("id", width=..., height=...))、有向エッジ(g.edge("src", "dst"))、およびグループ境界(g.cluster("id", nodes=[...], padding=..., header_height=...))を登録し、g.get_layout(width=..., height=..., margin=10.0) を呼び出して GraphLayout オブジェクト(layout["id"] で辞書参照可能)を取得します。
  2. 自由なビジュアル描画: 解決済みの座標を ArchitectureDiagram・FlowDiagram・SmartArts・基本図形(shapes / icons / lines)へ渡して描画します。
    • 中心座標(.xy と .local_xy): NodeLayout と ClusterLayout はグローバル中心 .xy((cx, cy))を公開しており、rectangle()・circle()・FlowDiagram・トップレベルの ArchitectureDiagram ノードへそのまま渡せます。また親クラスタ左下基準の相対中心 .local_xy は NodeGroup 内の子ノード配置に対応します。
    • 左下コーナー座標(.bottom_left と .local_bottom_left): 左下アンカーを採用するトップレベル NodeGroup(..., xy=cl.bottom_left)、入れ子の parent_grp.add(NodeGroup(...), xy=cl.local_bottom_left)、SmartArts、Charts へ直接渡せます。
    • エッジ経路ポリライン(edge.points, edge.src_port, edge.dst_port, edge.mid_xy): layout.edges の各 EdgeLayout はノード境界にスナップ済みのポート座標と直交ウェイポイント列を保持し、lines(edge.points, arrow_head="->") で即座に結線できます。
    • 座標の出力固定と微調整(print(layout) & layout.offset()): 自動配置をベースに手動調整コードへ移行したい場合は、print(layout)(str(layout))を実行すると全クラスタ・ノードの .xy / .bottom_left・エッジ経路が一覧出力され、明示的な Python 作図コードの雛形として転記できます。また layout.offset("node_id", dx=0.0, dy=-5.0) を使えば、接続エッジのポート座標を自動追従させたまま特定ノードだけを平行移動できます。

11.2 5 種類のグラフレイアウトソルバーの使い分け

drawlib.graph には BaseGraph を継承した 5 つの専用ソルバーが用意されており、システムの構造特性に応じて最適なアルゴリズムを選択できます。

ソルバークラス レイアウトアルゴリズム 主な構築ヘルパー 最適なアーキテクチャ用途
ArchitectureGraph 2 階層マクロ/ミクロ箱詰め+5 方位コンパス配置 g.group(), g.cluster(..., parent=..., pos="left"|"center"|..., order=...) マルチゾーン VPC、入れ子サブネット、クラウド構成図(ArchitectureDiagram 連携)
LayerGraph Sugiyama 階層型 DAG(閉路除去・最長経路ランク・交差最小化) g.tier(name, nodes, layer=...), g.cluster(), direction="LR"|"TB" ETL/データ系譜パイプライン、CI/CD ステージ、依存関係 DAG
TreeGraph Reingold-Tilford / Buchheim 対称コンパクト木配置 g.child(parent, child_id), root=..., direction="TB"|"LR" 組織図、構文木(AST)、障害木、厳密な単一親ツリー
RadialGraph 同心円 BFS リング+部分木重み付き角度配分 g.spoke(parent, spoke_id, ring=...), hub=..., layout.ring_radii イベントメッシュ、Pub/Sub ハブ&スポーク、API エコシステム図
GridGraph 2 次元 (row, col) マトリクス配置+チャネル回廊配線 g.cell(id, row=..., col=...), g.cluster_row(), g.cluster_column(), edge_routing="smart" サービスカタログ、状態マトリクス、均等グリッド構成図

11.3 階層ツリー(TreeGraph)と放射状トポロジー(RadialGraph)

直線的なパイプライン以外の階層構造やブローカー中心のネットワークでは、TreeGraph と RadialGraph が三角関数や部分木幅のバランス計算を自動化します。

11_auto_layout_graphs_2
図 11.2: 非線形トポロジーの自動配置 — 対称階層 TreeGraph(左)と同心円 RadialGraph イベントメッシュ(右)
Source Code
from drawlib.canvas import save, setup
from drawlib.graph import RadialGraph, TreeGraph
from drawlib.icons import phosphor
from drawlib.lines import line, lines
from drawlib.shapes import circle, rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=160, height=78)

# 左パネル: TreeGraph(階層タクソノミー)
rectangle((41, 39), width=74, height=68, style=Styles.MutedDashed.patch(shape_r=2.2))
phosphor.tree_structure((9.5, 68.0), width=4.2, style=Styles.DarkBold)
text(
    (13.5, 68.0),
    "TreeGraph: 基盤階層ツリー",
    style=Styles.DarkBold.patch(text_size=11.0, halign="left"),
)

tg = TreeGraph(root="root", direction="TB", sibling_sep=3.2, default_node_width=16.5, default_node_height=9.0)
tg.node("root", width=22.5, height=9.5)
tg.child("root", "compute", width=19.5, height=9.0)
tg.child("root", "data", width=19.5, height=9.0)
tg.child("compute", "k8s", width=16.5, height=9.0)
tg.child("compute", "jobs", width=16.5, height=9.0)
tg.child("data", "sql", width=16.5, height=9.0)

t_layout = tg.get_layout(width=60.0, height=52.0, margin=4.0)
ox1, oy1 = 11.0, 7.0

for e in t_layout.edges:
    pts = [(px + ox1, py + oy1) for px, py in e.points]
    lines(pts, arrow_head="->", style=Styles.DarkBold)

tree_nodes = [
    ("root", "クラウド基盤", Styles.PrimaryFlat, Styles.WhiteBold),
    ("compute", "計算リソース層", Styles.PrimaryNeutral, Styles.DarkBold),
    ("data", "データ永続層", Styles.SecondaryNeutral, Styles.DarkBold),
    ("k8s", "GKE クラスタ", Styles.Neutral, Styles.DarkBold),
    ("jobs", "バッチ実行", Styles.Neutral, Styles.DarkBold),
    ("sql", "Cloud SQL", Styles.Neutral, Styles.DarkBold),
]
for nid, label, st, txt_st in tree_nodes:
    n = t_layout[nid]
    rectangle(
        (n.x + ox1, n.y + oy1),
        width=n.width,
        height=n.height,
        style=st.patch(shape_r=1.6),
        text=label,
        text_style=txt_st.patch(text_size=10.0),
    )

# 右パネル: RadialGraph(ハブ&スポーク)
rectangle((119, 39), width=74, height=68, style=Styles.MutedDashed.patch(shape_r=2.2))
phosphor.share_network((87.5, 68.0), width=4.2, style=Styles.DarkBold)
text(
    (91.5, 68.0),
    "RadialGraph: 放射状メッシュ",
    style=Styles.DarkBold.patch(text_size=11.0, halign="left"),
)

rg = RadialGraph(
    hub="hub",
    center=(35.0, 26.5),
    radius_step=22.0,
    start_angle=18.0,
    default_node_width=17.5,
    default_node_height=8.5,
)
rg.node("hub", width=15.0, height=15.0)
for sid in ["auth", "orders", "billing", "notify", "search"]:
    rg.spoke("hub", sid)

r_layout = rg.get_layout(width=70.0, height=54.0, margin=4.0)
ox2, oy2 = 84.0, 7.0
hub_n = r_layout["hub"]
hub_xy = (hub_n.x + ox2, hub_n.y + oy2)

for r in r_layout.ring_radii.values():
    circle(hub_xy, radius=r, style=Styles.MutedDashed)

for e in r_layout.edges:
    sp = (e.src_port[0] + ox2, e.src_port[1] + oy2)
    dp = (e.dst_port[0] + ox2, e.dst_port[1] + oy2)
    line(sp, dp, arrow_head="->", style=Styles.DarkBold)

circle(
    hub_xy,
    radius=hub_n.width / 2,
    style=Styles.PrimaryFlat,
    text="イベント\nハブ",
    text_style=Styles.WhiteBold.patch(text_size=10.0),
)

spokes = [
    ("auth", "認証 API", Styles.PrimaryNeutral),
    ("orders", "注文管理", Styles.Neutral),
    ("billing", "課金決済", Styles.SecondaryNeutral),
    ("notify", "通知配信", Styles.Neutral),
    ("search", "検索基盤", Styles.PrimaryNeutral),
]
for sid, label, st in spokes:
    n = r_layout[sid]
    rectangle(
        (n.x + ox2, n.y + oy2),
        width=n.width,
        height=n.height,
        style=st.patch(shape_r=1.6),
        text=label,
        text_style=Styles.DarkBold.patch(text_size=10.0),
    )

save()

11.4 クラスタ境界(ArchitectureGraph)とマトリクス配置(GridGraph)

複数ゾーンに跨がるクラウド境界図や整然としたマトリクス構成図では、ArchitectureGraph と GridGraph がノード座標と同時にコンテナの外枠サイズも自動計算します。

from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import ArchitectureDiagram, GcpIcon, Node, NodeGroup, PhosphorIcon
from drawlib.graph import ArchitectureGraph
from drawlib.styles import Styles

setup(width=168, height=78)

# ステップ 1: ArchitectureGraph で入れ子 VPC とサブネット座標を解決
g = ArchitectureGraph(direction="LR", container_sep=14.0, default_node_width=24.0, default_node_height=14.0)
g.cluster("edge", ["client"], pos="left", padding=5.0)
g.group("vpc", pos="center", padding=5.0)
g.cluster("app_tier", ["api", "worker"], parent="vpc", order=1, padding=5.0)
g.cluster("data_tier", ["db"], parent="vpc", order=2, padding=5.0)

g.edge("client", "api")
g.edge("api", "worker")
g.edge("worker", "db")

layout = g.get_layout(margin=8.0)

# ステップ 2: .bottom_left / .local_bottom_left / .local_xy を用いて描画
d = ArchitectureDiagram(
    node_style=Styles.DarkBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
)

edge_grp = d.add(NodeGroup(title="External", padding=5.0, style=Styles.MutedDashed), xy=layout["edge"].bottom_left)
vpc_grp = d.add(NodeGroup(title="Production VPC", padding=5.0, style=Styles.MutedDashed), xy=layout["vpc"].bottom_left)
app_grp = vpc_grp.add(NodeGroup(title="Compute", padding=5.0, style=Styles.PrimaryNeutral), xy=layout["app_tier"].local_bottom_left)
data_grp = vpc_grp.add(NodeGroup(title="Storage", padding=5.0, style=Styles.SecondaryNeutral), xy=layout["data_tier"].local_bottom_left)

client = edge_grp.add(Node("Client", width=24, height=14, icon=PhosphorIcon.DEVICES), xy=layout["client"].local_xy)
api = app_grp.add(
    Node(
        "API Gateway",
        width=24,
        height=14,
        icon=GcpIcon.CLOUD_API_GATEWAY,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.5),
        card_style=Styles.PrimaryFlat,
    ),
    xy=layout["api"].local_xy,
)
worker = app_grp.add(Node("Worker", width=24, height=14, icon=GcpIcon.CLOUD_RUN), xy=layout["worker"].local_xy)
db = data_grp.add(Node("Cloud SQL", width=24, height=14, icon=GcpIcon.CLOUD_SQL), xy=layout["db"].local_xy)

d.connect(client, api, label="HTTPS")
d.connect(api, worker)
d.connect(worker, db)

d.draw()
save("arch_graph_vpc.png")

12. マルチフレームアニメーションと 16:9 スライド作成

静的な構成図がシステムの断面を切り取るのに対し、時系列アニメーションとワイドスクリーン発表資料は「処理の流れ」と「ストーリー」を動的に伝えます。Drawlib は drawlib.anim により APNG(.png)・Animated WebP(.webp)・ディザノイズのない Animated GIF(.gif) を純粋な Python コードから直接出力でき、さらに drawlib.slide により Markdown 原稿から 16:9 Web スライド・ベクター PDF・編集可能な PowerPoint(.pptx) を一括生成します。

12_animations_and_slides_1
図 12.1: Animation と ChevronProcess によるプログレッシブ表示ループ APNG アニメーション
Source Code
from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.smartarts import ChevronProcess
from drawlib.styles import Styles
from drawlib.text import text

setup(width=156, height=52)
anim = Animation(fps=1.5, loop=0)

stages = [
    ("1. 原稿管理", "Git 統合"),
    ("2. 構文検証", "静的チェック"),
    ("3. 図面描画", "グリッド検証"),
    ("4. 文書変換", "HTML / PDF"),
    ("5. 公開配信", "Web ポータル"),
]

for step in range(len(stages)):
    is_last = step == len(stages) - 1
    with anim.frame(duration=2.5 if is_last else 0.75):
        rectangle((78, 26), width=146, height=42, style=Styles.MutedDashed.patch(shape_r=2.2))
        phosphor.play_circle((10.5, 41.0), width=4.4, style=Styles.DarkBold)
        text(
            (14.5, 41.0),
            "段階的リリースパイプライン(マルチフレーム APNG)",
            style=Styles.DarkBold.patch(text_size=10.8, halign="left"),
        )
        rectangle(
            (134.5, 41.0),
            width=25.0,
            height=6.2,
            style=Styles.PrimaryNeutral.patch(shape_r=1.5),
            text=f"フレーム {step + 1} / {len(stages)}",
            text_style=Styles.DarkBold.patch(text_size=10.0),
        )
        line((10, 20.5), (146, 20.5), style=Styles.MutedDashed)

        proc = ChevronProcess(
            style=Styles.Neutral,
            text_style=Styles.DarkBold.patch(text_size=10.5),
            description_style=Styles.Dark.patch(text_size=10.0),
            corner_angle=62.0,
            spacing=1.6,
            flat_left_end=True,
        )
        for i, (title, desc) in enumerate(stages):
            is_active = i == step
            c_style = Styles.PrimaryFlat if is_active else (Styles.PrimaryNeutral if i < step else Styles.Neutral)
            t_style = Styles.WhiteBold.patch(text_size=10.5) if is_active else Styles.DarkBold.patch(text_size=10.5)
            d_style = Styles.White.patch(text_size=10.0) if is_active else Styles.Dark.patch(text_size=10.0)
            proc.add(
                title,
                description=desc,
                style=c_style,
                text_style=t_style,
                description_style=d_style,
                show=(i <= step),
            )
        proc.draw(xy=(10.0, 10.0), width=136.0, height=21.0)

save()

12.1 drawlib.anim によるマルチフレームアニメーション

キャンバスライフサイクルと Animation API

drawlib.anim から Animation をインポートし、フレームループの前に 1 回だけ setup(width=..., height=...) を呼び出したうえで、各フレームの描画処理を with anim.frame(duration=..., clear=True): コンテキスト内に記述します。

出力形式 拡張子 エンコード特性・画質 主な活用先
APNG .png / .apng 可逆 24bit RGB + 8bit 透過。ブラウザで滑らかに再生され、PDF 等では自動的に鮮明な静止画として表示 HTML ドキュメント(site / doc)、スライド、印刷用 PDF
Animated WebP .webp 可逆 24bit RGB + 8bit 透過のまま APNG 比で約 50% 軽量(minimize_size=True) 大規模 Web ドキュメントサイト、モバイル Web アプリ
Animated GIF .gif v0.3.7 のディザなし 256 色グローバル Median Cut 量子化(Image.Dither.NONE)により粒状ノイズ皆無 GitHub README、Slack、Notion、Confluence、X/Twitter

プログレッシブ描画の定石(show=False・draw_ratio・補間関数)

Drawlib の全高レベルモジュールは、フレーム間でレイアウトがガタつかないアニメーション機構を備えています。

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.charts.bar import BarChart
from drawlib.styles import Styles

setup(width=110, height=64)
anim = Animation(fps=10.0, loop=0)

chart = BarChart(
    categories=["Q1", "Q2", "Q3", "Q4"],
    axis_line_style=Styles.MutedDashed,
    axis_text_style=Styles.Dark.patch(text_size=10.0),
    grid_style=Styles.MutedThin,
    width=86,
    height=44,
    title="Quarterly API Throughput (kRPS)",
    title_style=Styles.DarkBold.patch(text_size=12.5),
)
chart.configure_y_axis(min_value=0, max_value=120)
series = chart.add_series("Throughput", [42, 68, 94, 112], style=Styles.PrimaryFlat)

steps = 6
for i in range(1, steps + 1):
    t = i / steps
    eased = t * t * (3.0 - 2.0 * t)  # Smoothstep イージング (0 -> 1)
    series.draw_ratio = eased
    with anim.frame(duration=2.2 if i == steps else 0.12):
        chart.draw(xy=(12, 8))

save("throughput_growth.gif")

12.2 16:9 プレゼンテーションスライドと PowerPoint 出力(drawlib.slide)

uv run drawlib init project slide pitch -l en,ja -s google でスライドプロジェクトを初期化すると、pitch_src/<lang>/*.md の連番 Markdown ファイル群から 1920×1080(16:9)のマルチターゲット発表資料が生成されます。

::: block xy=(1700, 1010) width=140 height=30
```drawlib file=page.svg
from drawlib.slide import draw_page_number

draw_page_number()
```
:::

::: block xy=(80, 40) width=1760 height=60
# ゼロトラスト API ゲートウェイ展開計画
:::

::: block xy=(80, 140) width=720 height=820
## 主要マイルストーン

- **エッジ mTLS**: クライアント証明書による相互認証
- **JWT キャッシュ**: Redis によるミリ秒未満の検証
- **カナリア判定**: SLO 違反時の自動ロールバック

---

*現在の発表位置: スライド 3*
:::

::: block xy=(840, 140) width=1000 height=820
```drawlib file=gateway_stage_ja.svg
from drawlib.canvas import clear, save, setup
from drawlib.shapes import rectangle
from drawlib.slide import current_slide
from drawlib.styles import Styles

clear()
setup(width=100, height=82)
rectangle(
    (50, 41),
    width=76,
    height=34,
    style=Styles.PrimaryFlat.patch(shape_r=2.5),
    text=f"ゲートウェイ構成 ({current_slide.text})",
    text_style=Styles.WhiteBold.patch(text_size=14.5),
)
save()
```
:::

::: note
左カラムの 3 つの要点を順に説明したあと、右図で `current_slide` が HTML・PDF・`.pptx` ビルド時に自動でページ番号を解決する仕組みを補足します。
:::
12_animations_and_slides_2
図 12.2: 16:9 スライドデッキのコンパイル構造 — Markdown 原稿・ノートから HTML・ベクター PDF・編集可能 .pptx へ変換
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.architecture import ArchitectureDiagram, Node, NodeGroup, PhosphorIcon
from drawlib.styles import Styles

setup(width=158, height=62)

d = ArchitectureDiagram(
    node_style=Styles.DarkBold,
    node_text_style=Styles.DarkBold.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
)

src = d.add(
    NodeGroup(
        title="スライド原稿 (slide_src/<lang>/*.md)",
        width=48.0,
        height=52.0,
        style=Styles.PrimaryNeutral.patch(shape_r=2.2),
        text_style=Styles.DarkBold.patch(text_size=10.5, halign="left"),
    ),
    xy=(2.0, 3.0),
)
b_stage = src.add(
    Node(
        "::: block & # 見出し\n(1920x1080 ステージ)",
        width=39.0,
        height=12.0,
        icon=PhosphorIcon.LAYOUT,
        icon_size=4.8,
        card_style=Styles.White.patch(shape_r=1.6),
    ),
    xy=(24.0, 39.5),
)
b_code = src.add(
    Node(
        "埋め込み drawlib 図面\n& current_slide",
        width=39.0,
        height=12.0,
        icon=PhosphorIcon.CODE,
        icon_size=4.8,
        card_style=Styles.White.patch(shape_r=1.6),
    ),
    xy=(24.0, 25.0),
)
b_note = src.add(
    Node(
        "::: note 発表台本\n(スピーカーノート)",
        width=39.0,
        height=12.0,
        icon=PhosphorIcon.NOTE,
        icon_size=4.8,
        card_style=Styles.White.patch(shape_r=1.6),
    ),
    xy=(24.0, 10.5),
)

compiler = d.add(
    Node(
        "Slide コンパイラ\n& Chromium DOM 計測",
        width=33.0,
        height=20.0,
        icon=PhosphorIcon.CPU,
        icon_size=5.6,
        style=Styles.WhiteBold,
        text_style=Styles.WhiteBold.patch(text_size=10.8),
        card_style=Styles.PrimaryFlat.patch(shape_r=2.2),
    ),
    xy=(75.5, 29.0),
)

out_html = d.add(
    Node(
        "16:9 HTML スライド\n(発表者ビュー & SVG)",
        width=41.0,
        height=13.0,
        icon=PhosphorIcon.PRESENTATION_CHART,
        icon_size=4.8,
        card_style=Styles.PrimaryNeutral.patch(shape_r=1.8),
    ),
    xy=(131.5, 45.5),
)
out_pdf = d.add(
    Node(
        "16:9 ベクター PDF\n(slide_<lang>.pdf)",
        width=41.0,
        height=13.0,
        icon=PhosphorIcon.FILE_PDF,
        icon_size=4.8,
        card_style=Styles.Neutral.patch(shape_r=1.8),
    ),
    xy=(131.5, 29.0),
)
out_pptx = d.add(
    Node(
        "編集可能 .pptx 資料\n(ネイティブ図形 & ノート)",
        width=41.0,
        height=13.0,
        icon=PhosphorIcon.SLIDESHOW,
        icon_size=4.8,
        card_style=Styles.SecondaryNeutral.patch(shape_r=1.8),
    ),
    xy=(131.5, 12.5),
)

d.connect(src, compiler, routing="direct", padding=0.8)
j_mid = d.junction((101.5, 29.0))
j_top = d.junction((101.5, 45.5))
j_bot = d.junction((101.5, 12.5))
d.connect(compiler, j_mid, arrow="-", routing="direct", padding=(0.8, 0.0))
d.connect(j_mid, j_top, arrow="-", routing="direct")
d.connect(j_mid, j_bot, arrow="-", routing="direct")
d.connect(j_top, out_html, arrow="->", routing="direct", padding=(0.0, 0.8))
d.connect(j_mid, out_pdf, arrow="->", routing="direct", padding=(0.0, 0.8))
d.connect(j_bot, out_pptx, arrow="->", routing="direct", padding=(0.0, 0.8))

d.draw(xy=(2.0, 2.0))
save()

プロジェクト作成と HTML・PDF・編集可能 PowerPoint(.pptx)のビルド

生成された slide プロジェクトには、Web スライド・ベクター PDF に加えてネイティブ Microsoft PowerPoint 出力用のスクリプト(build_pptx.sh)が標準同梱されています。drawlib build pptx は ::: block カード・テキスト枠・箇条書き・表・::: note 発表者ノートを編集可能な 960×540 pt の PowerPoint オブジェクトへ変換し、図面を 240 DPI の高精細画像として配置します。

# 1. 日英 2 言語対応の 16:9 スライドプロジェクトを初期化
uv run drawlib init project slide pitch -l en,ja -s google

# 2. インタラクティブ 16:9 HTML スライド + ベクター PDF + 画像アセットを一括ビルド
./pitch_src/build.sh

# 3. 編集可能な PowerPoint ファイル(pitch_en.pptx / pitch_ja.pptx)を出力
./pitch_src/build_pptx.sh
# 同等の CLI コマンド:
uv run drawlib build pptx pitch_src/ -o pitch.pptx -s pitch_src/styles.py -u pitch_src/utils.py

13. Documentation as Code と 5つのプロジェクト種別

単体のアーキテクチャ図をコードで描けるだけでは、実務のドキュメント運用課題の半分しか解決できません。実際の開発組織では、設計図は複数章からなる技術設計書・検索可能な API ポータル・RFC 白書・経営層向けスライドの内部に組み込まれ、多言語や多様な配布形式の間で常に整合性を保つ必要があります。

Drawlib は Illustration as Code をさらに発展させた Illustrated Documentation as Code を実現します。純粋 Python による作図コードと Markdown 原稿を単一の Git リポジトリで一元管理し、静的 HTML サイト・A4 ベクター PDF・編集可能な Microsoft Word(.docx)仕様書・16:9 HTML/PDF スライド・ネイティブ PowerPoint(.pptx)・アニメーション画像群へと決定論的に一括コンパイルします。

13_doc_as_code_and_projects_1
図 13.1: 単一の Git リポジトリから 5 つのプロジェクト種別へ展開する Drawlib の全体像 (v0.3.7)
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line, lines
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=74)

text(
    (79.0, 68.2),
    "Drawlib v0.3.7 — 単一の Git ソースから展開する 5 つのプロジェクト種別",
    style=Styles.DarkBold.patch(text_size=12.8),
)

# 左側の焦点ハブ(PrimaryFlat は 1 箇所のみ)
rectangle((24.0, 34.5), width=34.0, height=55.0, style=Styles.PrimaryFlat.patch(shape_r=2.2))
phosphor.git_branch((24.0, 55.0), width=5.6, style=Styles.WhiteBold)
text((24.0, 46.2), "Git 単一情報源\n(<target>_src/)", style=Styles.WhiteBold.patch(text_size=11.5))
line((11.0, 39.8), (37.0, 39.8), style=Styles.White)
text(
    (24.0, 23.2),
    "• drawlib.toml\n• styles.py\n• <lang>/styles.py\n• utils.py\n• _assets/\n• build.sh",
    style=Styles.White.patch(text_size=10.2),
)

# 右側 3 段への直交バス接続
line((41.0, 34.5), (47.5, 34.5), style=Styles.DarkBold)
lines([(47.5, 34.5), (47.5, 53.5), (53.0, 53.5)], arrow_head="->", style=Styles.DarkBold)
line((47.5, 34.5), (53.0, 34.5), arrow_head="->", style=Styles.DarkBold)
lines([(47.5, 34.5), (47.5, 15.5), (53.0, 15.5)], arrow_head="->", style=Styles.DarkBold)

# 1段目: images & doc(ニュートラル基調カード)
rectangle((76.5, 53.5), width=47.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.images((58.0, 57.5), width=4.6, style=Styles.PrimaryBold)
text((79.5, 57.5), "1. images — 単体図解群", style=Styles.DarkBold.patch(text_size=11.0))
text((76.5, 49.3), "*.py -> PNG / WebP / GIF / PDF / SVG", style=Styles.Dark.patch(text_size=10.0))

rectangle((128.5, 53.5), width=47.0, height=16.5, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
phosphor.file_pdf((110.0, 57.5), width=4.6, style=Styles.PrimaryBold)
text((131.5, 57.5), "2. doc — 技術白書・仕様書", style=Styles.DarkBold.patch(text_size=11.0))
text((128.5, 49.3), "<lang>/*.md -> HTML / A4 PDF / DOCX", style=Styles.Dark.patch(text_size=10.0))

# 2段目: site & catalog
rectangle((76.5, 34.5), width=47.0, height=16.5, style=Styles.SecondaryNeutral.patch(shape_r=2.0))
phosphor.globe((58.0, 38.5), width=4.6, style=Styles.SecondaryBold)
text((79.5, 38.5), "3. site — ドキュメントサイト", style=Styles.DarkBold.patch(text_size=11.0))
text((76.5, 30.3), "navbar.md -> 階層 HTML / 検索 / PDF", style=Styles.Dark.patch(text_size=10.0))

rectangle((128.5, 34.5), width=47.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.brackets_curly((110.0, 38.5), width=4.6, style=Styles.SecondaryBold)
text((132.0, 38.5), "4. catalog — API 辞書", style=Styles.DarkBold.patch(text_size=11.0))
text((128.5, 30.3), "# H1・## GET -> 2列 UI + / 即時検索", style=Styles.Dark.patch(text_size=10.0))

# 3段目: slide(ワイドカード)
rectangle((102.5, 15.5), width=99.0, height=16.5, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
phosphor.presentation_chart((61.0, 19.5), width=4.8, style=Styles.PrimaryBold)
text((104.5, 19.5), "5. slide — 16:9 ワイドスクリーンプレゼンテーション", style=Styles.DarkBold.patch(text_size=11.2))
text(
    (102.5, 11.3),
    "1920x1080 ::: block & ::: note -> Web スライド / 16:9 PDF / 編集可能 PPTX",
    style=Styles.Dark.patch(text_size=10.2),
)

save()

13.1 プロジェクトファースト原則(drawlib init project)

1 枚のシステム構成図を描く場合でも、複数章の技術マニュアルを構築する場合でも、Drawlib では必ず**プロジェクトファースト原則(Project-First Rule)**に従います。空のディレクトリに裸の .py スクリプトを直接置いたり手作業でフォルダを作ったりせず、まず drawlib init project で <target>_src/ プロジェクトを自動生成します。

最初にプロジェクトの骨格を生成することで、以下の 5 つのエンジニアリング基盤が即座に整います。

5 つのプロジェクト種別はいずれも drawlib init project <type> [target] -l <langs> -s <style> で初期化できます(初期化後はサンプル用の sample*.py を削除または置き換えて運用します)。

# 利用可能な 5 つのスターターテンプレート一覧を確認
uv run drawlib init project list

# 1. 単体 Python 図解・アニメーション管理プロジェクト (images_src/)
uv run drawlib init project images diagrams -l ja -s google

# 2. 日英バイリンガルの技術白書・仕様書 -> HTML + A4 PDF + MS Word (.docx)
uv run drawlib init project doc whitepaper -l en,ja -s google

# 3. サイドバー付きマルチページ・ドキュメントポータル (docs_src/)
uv run drawlib init project site docs -l en,ja -s default

# 4. 検索可能な API / CLI / スキーマリファレンス辞書 (catalog_src/)
uv run drawlib init project catalog api_ref -l en,ja -s google

# 5. 日英バイリンガル 16:9 プレゼンスライド -> HTML + PDF + PowerPoint (.pptx)
uv run drawlib init project slide pitch -l en,ja -s google

13.2 Markdown 埋め込み作図ブロックと 720pt タイポグラフィ整合

doc・site・catalog・slide プロジェクトの Markdown 原稿(<target>_src/<lang>/**/*.md)では、drawlib コードフェンスを使って Python 作図コードを本文中に直接埋め込みます。ビルド時に各ブロックは独立したキャンバス状態で実行され、.drawlib/cache.db にキャッシュされた上でレスポンシブな図表要素へと変換されます。

```drawlib align=center code=fold file=gateway_flow_ja.png caption="図 13.3: リージョナル API ゲートウェイの基本構成"
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles

setup(width=136, height=44)

rectangle((28, 22), width=32, height=18, style=Styles.Neutral, text="クライアント")
rectangle(
    (68, 22),
    width=32,
    height=18,
    style=Styles.PrimaryFlat,
    text="API ゲートウェイ",
    text_style=Styles.WhiteBold,
)
rectangle((108, 22), width=32, height=18, style=Styles.SecondaryNeutral, text="認証サービス")

line((44, 22), (52, 22), arrow_head="->", style=Styles.DarkBold)
line((84, 22), (92, 22), arrow_head="->", style=Styles.DarkBold)
save()
```

埋め込み drawlib フェンスでは、以下の 4 つのヘッダ属性で出力ファイル名・配置・コード表示モードを制御します。

フェンス属性 指定可能な値 推奨される使い分け
file= <stem>.png / .webp / .svg 必須。 出力画像名を明示します(言語共通図は file=arch.png、日本語ローカライズ図は file=arch_ja.png)。drawlib show <md> <file> での個別検証にも使われます。
caption= "図 X.Y: ..." 文書では必須。 HTML/PDF では <figcaption>、Word(.docx)では図キャプション段落として出力されます。
align= center | left | right 本文カラム内での水平配置を指定します(標準は align=center)。
code= hide | fold | show code=hide(既定)は図のみを表示する白書・仕様書向け、code=fold は図の直下に折りたたみ <details> でコードを格納(5〜15行目の Top Hero 図で必須)、code=show はコードと図を両方展開する入門解説向けです。

ピクセル幅指定(width=...px)を省略すべき数学的理由(720pt 法則)

Drawlib の描画エンジンは、setup(width=W, height=H) の論理幅 W が 100 でも 160 でも、画像全体の物理幅を常に 10 inch = 720 pt に正規化して出力します。この画像が HTML 本文カラム(幅 $W_{\text{HTML}}$、通常 880px〜960px)に表示されるとき、図中の text_size(単位 pt)は以下の画面ピクセルサイズで描画されます。

$$\text{画面上の描画文字サイズ (px)} = \text{text_size} \times \frac{W_{\text{HTML}}}{720}$$


13.3 5つのプロジェクト種別の徹底比較

Drawlib v0.3.7 は用途に応じた 5 つのプロジェクト種別を備えています。Markdown ベースの 4 種別(doc・site・catalog・slide)はすべて共通の <target>_src/<lang>/ ディレクトリ構造を採用しているため、git mv でファイルを移動することなく後からいつでも対応言語を追加できます。

種別 ソース構成 ナビゲーション・構造 生成される成果物 主なエンジニアリング用途
images images_src/*.py 単体/階層 Python スクリプト・AST 重複出力検査 .png(APNG 含む)・.webp・.gif・.pdf・.svg 構成図アセット集、README 図版、CI チャート生成、プロトコル解説アニメーション
doc doc_src/<lang>/*.md (00_cover.md, 01_*...) ファイル名順の章結合+--generate-index 目次自動挿入(navbar.md 不要) 単一ページ HTML 本・A4 ベクター PDF・編集可能 Word .docx・GitHub MD 技術 RFC、アーキテクチャ白書、オンボーディング教本、印刷用設計書、Word レビュー文書
site docs_src/<lang>/**/*.md + <lang>/navbar.md navbar.md による階層サイドバー・ダークモード・検索 マルチページ HTML ポータル・個別 PDF・GitHub MD・llms.txt プロダクト公式ドキュメント、開発者ポータル、複数チームの設計 Wiki
catalog catalog_src/<lang>/*.md (自動インデックス) # H1 カテゴリ&## H2 項目の固定 2 ペイン UI+/ 即時ライブ検索 2 カラム検索対応 HTML 辞書・GitHub MD・llms.txt REST/gRPC API 辞書、CLI オプション辞典、イベント/メトリクス一覧、DB スキーマ定義書
slide slide_src/<lang>/*.md (01_title.md...) 1920×1080 ::: block ステージ+::: note 台本+発表者ビュー 16:9 対話型 HTML スライド・16:9 ベクター PDF・編集可能 .pptx 技術カンファレンス登壇資料、アーキテクチャ審査会、スプリントデモ、顧客提案スライド

実務で使い分ける際の各アーキタイプの特徴は以下の通りです。

  1. images(単体図解・アニメーション集): save() を呼ぶ各 *.py スクリプトから <script_stem>.png(Animation() 使用時は .webp / .gif 等も対応)を一括生成します。実行前に Python の ast モジュールで全スクリプトを静的解析し、出力ファイル名の衝突があれば実行前に即座にエラー停止します。1 スクリプト内で複数画像を出力する場合は、次の setup() の前に必ず clear() を呼びます。
  2. doc(章立て技術白書・A4 PDF・Word .docx): 番号付き章ファイル(00_cover.md, 01_overview.md, ...)を辞書順に結合し、改ページ(--page-break)と多言語対応の自動目次(--generate-index)を 00_cover.md の直後に挿入します。標準の ./doc_src/build.sh(HTML + A4 PDF)に加え、./doc_src/build_docx.sh(drawlib build docx)を実行すればネイティブ編集可能な Word 文書(.docx)を出力できます。
  3. site(マルチページ・ドキュメントポータル): <lang>/navbar.md の定義に従い、階層化された Markdown 群をライト/ダークモード切り替え・多言語スイッチャー・シンタックスハイライト付きの Web ポータルへ変換します(内部 .md リンクは自動で .html に書き換えられます)。
  4. catalog(検索可能な API・スキーマリファレンス辞書): 手動の navbar.md 管理を不要にした 1 Markdown = 1 HTML ページ 構成です。各ファイルの # H1 が左サイドバーのアコーディオン項目、すべての ## H2 がスクロール連動 #slug リンクとなり、/ キーによる即時インクリメンタルフィルタと意味別カラーバッジ(HTTP メソッド ## `GET` /v1/users/<id>、コード要素 ## `def` setup()、基盤要素 ## `service` auth-api・## `table` accounts・## `cmd` drawlib build など)を自動レンダリングします。
  5. slide(16:9 ワイドスクリーンスライド & 編集可能 .pptx): 聴衆が見るスライド面(1920×1080 ピクセル座標の ::: block xy=(x, y) width=W height=H)と発表者用の読み上げ台本(::: note)を明確に分離します。デュアルウィンドウ発表者ビュー付き HTML や 16:9 ベクター PDF に加え、./slide_src/build_pptx.sh(drawlib build pptx)で Chromium DOM を計測し、編集可能なネイティブ PowerPoint 図形・テキスト・表・スピーカーノート・240 DPI 図版を生成します。
13_doc_as_code_and_projects_2
図 13.2: 多言語対応プロジェクトのディレクトリ構造(TreeNode ソース構成から docs/ 成果物群へのコンパイル)
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.smartarts import CardList, Panel, TreeNode
from drawlib.styles import Colors, Styles

setup(width=158, height=74)

# 左パネル: 信頼できる唯一の情報源 (docs_src/)
src_panel = Panel(
    "ソースディレクトリ (docs_src/)",
    style=Styles.Neutral.patch(shape_r=2.2),
    header_style=Styles.PrimaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=12.0),
    icon=phosphor.folder,
    icon_style=Styles.PrimaryBold,
    header_height=8.8,
)
src_panel.draw((6.0, 6.0), width=76.0, height=62.0)

TreeNode.register_drawing_item(
    name="dir_item",
    location="before",
    padding_width=4.4,
    function=phosphor.folder,
    style=Styles.Primary.patch(icon_color=Colors.Primary),
    args={"width": 3.0},
)
TreeNode.register_drawing_item(
    name="py_item",
    location="before",
    padding_width=4.4,
    function=phosphor.file_py,
    style=Styles.Secondary.patch(icon_color=Colors.Secondary),
    args={"width": 3.0},
)
TreeNode.register_drawing_item(
    name="cfg_item",
    location="before",
    padding_width=4.4,
    function=phosphor.file_code,
    style=Styles.Dark.patch(icon_color=Colors.Dark),
    args={"width": 3.0},
)

root = TreeNode(
    "docs_src/",
    text_style=Styles.DarkBold.patch(text_size=10.5),
    line_style=Styles.MutedBold.patch(line_width=1.2),
    line_horizontal_margin=2.8,
    line_horizontal_length=2.8,
    line_vertical_margin=4.8,
    children=[
        TreeNode("drawlib.toml  ([project], [i18n], [seo])", text_style=Styles.Dark.patch(text_size=10.2)).set_drawing_item("cfg_item"),
        TreeNode("styles.py & utils.py  (共通テーマ定義)", text_style=Styles.Dark.patch(text_size=10.2)).set_drawing_item("py_item"),
        TreeNode("_assets/  (favicon.png・ロゴ素材)", text_style=Styles.Dark.patch(text_size=10.2)).set_drawing_item("dir_item"),
        TreeNode(
            "en/  (英語デフォルト原稿)",
            children=[
                TreeNode("navbar.md & index.md", text_style=Styles.Dark.patch(text_size=10.0)).set_drawing_item("cfg_item"),
            ],
        ).set_drawing_item("dir_item"),
        TreeNode(
            "ja/  (日本語ローカライズ原稿)",
            children=[
                TreeNode("styles.py, navbar.md & index.md", text_style=Styles.Dark.patch(text_size=10.0)).set_drawing_item("py_item"),
            ],
        ).set_drawing_item("dir_item"),
        TreeNode("build.sh & serve.sh", text_style=Styles.Dark.patch(text_size=10.2)).set_drawing_item("cfg_item"),
    ],
).set_drawing_item("dir_item")
root.draw(xy=(10.5, 54.5))

# コンパイラ接続矢印
line((83.2, 37.0), (89.8, 37.0), arrow_head="->", style=Styles.DarkBold)

# 右パネル: コンパイル生成物 (docs/)
out_panel = Panel(
    "コンパイル生成物 (docs/)",
    style=Styles.PrimaryNeutral.patch(shape_r=2.2),
    header_style=Styles.SecondaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=12.0),
    icon=phosphor.globe,
    icon_style=Styles.PrimaryBold,
    header_height=8.8,
)
out_panel.draw((91.0, 6.0), width=61.0, height=62.0)

outputs = CardList(
    style=Styles.White.patch(shape_r=1.6),
    text_style=Styles.DarkBold.patch(text_size=10.8),
    description_style=Styles.Dark.patch(text_size=10.0),
    badge_shape="none",
    icon_style=Styles.DarkBold,
    spacing=2.2,
)
outputs.add(
    "docs/ (または docs_html/)",
    "en/・ja/・共有 images/・言語切替 UI",
    icon=phosphor.globe,
    style=Styles.PrimaryFlat.patch(shape_r=1.6),
    text_style=Styles.WhiteBold.patch(text_size=10.8),
    description_style=Styles.White.patch(text_size=10.0),
    icon_style=Styles.WhiteBold,
)
outputs.add(
    "PDF & Office ドキュメント",
    "*_<lang>.pdf / .docx / .pptx",
    icon=phosphor.file_pdf,
)
outputs.add(
    "SEO & AI エージェント索引",
    "sitemap.xml・robots.txt・llms.txt",
    icon=phosphor.brackets_curly,
)
outputs.add(
    "差分ビルドキャッシュ (.drawlib/)",
    "SHA-256 SQLite cache.db (<1ms 復元)",
    icon=phosphor.database,
    style=Styles.SecondaryNeutral.patch(shape_r=1.6),
)
outputs.draw((94.5, 9.0), width=54.0, height=46.5)

save()

13.4 drawlib.toml の設計:[project]・[i18n]・[seo]

Markdown ベースのすべてのプロジェクト(doc・site・catalog・slide)は、<target>_src/drawlib.toml によって宣言的に制御されます。メタデータや出力ポリシーを drawlib.toml に集約することで、シェルスクリプトや CI 設定でのオプション指定漏れを防げます。

[project]
name = "platform_docs"
type = "site"                 # "images" | "doc" | "site" | "catalog" | "slide"
title = "Cloud Platform Architecture"
description = "Unified architecture portal and API guide."
style = "google"              # "default" | "google" | "monochrome"

[i18n]
default_lang = "en"
langs = ["en", "ja"]          # doc/site/catalog/slide では単一言語でも必須

[i18n.labels]
en = "English"
ja = "日本語"

[i18n.descriptions]
en = "Cloud Platform architecture documentation and operational runbooks."
ja = "クラウドプラットフォームのアーキテクチャ設計書および運用ガイド。"

[seo]
site_url = "https://docs.example.com"
og_image = "_assets/ogp.png"
twitter_card = "summary_large_image"
sitemap = true
robots = true
llms_txt = true

多言語スケルトン整合検査([i18n])と AI 検索最適化([seo])

langs に複数言語(["en", "ja"] など)を設定すると、Drawlib はビルド開始時に**4 段階の機械的パリティ検査(構造整合チェック)**を自動実行し、翻訳漏れや構造崩れを本番デプロイ前に検出します。

14. CLI ツールチェインと自律型 AI エージェント協業

Drawlib は、人間のエンジニアだけでなくマルチモーダル AI コーディングエージェントとの協業を前提にゼロから設計されています。すべての図版・技術文書・スライドが決定論的な CLI(drawlib)を通じてヘッドレスにビルドできるため、AI エージェントが当て推量で座標を書いたり GUI 操作で詰まったりすることはありません。

AI エージェントは必要に応じて内蔵リファレンス(drawlib rules show)を参照し、プロジェクトを初期化(drawlib init)し、座標グリッド付きで単体プレビューを出力(drawlib show -g)して視覚的に自己検証し、文字重なりを 1 回で補正した上でリンク切れ検査(drawlib serve --check)までを自律的に完結させます。

14_cli_and_ai_workflow_1
図 14.1: 自律型 AI エージェントによる 3 段階マルチモーダル自己修正ループ
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line, lines
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=70)

text(
    (79.0, 63.8),
    "自律型 AI エージェントの 3 段階マルチモーダル自己修正ループ",
    style=Styles.DarkBold.patch(text_size=12.8),
)

# Step 0: ルール参照 & コード執筆(Neutral)
rectangle((22.5, 39.5), width=32.5, height=33.0, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.book_open((22.5, 50.8), width=5.0, style=Styles.PrimaryBold)
text((22.5, 44.0), "0. 規約参照・執筆", style=Styles.DarkBold.patch(text_size=11.0))
text(
    (22.5, 31.8),
    "• rules show <topic>\n• .md / .py を作成\n• styles.py・utils.py",
    style=Styles.Dark.patch(text_size=10.0),
)

# Stage 1: 静的・アンカー検査(PrimaryNeutral)
rectangle((60.2, 39.5), width=32.5, height=33.0, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
phosphor.list_checks((60.2, 50.8), width=5.0, style=Styles.PrimaryBold)
text((60.2, 44.0), "Stage 1: 静的検査", style=Styles.DarkBold.patch(text_size=11.0))
text(
    (60.2, 31.8),
    "• 2図以上・Hero L5-15\n• text_size >= 10.0\n• 座標アンカー確認",
    style=Styles.Dark.patch(text_size=10.0),
)

# Stage 2: グリッド視覚検証(焦点 PrimaryFlat)
rectangle((97.8, 39.5), width=32.5, height=33.0, style=Styles.PrimaryFlat.patch(shape_r=2.0))
phosphor.grid_four((97.8, 50.8), width=5.0, style=Styles.WhiteBold)
text((97.8, 44.0), "Stage 2: Grid (-g)", style=Styles.WhiteBold.patch(text_size=11.0))
text(
    (97.8, 31.8),
    "• drawlib show -g\n• 10/5単位グリッド確認\n• 重なり・はみ出し検知",
    style=Styles.White.patch(text_size=10.0),
)

# Stage 3: マクロページ検証(SecondaryNeutral)
rectangle((135.5, 39.5), width=32.5, height=33.0, style=Styles.SecondaryNeutral.patch(shape_r=2.0))
phosphor.browser((135.5, 50.8), width=5.0, style=Styles.SecondaryBold)
text((135.5, 44.0), "Stage 3: 全体検証", style=Styles.DarkBold.patch(text_size=11.0))
text(
    (135.5, 31.8),
    "• build & serve --check\n• 1280x920 画面確認\n• PDF / DOCX / PPTX",
    style=Styles.Dark.patch(text_size=10.0),
)

# 前進パイプライン矢印
line((38.8, 39.5), (43.9, 39.5), arrow_head="->", style=Styles.DarkBold)
line((76.5, 39.5), (81.5, 39.5), arrow_head="->", style=Styles.DarkBold)
line((114.1, 39.5), (119.2, 39.5), arrow_head="->", style=Styles.DarkBold)

# Stage 2 から Step 0 への自己修正フィードバック経路
lines([(97.8, 22.8), (97.8, 11.0), (22.5, 11.0), (22.5, 22.2)], arrow_head="->", style=Styles.PrimaryBold)
rectangle((60.2, 11.0), width=64.0, height=7.6, style=Styles.White.patch(shape_r=1.5))
text(
    (60.2, 11.0),
    "自己修正ループ:グリッド目盛りで座標を一発補正",
    style=Styles.DarkBold.patch(text_size=10.2),
)

save()

14.1 プロジェクトと AI ルールの初期化(drawlib init)

drawlib init コマンド群は、プロジェクト骨格の生成(drawlib init project)と、AI コーディングエージェント向けルールファイルの自動配置(drawlib init agent)の 2 つの役割を担います。

drawlib init agent を実行すると、Drawlib の設計規約・720pt タイポグラフィ計算式・座標アンカー規則・3 段階自己検証プロトコルが各 AI ツール固有のルール形式で書き出されます。Drawlib v0.3.7 は以下の 9 つのエージェントターゲットを標準サポートしており、さらに共通規格である AGENTS.md を介して Kiro や OpenCode などの AGENTS.md 準拠エージェントともそのまま連携できます。

CLI ターゲット 対応 AI アシスタント 生成されるワークスペースパス ルール形式・読み込み挙動
antigravity Google Antigravity .agents/rules/drawlib.md YAML フロントマター(trigger: model_decision または always_on)
gemini Gemini CLI GEMINI.md HTML コメントマーカーによる非破壊マージ(--global で ~/.gemini/GEMINI.md)
claude Claude Code .claude/rules/drawlib.md ルートの CLAUDE.md と競合しないモジュラールールファイル
cursor Cursor .cursor/rules/drawlib.mdc MDC フロントマター(description, alwaysApply)
codex OpenAI Codex / AGENTS.md AGENTS.md HTML マーカーによる非破壊追記(Codex・Kiro 等の AGENTS.md 対応ツール共通)
copilot GitHub Copilot .github/instructions/drawlib.instructions.md YAML フロントマター(applyTo: "**/*_src/**, **/*.md, **/*.py")
windsurf Windsurf .windsurf/rules/drawlib.md YAML トリガー付き Cascade ワークスペースルール
cline Cline .clinerules/drawlib.md .clinerules/ 配下で個別に ON/OFF 可能なルールファイル
roo Roo Code .roo/rules/drawlib.md ワークスペースまたは --global 対応のモジュラールール

--detect フラグを渡せばリポジトリ内の既存エージェント設定フォルダを自動検出して最適なルールを生成でき、既定の -m auto モードにより作図・ドキュメント関連タスク時のみオンデマンドで読み込まれるためコンテキストトークンを無駄に消費しません。

# 対応する AI エージェント一覧と出力先パスを表示
uv run drawlib init agent list

# リポジトリ内で使用中の AI エージェントを自動検出してルールを生成
uv run drawlib init agent --detect

# 特定の AI エージェント向けにルールを一括生成('all' でモジュラー 7 種を一括生成)
uv run drawlib init agent antigravity claude cursor copilot

# ドキュメント専用リポジトリ向けに常時読み込みモード(always)で生成
uv run drawlib init agent antigravity --mode always

# ユーザーホーム配下(~/.claude/rules/ 等)へグローバルルールとして導入
uv run drawlib init agent claude --global

14.2 マルチ形式ビルド・SHA-256 キャッシュ・検証サーバー

プロジェクト初期化後の日常開発・高速プレビュー・マルチ形式出版・CI 品質検査は、以下の 5 つの主要 CLI コマンドグループで完結します。

コマンド群 主なサブコマンド・オプション 主な役割
drawlib build html, pdf, docx, slide, pptx, markdown, image(-s, -u, --no-cache) <target>_src/ から HTML サイト/白書/スライド・ベクター PDF・編集可能な .docx/.pptx・GitHub Markdown・PNG/WebP/GIF 画像を一括コンパイルします。
drawlib show <file> [target] -s styles.py -u utils.py -g -o preview.png 単一の .py または Markdown 内の名前付きブロック(file=arch.png)だけを 10/5 単位グリッド(-g)付きで高速ヘッドレス出力します。
drawlib serve <html_dir> [-p 8000] [--no-browser] [--check] 依存ゼロのローカル確認用 HTTP サーバーを起動します。--check を付けると <a href> リンク切れと <img src> 欠損を検査して終了コードを返します。
drawlib cache list, clear --images, clear --all, download --all, download --browser .drawlib/cache.db の画像キャッシュ管理に加え、フォント・34 種アイコン・地図データ・Playwright ヘッドレス Chromium の事前取得を行います。
drawlib rules list, show <topic> [--raw | --rebuild], build --all, clear 人間と LLM の双方がターミナル上で即座に参照できる自己文書化マニュアル(overview, cli, style-guide, review-guide, lib-*)を提供します。

.drawlib/cache.db の仕組み:SHA-256 コンテンツアドレス型キャッシュ

本文の誤字を 1 文字直すたびに 50 枚の Python 図版を再描画していては執筆サイクルが滞ります。Drawlib は各 drawlib ブロックや .py スクリプトを実行する前に、以下の 4 要素から決定論的な SHA-256 ハッシュキーを算出します。

  1. ブロックまたは .py スクリプトの正確な Python 作図コード。
  2. styles.py(-s)および utils.py(-u)のファイル内容全体。
  3. コード内の文字列リテラルで参照されたローカル静的ファイル("_assets/logo.png" など)。
  4. 出力画像フォーマット(png / webp / svg)・相対パス・総スライド数。

.drawlib/cache.db 内に一致するハッシュが存在すれば、Matplotlib を一切起動せず 1 ミリ秒未満でキャッシュ済みバイナリを復元します。キャッシュ DB は自動で .drawlib/.gitignore(*)を生成し、drawlib や matplotlib のバージョン更新時の自動無効化と 1 GiB 超過時の古いエントリ 50% 自動削除(LRU)も備えています。styles.py / utils.py 以外の外部自作モジュールを import して編集した場合は、--no-cache または drawlib cache clear --images で再描画を強制できます。

# 1. Markdown 内の特定図版だけを 10/5 単位の座標グリッド (-g) 付きで高速出力
uv run drawlib show doc_src/en/01_overview.md system_arch.png \
    -s doc_src/styles.py -u doc_src/utils.py -g --no-cache -o .drawlib/scratch/arch_grid.png

# 2. マルチ形式ビルド(HTML・目次付き A4 PDF・Word .docx・16:9 スライド・PowerPoint .pptx)
uv run drawlib build html docs_src/ -o docs_html/ -s docs_src/styles.py -u docs_src/utils.py
uv run drawlib build pdf doc_src/ -o manual.pdf --page-break --generate-index -s doc_src/styles.py
uv run drawlib build docx doc_src/ -o manual.docx --page-break --generate-index --dpi 200
uv run drawlib build slide slide_src/ -o slide_html/ -s slide_src/styles.py
uv run drawlib build pptx slide_src/ -o slide.pptx --dpi 240 -s slide_src/styles.py
uv run drawlib build markdown docs_src/ -o docs_markdown/
uv run drawlib build image images_src/ -o images/ -f webp

# 3. CI 用アセット事前取得・画像キャッシュ削除・リンク切れ検査・オンデマンド規約参照
uv run drawlib cache download --all
uv run drawlib cache clear --images
uv run drawlib serve docs_html/ --check
uv run drawlib rules list
uv run drawlib rules show lib-smartarts-overview --raw
14_cli_and_ai_workflow_2
図 14.2: Drawlib CLI コマンド体系と SHA-256 SQLite キャッシュパイプライン
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=76)

text(
    (79.0, 70.2),
    "Drawlib CLI コマンド体系と SHA-256 差分ビルドキャッシュ機構",
    style=Styles.DarkBold.patch(text_size=12.8),
)

# 上段: init / rules / show(ニュートラル基調カード 3 枚)
rectangle((29.0, 56.5), width=46.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.folder_plus((11.5, 60.5), width=4.6, style=Styles.PrimaryBold)
text((31.5, 60.5), "drawlib init", style=Styles.DarkBold.patch(text_size=11.2))
text((29.0, 52.2), "project (5種別) | agent (--detect)", style=Styles.Dark.patch(text_size=10.0))

rectangle((79.0, 56.5), width=46.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.book_open((61.5, 60.5), width=4.6, style=Styles.PrimaryBold)
text((81.5, 60.5), "drawlib rules", style=Styles.DarkBold.patch(text_size=11.2))
text((79.0, 52.2), "list | show <topic> (--raw)", style=Styles.Dark.patch(text_size=10.0))

rectangle((129.0, 56.5), width=46.0, height=16.5, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
phosphor.grid_four((111.5, 60.5), width=4.6, style=Styles.PrimaryBold)
text((131.5, 60.5), "drawlib show", style=Styles.DarkBold.patch(text_size=11.2))
text((129.0, 52.2), "<file> [target] -g -o preview.png", style=Styles.Dark.patch(text_size=10.0))

# 中段: build(焦点 PrimaryFlat) / serve / cache
rectangle((29.0, 37.0), width=46.0, height=16.5, style=Styles.PrimaryFlat.patch(shape_r=2.0))
phosphor.cpu((11.5, 41.0), width=4.6, style=Styles.WhiteBold)
text((31.5, 41.0), "drawlib build", style=Styles.WhiteBold.patch(text_size=11.2))
text((29.0, 32.7), "html | pdf | docx | slide | pptx | image", style=Styles.White.patch(text_size=10.0))

rectangle((79.0, 37.0), width=46.0, height=16.5, style=Styles.SecondaryNeutral.patch(shape_r=2.0))
phosphor.globe((61.5, 41.0), width=4.6, style=Styles.SecondaryBold)
text((81.5, 41.0), "drawlib serve", style=Styles.DarkBold.patch(text_size=11.2))
text((79.0, 32.7), "<dir> -p 8000 | --check (CI 検証)", style=Styles.Dark.patch(text_size=10.0))

rectangle((129.0, 37.0), width=46.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.0))
phosphor.database((111.5, 41.0), width=4.6, style=Styles.SecondaryBold)
text((131.5, 41.0), "drawlib cache", style=Styles.DarkBold.patch(text_size=11.2))
text((129.0, 32.7), "list | clear --images | download --all", style=Styles.Dark.patch(text_size=10.0))

# 下段: .drawlib/cache.db SHA-256 キャッシュパイプライン
rectangle((79.0, 14.0), width=146.0, height=16.5, style=Styles.Neutral.patch(shape_r=2.2))
rectangle((31.0, 14.0), width=42.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
text((31.0, 14.0), "コード + styles/utils + 素材", style=Styles.DarkBold.patch(text_size=10.0))

line((52.0, 14.0), (59.5, 14.0), arrow_head="->", style=Styles.DarkBold)

rectangle((78.5, 14.0), width=36.0, height=11.5, style=Styles.PrimaryNeutral.patch(shape_r=1.5))
text((78.5, 14.0), "SHA-256 cache.db", style=Styles.DarkBold.patch(text_size=10.2))

line((96.5, 14.0), (104.0, 14.0), arrow_head="->", style=Styles.DarkBold)

rectangle((126.5, 14.0), width=43.0, height=11.5, style=Styles.White.patch(shape_r=1.5))
text((126.5, 14.0), "Hit: <1ms復元 | Miss: 再描画", style=Styles.DarkBold.patch(text_size=10.0))

# build からキャッシュ機構外枠への接続
line((29.0, 28.5), (29.0, 22.6), arrow_head="->", style=Styles.DarkBold)

save()

14.3 徹底解説:AI エージェントの 3 段階自律検証ループ

単体 PNG 画像を 1 回眺めるだけでは、出版品質は保証できません。キャンバス内部での微妙なラベル重なり(ミクロ幾何の欠陥)と、HTML カラムや 16:9 スライドに埋め込んだ際の文字縮小・ファーストビュー崩れ(マクロページの欠陥)は性質が異なるためです。AI エージェントは以下の 3 段階検証を順に実行することで両方の欠陥を自律的に解消します。

  1. Stage 1 — 静的コード・座標アンカー事前検査:
    • 各ドキュメントページに 2 個以上の drawlib 図版が含まれ、5〜15 行目に code=fold の Top Hero 図が配置されていることを確認します。
    • フェンスへの狭いピクセル幅指定(width=500px / 600px)がないこと、text_size >= 10.0 であること、相対パスのみが使われていること、および各コンポーネントの座標アンカー仕様(Center / Bottom-Left / Top-Left)の計算が正しいことを確認します。
  2. Stage 2 — 座標グリッドによるミクロ幾何レビュー(drawlib show -g):
    • 作成・変更した各図版を -g 付きで個別出力(uv run drawlib show <file> <target.png> -s styles.py -u utils.py -g -o .drawlib/scratch/<unique>_grid.png)し、マルチモーダル視覚ツールで確認します。
    • -g オーバーレイには数値ラベル付きの 10 単位主グリッド線と 5 単位補助破線が描画されるため、テキストの枠線はみ出し・矢印との干渉・外周マージン不足(< 4〜6 単位)・虹色過多(ニュートラル比率 < 50%)を見つけた際、正確な修正先座標を 1 回で読み取って修正できます。
  3. Stage 3 — マクロページ・最終成果物レビュー:
    • ./<target>_src/build.sh でビルド後、uv run drawlib serve <target>_html/ --check でリンク切れと画像欠損がゼロであることを検証し、1280×920 デスクトップ画面のヘッドレス Chromium スクリーンショット(および生成された PDF / DOCX / PPTX)を視覚確認します。
    • スクロールなしで Top Hero 図がファーストビューに収まり、図中テキストが周囲の 16px HTML 本文(1920×1080 スライドでは 26px 本文)と同等の視認性を持っていることを確認し、最後に .drawlib/scratch/ を削除します。

14.4 並列サブエージェント委譲による大規模ドキュメント構築

全 16 章の技術書(doc)や数十ページのポータル(site / catalog)、20 枚超のスライド(slide)を単一エージェントのコンテキスト内で直列に執筆・画像確認すると、高解像度プレビュー画像でコンテキストウィンドウが圧迫されます。Drawlib のファイル分割構造は、3 フェーズの親オーケストレータ・並列サブエージェント委譲パターンにそのまま適合します。

15. ビジュアル設計原則と本番チェックリスト

優れた技術アーキテクチャ図は、派手な色を塗り重ねるのではなく、数学的に計算された文字サイズ・落ち着いたニュートラル配色・正確な座標アンカー・再利用可能なコンポーネントマクロによって、3 秒以内にシステムの全体構造を読み手に伝達します。

最終章となる本章では、エンジニアと AI コーディングエージェントが図版・技術白書・ドキュメントサイト・API 辞書・16:9 スライドを本番公開する前に確認すべき「設計原則と最終チェックリスト」を 1 ページのリファレンスに集約します。

15_best_practices_1
図 15.1: 配色とコントラストの規律 — DON'T(虹色の乱用)対 DO(50%以上のニュートラル基調+焦点ハイライト)
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line, lines
from drawlib.shapes import rectangle
from drawlib.smartarts import Panel
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=68)

# 左パネル: DON'T(原色の乱用と低コントラスト)
dont_panel = Panel(
    "DON'T: 虹色の乱用・低コントラスト",
    style=Styles.Neutral.patch(shape_r=2.2),
    header_style=Styles.DangerNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.4),
    icon=phosphor.x_circle,
    icon_style=Styles.DangerBold,
    header_height=8.8,
)
dont_panel.draw((6.0, 6.0), width=70.0, height=56.0)

rectangle(
    (24.0, 42.5),
    width=25.0,
    height=11.5,
    style=Styles.AccentFlat.patch(shape_r=1.6),
    text="クライアント",
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)
rectangle(
    (57.0, 42.5),
    width=27.0,
    height=11.5,
    style=Styles.WarningFlat.patch(shape_r=1.6),
    text="API ゲートウェイ",
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)
rectangle(
    (24.0, 25.5),
    width=25.0,
    height=11.5,
    style=Styles.SuccessFlat.patch(shape_r=1.6),
    text="認証ワーカー",
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)
rectangle(
    (57.0, 25.5),
    width=27.0,
    height=11.5,
    style=Styles.DangerFlat.patch(shape_r=1.6),
    text="プライマリ DB",
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)

line((36.5, 42.5), (43.5, 42.5), arrow_head="->", style=Styles.AccentBold)
line((57.0, 36.7), (57.0, 31.3), arrow_head="->", style=Styles.DangerBold)
lines([(43.5, 39.5), (39.5, 39.5), (39.5, 25.5), (36.5, 25.5)], arrow_head="->", style=Styles.PrimaryBold)

rectangle((41.0, 12.2), width=62.0, height=7.4, style=Styles.White.patch(shape_r=1.4))
text(
    (41.0, 12.2),
    "全ノードが原色で主張し合い視覚的な主役が消失",
    style=Styles.DangerBold.patch(text_size=10.0),
)

# 右パネル: DO(50%以上のニュートラル基調+焦点ハイライト 1 箇所)
do_panel = Panel(
    "DO: 50%以上ニュートラル+1点ハイライト",
    style=Styles.Neutral.patch(shape_r=2.2),
    header_style=Styles.PrimaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.4),
    icon=phosphor.check_circle,
    icon_style=Styles.PrimaryBold,
    header_height=8.8,
)
do_panel.draw((82.0, 6.0), width=70.0, height=56.0)

rectangle((117.0, 34.0), width=63.0, height=33.5, style=Styles.MutedDashed.patch(shape_r=1.8))

rectangle(
    (100.0, 42.5),
    width=25.0,
    height=11.5,
    style=Styles.White.patch(shape_r=1.6),
    text="クライアント",
    text_style=Styles.DarkBold.patch(text_size=10.5),
)
rectangle(
    (133.0, 42.5),
    width=27.0,
    height=11.5,
    style=Styles.PrimaryFlat.patch(shape_r=1.6),
    text="API ゲートウェイ",
    text_style=Styles.WhiteBold.patch(text_size=10.5),
)
rectangle(
    (100.0, 25.5),
    width=25.0,
    height=11.5,
    style=Styles.SecondaryNeutral.patch(shape_r=1.6),
    text="認証ワーカー",
    text_style=Styles.DarkBold.patch(text_size=10.5),
)
rectangle(
    (133.0, 25.5),
    width=27.0,
    height=11.5,
    style=Styles.PrimaryNeutral.patch(shape_r=1.6),
    text="プライマリ DB",
    text_style=Styles.DarkBold.patch(text_size=10.5),
)

line((112.5, 42.5), (119.5, 42.5), arrow_head="->", style=Styles.DarkBold)
line((133.0, 36.7), (133.0, 31.3), arrow_head="->", style=Styles.DarkBold)
lines([(119.5, 39.5), (115.5, 39.5), (115.5, 25.5), (112.5, 25.5)], arrow_head="->", style=Styles.DarkBold)

rectangle((117.0, 12.2), width=62.0, height=7.4, style=Styles.White.patch(shape_r=1.4))
text(
    (117.0, 12.2),
    "落ち着いたニュートラル基調で主役が一目で際立つ",
    style=Styles.PrimaryBold.patch(text_size=10.0),
)

save()

15.1 720pt タイポグラフィスケールと横幅設計

Drawlib は setup(width=W, height=H) の論理幅にかかわらずキャンバス横幅を 10 inch = 720 pt に固定して描画するため、HTML や PDF 上での実表示フォントサイズは text_size × 表示カラム幅 / 720 で決まります。Markdown フェンスに width=500px や 600px のような狭いピクセル上限を書くと画像全体が縮小され、16px の本文に対して図中文字だけが極端に小さくなります。ドキュメントフェンスではピクセル幅指定を必ず省略し、以下のスケール表に従ってください。

テキスト階層 推奨 text_size 推奨 Styles トークン ~900px カラムでの実描画サイズ
キャンバスタイトル 14.0 – 16.0 Styles.DarkBold.patch(text_size=14.5) ~17.5px – 20.0px(明快な図タイトル)
セクション・枠見出し 12.0 – 13.5 Styles.DarkBold.patch(text_size=12.5) ~15.0px – 16.9px(小見出しと同等)
ノード・カード本文 10.5 – 12.0 Styles.DarkBold(淡色上)/Styles.WhiteBold(*Flat 上) ~13.1px – 15.0px(16px 本文と調和)
接続線・軸ラベル 10.0 Styles.Dark または Styles.Muted ~12.5px(プロトコル・単位表記)
絶対下限(目盛り・コード) 9.5(9.5 未満禁止) Styles.Dark / SourceCode ~11.9px(判読可能な最小限界値)

16:9 スライド(1920×1080 ステージ)での補正ルール: スライドの本文フォントは 26px と Web 文書(16px)より大きく設計されています。左右 2 カラム分割の ::: block(width=960..1040)に図を配置する場合は、ノード/軸ラベルの text_size を 13.5 – 15.5(タイトルは 16.0 – 18.5)へ引き上げ、同時にボックス幅を 25%〜40% 広げて本文との文字サイズ比を揃えてください。


15.2 50%+ ニュートラル基調とコントラスト規律

プロフェッショナルな設計図は、すべての箱をカラフルに塗るのではなく、彩度を抑えた背景の中に主役だけを際立たせます。


15.3 座標アンカー完全早見表(Center・Bottom-Left・Top-Left)

Drawlib のキャンバス座標系は常に左下 (0, 0) 原点(右が +X、上が +Y)ですが、各コンポーネントに渡す xy 引数が「中央・左下・左上」のどれを指すかはモジュール群ごとに明確に定まっています。以下の対応表を押さえることで、「半分だけ右や上へズレる」配置バグを 100% 防げます。

アンカー種別 対象モジュール・クラス群 xy の解釈 占有領域(X / Y スパン)
中央 Center (cx, cy) shapes.*(rectangle, circle, cylinder 等)
text.text(), icons.*(phosphor, gcp, aws, azure 等)
images.image()
smartarts.Cycle, smartarts.MindMapNode
diagrams.* の Node(..., xy=(cx, cy))
要素の幾何学的中心 x: [cx - w/2, cx + w/2]
y: [cy - h/2, cy + h/2]
左下 Bottom-Left (x0, y0) charts.*(全 10 種の定量チャート)
diagrams.* の d.draw(xy=(x0, y0)) & NodeGroup(..., xy=(x0, y0))
大半の smartarts.*(BoxList, CardList, ChevronProcess, GridLayout, Pyramid, KpiCards, Roadmap, Panel, GeoMap, *Calendar)
領域の左下隅(右・上へ伸長)。幅 W のキャンバス中央配置は x0 = (W - width) / 2 x: [x0, x0 + width]
y: [y0, y0 + height]
左上 Top-Left (x0, y_top) smartarts.Table
smartarts.TreeNode
smartarts.BulletPoints
smartarts.SourceCode
領域の左上隅(行やツリー枝が y = 0 に向かって下方向へ伸長) x: [x0, x0 + width]
y: [y_top - height, y_top]
両方保持(get_layout()) graph.*(ArchitectureGraph, LayerGraph, TreeGraph, RadialGraph, GridGraph) 自動計算されたレイアウト結果が中心と左下の両プロパティを提供 .xy / .local_xy(中央)
.bottom_left / .local_bottom_left(左下)
15_best_practices_2
図 15.2: 座標アンカー視覚早見表 — 中央 (cx, cy)・左下 (x0, y0)・左上 (x0, y_top)
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import circle, rectangle
from drawlib.smartarts import Panel
from drawlib.styles import Styles
from drawlib.text import text

setup(width=158, height=70)

# カード 1: 中央アンカー Center (cx, cy)
p1 = Panel(
    "1. 中央 (cx, cy)",
    style=Styles.Neutral.patch(shape_r=2.0),
    header_style=Styles.PrimaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.2),
    icon=phosphor.crosshair,
    icon_style=Styles.PrimaryBold,
    header_height=8.5,
)
p1.draw((6.0, 6.0), width=46.0, height=58.0)

text((29.0, 51.5), "基準点: 中央 (cx, cy)", style=Styles.PrimaryBold.patch(text_size=10.2))
rectangle((29.0, 41.0), width=28.0, height=13.0, style=Styles.White.patch(shape_r=1.5))
line((15.0, 41.0), (43.0, 41.0), style=Styles.MutedDashed)
line((29.0, 34.5), (29.0, 47.5), style=Styles.MutedDashed)
circle((29.0, 41.0), radius=2.3, style=Styles.PrimaryOutlineBold)
circle((29.0, 41.0), radius=1.3, style=Styles.PrimaryFlat)

line((10.5, 29.0), (47.5, 29.0), style=Styles.Muted)
text(
    (29.0, 17.5),
    "• shapes.* & text()\n• icons.* & images.*\n• Cycle & MindMapNode\n• Diagram Node(xy=...)",
    style=Styles.Dark.patch(text_size=10.0),
)

# カード 2: 左下アンカー Bottom-Left (x0, y0)
p2 = Panel(
    "2. 左下 (x0, y0)",
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
    header_style=Styles.SecondaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.2),
    icon=phosphor.corners_out,
    icon_style=Styles.PrimaryBold,
    header_height=8.5,
)
p2.draw((56.0, 6.0), width=46.0, height=58.0)

rectangle((79.0, 43.5), width=28.0, height=13.0, style=Styles.White.patch(shape_r=1.5))
rectangle((71.5, 40.5), width=4.2, height=5.0, style=Styles.SecondaryNeutral)
rectangle((79.0, 42.5), width=4.2, height=9.0, style=Styles.PrimaryNeutral)
rectangle((86.5, 41.5), width=4.2, height=7.0, style=Styles.SecondaryNeutral)
line((65.0, 37.0), (94.8, 37.0), arrow_head="->", style=Styles.DarkBold)
line((65.0, 37.0), (65.0, 51.8), arrow_head="->", style=Styles.DarkBold)
circle((65.0, 37.0), radius=1.6, style=Styles.AccentFlat)
text((79.0, 32.5), "基準点: 左下隅 (x0, y0)", style=Styles.DarkBold.patch(text_size=10.2))

line((60.5, 29.0), (97.5, 29.0), style=Styles.Muted)
text(
    (79.0, 17.5),
    "• 全 10 種の Charts\n• Diagrams draw(xy=...)\n• ChevronProcess, Panel\n• CardList, KpiCards",
    style=Styles.Dark.patch(text_size=10.0),
)

# カード 3: 左上アンカー Top-Left (x0, y_top)
p3 = Panel(
    "3. 左上 (x0, y_top)",
    style=Styles.Neutral.patch(shape_r=2.0),
    header_style=Styles.PrimaryNeutral,
    text_style=Styles.DarkBold.patch(text_size=11.2),
    icon=phosphor.squares_four,
    icon_style=Styles.PrimaryBold,
    header_height=8.5,
)
p3.draw((106.0, 6.0), width=46.0, height=58.0)

text((129.0, 51.5), "基準点: 左上隅 (x0, y_top)", style=Styles.PrimaryBold.patch(text_size=10.2))
rectangle((129.0, 40.0), width=28.0, height=13.0, style=Styles.White.patch(shape_r=1.5))
rectangle((129.0, 44.5), width=28.0, height=4.0, style=Styles.PrimaryNeutral)
line((115.0, 38.0), (143.0, 38.0), style=Styles.Muted)
line((115.0, 46.5), (144.8, 46.5), arrow_head="->", style=Styles.DarkBold)
line((115.0, 46.5), (115.0, 32.0), arrow_head="->", style=Styles.DarkBold)
circle((115.0, 46.5), radius=1.6, style=Styles.PrimaryFlat)

line((110.5, 29.0), (147.5, 29.0), style=Styles.Muted)
text(
    (129.0, 17.5),
    "• Table\n• TreeNode\n• BulletPoints\n• SourceCode",
    style=Styles.Dark.patch(text_size=10.0),
)

save()

15.4 再利用マクロと多言語テーマ設計(utils.py & styles.py)

プロジェクト内の図版が 5 枚を超えた段階で、カード寸法やサブタイトル位置・日本語フォント指定を各 Markdown ブロックへコピー&ペーストし始めると保守性が急速に低下します。以下の 2 ファイルへ責務を分離して DRY(Don't Repeat Yourself) を徹底してください。

# 各章ブロックやスクリプト内 — utils.py と styles.py の共通マクロを再利用
from drawlib.canvas import save, setup
from drawlib.styles import Styles
from drawlib.utils import connect, service_card

setup(width=128, height=42)

# 共通マクロで焦点ノード 1 個+ニュートラル補助ノードを簡潔に描画
service_card((26, 21), "Ingress Proxy", "Envoy :443", width=32, height=18, style=Styles.PrimaryFlat)
service_card((96, 21), "Billing API", "Python / gRPC", width=32, height=18, style=Styles.SecondaryNeutral)
connect((42, 21), (80, 21), label="mTLS gRPC", style=Styles.DarkBold)

save()

15.5 本番リリース前の最終チェックリスト

プルリクエストのマージや成果物の配布前に、以下の 5 項目を満たしているか最終確認を行ってください。