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

## はじめに

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

https://qiita.com/Tadataka_Takahashi/items/8bea4e5fdda481773a96

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/e4aa1806-9fd7-c512-481e-3414d342730b.png)


## 環境準備

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

```bash
pip install svgwrite pyyaml
```

## 実装

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

```python
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設定ファイルの例:

```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: 配送
```

## 使用例

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

## 出力例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4de1e583-c6b7-dfd7-3885-ee1498662024.png)

## 実装のポイント解説

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

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

### 3. レイアウト制御の改善
- 座標を明示的に指定可能
- 垂直方向の配置にも対応
- 複雑な接続パターンをサポート
- ベジェ曲線による自然な接続

### 4. YAML設定の拡張
- ノードタイプの追加（process/decision/subprocess）
- 座標情報の追加
- 接続情報のラベル対応

## カスタマイズのポイント

1. ノードのスタイル
   - 各ノードタイプの色や形状を変更可能
   - フォントサイズや種類の調整
   - 角の丸みやボーダーの設定

2. レイアウト
   - `spacing`や`vertical_spacing`で間隔を調整
   - 座標指定による自由な配置
   - ノードサイズの変更

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

## まとめ

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

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

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

## 参考リンク

- [svgwrite documentation](https://svgwrite.readthedocs.io/)
- [PyYAML documentation](https://pyyaml.org/)
- [BPMN 2.0 仕様](https://www.omg.org/spec/BPMN/2.0/)

## クラス図

```mermaid
classDiagram
    class AdvancedProcessDiagram {
        -dwg: Drawing
        -node_width: int = 120
        -node_height: int = 60
        -spacing: int = 100
        -current_x: int = 50
        -current_y: int = 50
        -vertical_spacing: int = 80
        +__init__(width: int, height: int)
        +add_process_node(text: str, fill: str) tuple
        +add_decision_node(text: str) tuple
        +add_subprocess(text: str, subprocess_steps: List[str]) tuple
        -arrow_marker() str
        +add_arrow(start: tuple, end: tuple, label: str)
        +save(filename: str)
    }
    
    class ProcessNode {
        <<interface>>
        +position: tuple
        +render()
    }
    
    class DecisionNode {
        +position: tuple
        +render()
    }
    
    class SubprocessNode {
        +position: tuple
        +steps: List[str]
        +render()
    }

    ProcessNode <|.. DecisionNode
    ProcessNode <|.. SubprocessNode
    AdvancedProcessDiagram ..> ProcessNode : creates
```

## シーケンス図

```mermaid
sequenceDiagram
    participant メイン処理
    participant 図生成処理
    participant プロセス図
    participant YAML処理
    participant SVG描画

    メイン処理->>YAML処理: 設定ファイル読み込み
    activate YAML処理
    YAML処理-->>メイン処理: 設定データ
    deactivate YAML処理

    メイン処理->>図生成処理: プロセス図生成開始
    activate 図生成処理
    
    図生成処理->>プロセス図: プロセス図オブジェクト作成
    activate プロセス図

    loop 設定の各ノードに対して
        図生成処理->>プロセス図: ノード処理実行
        activate プロセス図
        alt 通常プロセスの場合
            プロセス図->>SVG描画: プロセスノード追加
        else 条件分岐の場合
            プロセス図->>SVG描画: 分岐ノード追加
        else サブプロセスの場合
            プロセス図->>SVG描画: サブプロセスノード追加
        end
        プロセス図-->>図生成処理: ノード位置情報
        deactivate プロセス図
    end

    loop 設定の各接続に対して
        図生成処理->>プロセス図: 矢印追加
        activate プロセス図
        プロセス図->>SVG描画: 矢印パス描画
        プロセス図->>SVG描画: ラベル追加
        deactivate プロセス図
    end

    図生成処理->>プロセス図: SVGファイル保存
    プロセス図->>SVG描画: SVG出力

    SVG描画-->>図生成処理: 保存完了
    図生成処理-->>メイン処理: 処理完了

    deactivate 図生成処理
    deactivate プロセス図
```