1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SVGAdvent Calendar 2024

Day 14

PythonでSVG図解を作成する方法 - 条件分岐とサブプロセス編

Posted at

はじめに

前回の記事では基本的なプロセス図の生成方法を紹介しました。今回は条件分岐やサブプロセスなど、より複雑なビジネスプロセスの表現方法を解説します。

image.png

環境準備

前回と同じライブラリを使用します:

pip install svgwrite pyyaml

実装

基本クラスを拡張して、条件分岐とサブプロセスの機能を追加します:

import svgwrite
from typing import List, Dict, Optional
import yaml

class AdvancedProcessDiagram:
    def __init__(self, width: int = 1200, height: int = 800):
        self.dwg = svgwrite.Drawing(size=(width, height))
        self.node_width = 120
        self.node_height = 60
        self.spacing = 100
        self.current_x = 50
        self.current_y = 50
        self.vertical_spacing = 80
        
    def add_process_node(self, text: str, fill: str = "#E8F4F9") -> tuple:
        """プロセスノードを追加"""
        group = self.dwg.g()
        
        rect = self.dwg.rect(
            insert=(self.current_x, self.current_y),
            size=(self.node_width, self.node_height),
            fill=fill,
            stroke="black",
            rx=5, ry=5
        )
        group.add(rect)
        
        text_element = self.dwg.text(
            text,
            insert=(self.current_x + self.node_width/2, 
                    self.current_y + self.node_height/2),
            text_anchor="middle",
            dominant_baseline="middle"
        )
        group.add(text_element)
        
        self.dwg.add(group)
        return (self.current_x, self.current_y)
    
    def add_decision_node(self, text: str) -> tuple:
        """条件分岐ノードを追加(ダイヤモンド形)"""
        group = self.dwg.g()
        
        # ダイヤモンド形のパスを作成
        diamond_width = self.node_width
        diamond_height = self.node_height
        center_x = self.current_x + diamond_width/2
        center_y = self.current_y + diamond_height/2
        
        path = self.dwg.path(
            d=f"M {center_x},{self.current_y} "
            f"L {self.current_x + diamond_width},{center_y} "
            f"L {center_x},{self.current_y + diamond_height} "
            f"L {self.current_x},{center_y} Z",
            fill="#FFB366",  # オレンジ色を濃くした
            stroke="black"
        )
        group.add(path)
        
        text_element = self.dwg.text(
            text,
            insert=(center_x, center_y),
            text_anchor="middle",
            dominant_baseline="middle"
        )
        group.add(text_element)
        
        self.dwg.add(group)
        return (self.current_x, self.current_y)
    
    def add_subprocess(self, text: str, subprocess_steps: List[str]) -> tuple:
        """サブプロセスを表現するノードを追加"""
        group = self.dwg.g()
        
        # メインの長方形
        rect = self.dwg.rect(
            insert=(self.current_x, self.current_y),
            size=(self.node_width, self.node_height),
            fill="#F0F8FF",
            stroke="black",
            rx=5, ry=5
        )
        group.add(rect)
        
        # サブプロセスを示す小さい四角を右下に追加
        small_rect = self.dwg.rect(
            insert=(self.current_x + self.node_width - 20, 
                   self.current_y + self.node_height - 20),
            size=(15, 15),
            fill="white",
            stroke="black"
        )
        group.add(small_rect)
        
        # メインテキスト
        text_element = self.dwg.text(
            text,
            insert=(self.current_x + self.node_width/2, 
                    self.current_y + self.node_height/2),
            text_anchor="middle",
            dominant_baseline="middle"
        )
        group.add(text_element)
        
        self.dwg.add(group)
        return (self.current_x, self.current_y)

    def arrow_marker(self):
        """矢印マーカーの定義"""
        marker = self.dwg.marker(
            insert=(10,5), size=(10,10),
            orient="auto",
            markerUnits="strokeWidth"
        )
        marker.add(self.dwg.path(
            d="M 0,0 L 10,5 L 0,10 L 4,5 Z",
            fill="black"
        ))
        self.dwg.defs.add(marker)
        return marker.get_funciri()
    
    def add_arrow(self, start: tuple, end: tuple, label: str = ""):
        """ラベル付き矢印を追加(改善版)"""
        def calculate_start_point(node_type, direction):
            if node_type == 'decision':
                center_x = start[0] + self.node_width/2
                center_y = start[1] + self.node_height/2
                if direction == 'right':
                    return (start[0] + self.node_width, center_y)
                elif direction == 'bottom':
                    return (center_x, start[1] + self.node_height)
            return (start[0] + self.node_width, start[1] + self.node_height/2)

        def calculate_control_point(start_point, end_point):
            """ベジェ曲線の制御点を計算"""
            if label == 'なし':
                return (start_point[0], end_point[1])
            return None

        # 開始点と終点の計算
        if label == 'あり':
            start_point = calculate_start_point('decision', 'right')
        elif label == 'なし':
            start_point = calculate_start_point('decision', 'bottom')
        else:
            start_point = calculate_start_point('process', 'right')

        end_point = (end[0], end[1] + self.node_height/2)
        control_point = calculate_control_point(start_point, end_point)

        # パスの作成
        if control_point:
            # 曲線的な接続
            path_d = (f"M {start_point[0]},{start_point[1]} "
                     f"Q {control_point[0]},{control_point[1]} "
                     f"{end_point[0]},{end_point[1]}")
        else:
            path_d = f"M {start_point[0]},{start_point[1]} L {end_point[0]},{end_point[1]}"

        # 矢印を描画
        arrow = self.dwg.path(
            d=path_d,
            stroke="black",
            fill="none",
            stroke_width="1.5",
            marker_end=self.arrow_marker()
        )
        self.dwg.add(arrow)

        # ラベルを追加(位置調整)
        if label:
            if label == 'なし':
                mid_x = (start_point[0] + end_point[0]) / 2
                mid_y = start_point[1] + 15
            else:
                mid_x = (start_point[0] + end_point[0]) / 2
                mid_y = (start_point[1] + end_point[1]) / 2 - 5
            
            text = self.dwg.text(
                label,
                insert=(mid_x, mid_y),
                text_anchor="middle",
                font_size="12px",
                font_weight="bold"
            )
            self.dwg.add(text)
    
    def save(self, filename: str):
        """SVGファイルを保存"""
        self.dwg.save()

def generate_advanced_diagram(config_file: str, output_file: str):
    """設定ファイルから図を生成"""
    # 設定ファイルの読み込み
    with open(config_file, 'r', encoding='utf-8') as f:
        config = yaml.safe_load(f)
    
    # 図の初期化
    diagram = AdvancedProcessDiagram()
    
    # ノードの位置を記録
    node_positions = {}
    
    def process_node(node_config: Dict, x: int, y: int) -> None:
        """ノードの種類に応じて適切なメソッドを呼び出す"""
        diagram.current_x = x
        diagram.current_y = y
        
        if node_config['type'] == 'process':
            pos = diagram.add_process_node(node_config['name'])
        elif node_config['type'] == 'decision':
            pos = diagram.add_decision_node(node_config['name'])
        elif node_config['type'] == 'subprocess':
            pos = diagram.add_subprocess(node_config['name'], 
                                      node_config.get('steps', []))
        
        node_positions[node_config['name']] = pos
    
    # ノードの配置
    for node in config['nodes']:
        process_node(node, node['x'], node['y'])
    
    # 矢印の追加
    for connection in config['connections']:
        start_pos = node_positions[connection['from']]
        end_pos = node_positions[connection['to']]
        diagram.add_arrow(start_pos, end_pos, connection.get('label', ''))
    
    # 図の保存
    diagram.save(output_file)

設定ファイルの作成

より複雑なプロセスを表現するYAML設定ファイルの例:

# advanced_process_config.yaml
nodes:
  - name: 受注受付
    type: process
    x: 50
    y: 50
  
  - name: 在庫確認
    type: decision
    x: 250
    y: 50
  
  - name: 出荷準備
    type: subprocess
    x: 450
    y: 50
    steps:
      - ピッキング
      - 梱包
      - 伝票作成
  
  - name: 発注処理
    type: process
    x: 200
    y: 150
  
  - name: 配送
    type: process
    x: 650
    y: 50

connections:
  - from: 受注受付
    to: 在庫確認
  
  - from: 在庫確認
    to: 出荷準備
    label: あり
  
  - from: 在庫確認
    to: 発注処理
    label: なし
  
  - from: 発注処理
    to: 出荷準備
  
  - from: 出荷準備
    to: 配送

使用例

if __name__ == "__main__":
    generate_advanced_diagram('advanced_process_config.yaml', 
                            'advanced_process.svg')

出力例

image.png

実装のポイント解説

1. 条件分岐ノードの実装

  • ダイヤモンド形状をSVGのパスで描画
  • 中央にテキストを配置
  • 分岐条件のラベル付き矢印に対応
  • 矢印の接続位置を条件によって調整

2. サブプロセスの表現

  • 通常のプロセスノードに小さな四角を追加
  • サブプロセスの詳細をYAMLで管理
  • 視覚的な区別を付けやすい設計

3. レイアウト制御の改善

  • 座標を明示的に指定可能
  • 垂直方向の配置にも対応
  • 複雑な接続パターンをサポート
  • ベジェ曲線による自然な接続

4. YAML設定の拡張

  • ノードタイプの追加(process/decision/subprocess)
  • 座標情報の追加
  • 接続情報のラベル対応

カスタマイズのポイント

  1. ノードのスタイル

    • 各ノードタイプの色や形状を変更可能
    • フォントサイズや種類の調整
    • 角の丸みやボーダーの設定
  2. レイアウト

    • spacingvertical_spacingで間隔を調整
    • 座標指定による自由な配置
    • ノードサイズの変更
  3. 矢印とラベル

    • 矢印の形状やスタイルをカスタマイズ
    • ラベルの位置を条件に応じて調整
    • 線の種類や太さを変更
    • ベジェ曲線のパラメータ調整

まとめ

前回の基本実装を拡張し、以下の機能を追加しました:

  • 条件分岐による複雑なフローの表現
  • サブプロセスによる階層的な表現
  • 条件に応じた矢印の接続位置調整
  • ベジェ曲線による自然な矢印の描画
  • 柔軟なレイアウト制御
  • 設定ファイルの拡張性向上

これにより、より実践的で見やすいビジネスプロセス図の作成が可能になりました。

参考リンク

クラス図

シーケンス図

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?