mermaid graphの複雑度を定量化するツールをつくった


# やりたいこと

- mermaid graphの複雑度を定量化したい
    - LangGraphのようなグラフ構造の処理を作る際の複雑度を品質保証の指針としたい
    - 出力するアウトプットの複雑度に対し、必要となる処理側の複雑度を把握したい

# ツールをつくってみた

- mdファイル（複数）をインプットに処理できるようにした
- tkinterを使ってGUIで動かせるようにしてみた
    - ファイルを選んで分析ボタンを押すだけ
    - 各種メトリクスが表示される
    - 総合複雑度のスコアはメトリクスに重みづけた自作指標


### 動作イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/d24bf83f-5766-2d8d-6062-4f9578185121.png)



### 実行コード

``` 
# 必要なライブラリのインストール
pip install tkinter numpy matplotlib networkx pandas

```


``` mermaid_workflow_analyzer.py

import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, messagebox
import re
from collections import defaultdict
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np 
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import logging
from typing import Optional, Dict, List, Set, Tuple
import os
import pandas as pd
from datetime import datetime


class WorkflowAnalyzer:
    def __init__(self):
        self.nodes: Set[str] = set()
        self.edges: List[Tuple[str, str]] = []
        self.subgraphs: Dict[str, Set[str]] = {}
        self.adjacency_list: Dict[str, List[str]] = defaultdict(list)
        self.graph: Optional[nx.DiGraph] = None
        self.raw_lines: List[str] = []

    def parse_mermaid_file(self, file_path: str) -> None:
        """Mermaidファイルを解析する"""
        try:
            if not os.path.exists(file_path):
                raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")
                
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
                self.raw_lines = content.split('\n')
            
            # コンテンツからグラフ定義部分を抽出
            lines = content.split('\n')
            graph_lines = []
            in_graph = False
            
            for line in lines:
                if 'graph' in line:
                    in_graph = True
                if in_graph:
                    graph_lines.append(line)
                    
            if not graph_lines:
                raise ValueError("グラフ定義が見つかりません")
                
            # データをクリア
            self.nodes.clear()
            self.edges.clear()
            self.subgraphs.clear()
            self.adjacency_list.clear()
            
            current_subgraph = None
            
            # 行ごとに解析
            for line in lines:
                line = line.strip()
                
                # サブグラフの開始を検出
                if 'subgraph' in line:
                    subgraph_match = re.search(r'subgraph\s+[""]?([^"\s]+)[""]?', line)
                    if subgraph_match:
                        current_subgraph = subgraph_match.group(1)
                        self.subgraphs[current_subgraph] = set()
                
                # サブグラフの終了を検出
                elif 'end' in line and current_subgraph:
                    current_subgraph = None
                
                # ノードの定義を検出
                elif '[' in line and ']' in line:
                    node_match = re.search(r'(\w+)\[[""]?([^]"]+)[""]?\]', line)
                    if node_match:
                        node_id = node_match.group(1)
                        self.nodes.add(node_id)
                        if current_subgraph:
                            self.subgraphs[current_subgraph].add(node_id)
                
                # エッジの定義を検出
                elif '-->' in line:
                    # 複数のエッジパターンに対応
                    edge_patterns = [
                        r'(\w+)\s*-->(?:\|[^|]*\|)?\s*(\w+)',  # 基本的な矢印
                        r'(\w+)\s*-.+?.->\s*(\w+)',  # ラベル付き点線
                        r'(\w+)\s*==+>\s*(\w+)',     # 太い矢印
                    ]
                    
                    for pattern in edge_patterns:
                        edge_matches = re.finditer(pattern, line)
                        for match in edge_matches:
                            source, target = match.groups()
                            if source and target:  # 両方のノードが存在する場合のみ
                                self.nodes.add(source)
                                self.nodes.add(target)
                                self.edges.append((source, target))
                                self.adjacency_list[source].append(target)
            
            if not self.nodes:
                raise ValueError("有効なノードが見つかりません")
                
            self._create_networkx_graph()
            logging.info(f"ファイルの解析が完了しました: {file_path}")
            logging.info(f"検出されたノード数: {len(self.nodes)}")
            logging.info(f"検出されたエッジ数: {len(self.edges)}")
            logging.info(f"検出されたサブグラフ数: {len(self.subgraphs)}")
            
        except Exception as e:
            logging.error(f"ファイル解析中にエラーが発生しました: {str(e)}")
            raise

    def _create_networkx_graph(self) -> None:
        """NetworkXグラフの作成"""
        self.graph = nx.DiGraph()
        
        # ノードの追加（サブグラフ情報付き）
        for node in self.nodes:
            subgraph_membership = []
            for subgraph_name, nodes in self.subgraphs.items():
                if node in nodes:
                    subgraph_membership.append(subgraph_name)
            self.graph.add_node(node, subgraphs=subgraph_membership)
        
        # エッジの追加
        self.graph.add_edges_from(self.edges)

    def _calculate_node_count(self) -> int:
        """ノード数を計算"""
        return len(self.nodes)

    def _calculate_edge_count(self) -> int:
        """エッジ数を計算"""
        return len(self.edges)

    def _calculate_density(self) -> float:
        """グラフの密度を計算"""
        n = len(self.nodes)
        if n <= 1:
            return 0
        max_edges = n * (n - 1)
        return len(self.edges) / max_edges if max_edges > 0 else 0

    def _calculate_avg_degree(self) -> float:
        """平均次数を計算"""
        return 2 * len(self.edges) / len(self.nodes) if self.nodes else 0

    def _calculate_max_depth(self) -> int:
        """最大深さを計算"""
        def dfs(node: str, visited: Set[str], depth: int) -> int:
            if node in visited:
                return depth
            visited.add(node)
            max_depth = depth
            for neighbor in self.adjacency_list[node]:
                child_depth = dfs(neighbor, visited.copy(), depth + 1)
                max_depth = max(max_depth, child_depth)
            return max_depth

        start_nodes = [node for node in self.nodes if not any(edge[1] == node for edge in self.edges)]
        if not start_nodes:
            return 0

        max_depth = 0
        for start in start_nodes:
            depth = dfs(start, set(), 0)
            max_depth = max(max_depth, depth)
        return max_depth

    def _calculate_cyclomatic_complexity(self) -> int:
        """循環的複雑度を計算
        McCabeの循環的複雑度の計算式: E - N + 2P
        E: エッジ数
        N: ノード数
        P: 連結成分の数（独立したサブグラフの数）
        """
        try:
            if not self.graph:
                return 1
            
            # 連結成分（独立したサブグラフ）の数を計算
            connected_components = nx.number_weakly_connected_components(self.graph)
            
            # エッジ数とノード数を取得
            edges = len(self.edges)
            nodes = len(self.nodes)
            
            # 循環的複雑度の計算
            complexity = edges - nodes + (2 * connected_components)
            
            # 最小値は1を保証
            return max(1, complexity)
            
        except Exception as e:
            logging.error(f"循環的複雑度の計算中にエラーが発生しました: {str(e)}")
            return 1

    def _calculate_branching_factor(self) -> float:
        """分岐係数を計算（平均出次数）"""
        if not self.nodes:
            return 0
        total_out_degree = sum(len(children) for children in self.adjacency_list.values())
        return total_out_degree / len(self.nodes)

    def _calculate_structural_complexity(self) -> float:
        """構造的複雑度を計算"""
        return len(self.edges) * self._calculate_branching_factor()

    def _calculate_modularity(self) -> float:
        """モジュール性を計算"""
        if not self.subgraphs:
            return 0
        
        # サブグラフ内部のエッジ数と外部のエッジ数の比率を計算
        internal_edges = 0
        external_edges = 0
        
        for edge in self.edges:
            source, target = edge
            is_internal = False
            for subgraph_nodes in self.subgraphs.values():
                if source in subgraph_nodes and target in subgraph_nodes:
                    internal_edges += 1
                    is_internal = True
                    break
            if not is_internal:
                external_edges += 1
                
        total_edges = internal_edges + external_edges
        return internal_edges / total_edges if total_edges > 0 else 0

    def _calculate_connectivity(self) -> float:
        """接続性を計算"""
        try:
            return nx.average_node_connectivity(self.graph)
        except:
            return 0

    def _calculate_cluster_coefficient(self) -> float:
        """クラスタ係数を計算"""
        try:
            return nx.average_clustering(self.graph)
        except:
            return 0

    def _calculate_loc_metrics(self) -> Dict[str, int]:
        """コードの行数に関連するメトリクスを計算"""
        if not self.raw_lines:
            return {
                'total_loc': 0,
                'effective_loc': 0,
                'comment_loc': 0,
                'blank_loc': 0
            }

        total_loc = len(self.raw_lines)
        comment_loc = sum(1 for line in self.raw_lines if line.strip().startswith('%'))
        blank_loc = sum(1 for line in self.raw_lines if not line.strip())
        effective_loc = total_loc - comment_loc - blank_loc

        return {
            'total_loc': total_loc,
            'effective_loc': effective_loc,
            'comment_loc': comment_loc,
            'blank_loc': blank_loc
        }

    def _calculate_overall_complexity(self, metrics: Dict) -> float:
        """総合複雑度指標を計算"""
        weights = {
            'cyclomatic': 0.25,    # 循環的複雑度の重み
            'size': 0.20,          # サイズ要素の重み
            'structure': 0.20,     # 構造要素の重み
            'modularity': -0.15,   # モジュール性の重み（負の係数）
            'loc': 0.20            # コードサイズの重み
        }

        # サイズ要素の正規化（0-1のスケールに）
        size_factor = (metrics['node_count'] + metrics['edge_count']) / 100
        size_factor = min(1.0, size_factor)  # 1.0を上限とする

        # 構造要素の計算（深さと分岐を考慮）
        structure_factor = (
            metrics['max_depth'] * metrics['branching_factor'] * 
            metrics['density']
        ) ** 0.5  # ルートを取ることで極端な値を抑制

        # コードサイズ要素の正規化
        loc_factor = metrics['effective_loc'] / 500  # 500行を基準に正規化
        loc_factor = min(1.0, loc_factor)

        # 総合複雑度の計算
        overall_complexity = (
            weights['cyclomatic'] * (metrics['cyclomatic_complexity'] / 10) +
            weights['size'] * size_factor +
            weights['structure'] * min(1.0, structure_factor) +
            weights['modularity'] * (1 - metrics['modularity']) +
            weights['loc'] * loc_factor
        )

        # 0-100のスケールに変換
        overall_complexity = max(0, min(100, overall_complexity * 100))
        
        return round(overall_complexity, 2)

    def calculate_metrics(self) -> Dict:
        """ワークフローの複雑度メトリクスを計算"""
        try:
            if not self.graph:
                raise ValueError("グラフが初期化されていません")

            # 各種メトリクスを計算
            metrics = {
                'node_count': self._calculate_node_count(),
                'edge_count': self._calculate_edge_count(),
                'density': self._calculate_density(),
                'avg_degree': self._calculate_avg_degree(),
                'max_depth': self._calculate_max_depth(),
                'cyclomatic_complexity': self._calculate_cyclomatic_complexity(),
                'branching_factor': self._calculate_branching_factor(),
                'structural_complexity': self._calculate_structural_complexity(),
                'subgraph_count': len(self.subgraphs),
                'connected_components': nx.number_weakly_connected_components(self.graph),
                'modularity': self._calculate_modularity(),
                'connectivity': self._calculate_connectivity(),
                'cluster_coefficient': self._calculate_cluster_coefficient()
            }

            # LOCメトリクスを追加
            loc_metrics = self._calculate_loc_metrics()
            metrics.update(loc_metrics)

            # 総合複雑度を計算
            metrics['overall_complexity'] = self._calculate_overall_complexity(metrics)

            # メトリクスの妥当性チェック
            self._validate_metrics(metrics)
            
            return metrics
            
        except Exception as e:
            logging.error(f"メトリクス計算中にエラーが発生しました: {str(e)}")
            raise

    def _validate_metrics(self, metrics: Dict) -> None:
        """メトリクスの値が妥当な範囲内かチェック"""
        validations = {
            'node_count': lambda x: x >= 0,
            'edge_count': lambda x: x >= 0,
            'density': lambda x: 0 <= x <= 1,
            'avg_degree': lambda x: x >= 0,
            'max_depth': lambda x: x >= 0,
            'cyclomatic_complexity': lambda x: x >= 1,
            'branching_factor': lambda x: x >= 0,
            'structural_complexity': lambda x: x >= 0,
            'subgraph_count': lambda x: x >= 0,
            'connected_components': lambda x: x >= 1,
            'modularity': lambda x: 0 <= x <= 1,
            'connectivity': lambda x: x >= 0,
            'cluster_coefficient': lambda x: 0 <= x <= 1,
            'total_loc': lambda x: x >= 0,
            'effective_loc': lambda x: x >= 0,
            'comment_loc': lambda x: x >= 0,
            'blank_loc': lambda x: x >= 0,
            'overall_complexity': lambda x: 0 <= x <= 100
        }
        
        for metric, validator in validations.items():
            if metric in metrics:
                value = metrics[metric]
                if not validator(value):
                    logging.warning(f"メトリクス {metric} の値 {value} が想定範囲外です")
                    # 値の補正
                    if metric in ['density', 'modularity', 'cluster_coefficient']:
                        metrics[metric] = max(0, min(1, value))
                    elif metric == 'cyclomatic_complexity':
                        metrics[metric] = max(1, value)
                    elif metric == 'overall_complexity':
                        metrics[metric] = max(0, min(100, value))
                    else:
                        metrics[metric] = max(0, value)

    def generate_csv_report(self) -> Dict:
        """CSV出力用の1行のレポートを生成"""
        metrics = self.calculate_metrics()
        return {
            'ノード数': metrics['node_count'],
            'エッジ数': metrics['edge_count'],
            'サブグラフ数': metrics['subgraph_count'],
            'グラフ密度': round(metrics['density'], 3),
            '平均次数': round(metrics['avg_degree'], 2),
            '最大深さ': metrics['max_depth'],
            '循環的複雑度': metrics['cyclomatic_complexity'],
            '平均分岐係数': round(metrics['branching_factor'], 2),
            '構造的複雑度': round(metrics['structural_complexity'], 2),
            'モジュール性': round(metrics['modularity'], 3),
            '接続性': round(metrics['connectivity'], 3),
            'クラスタ係数': round(metrics['cluster_coefficient'], 3),
            '総行数': metrics['total_loc'],
            '実効行数': metrics['effective_loc'],
            'コメント行数': metrics['comment_loc'],
            '空行数': metrics['blank_loc'],
            '総合複雑度': metrics['overall_complexity']
        }

    def generate_detailed_report(self) -> str:
        """詳細な分析レポートを生成"""
        metrics = self.calculate_metrics()
        
        complexity_assessment = "低" if metrics['overall_complexity'] <= 30 else \
                              "中" if metrics['overall_complexity'] <= 60 else "高"
        
        report = [
            "# ワークフロー複雑度分析レポート",
            "",
            "## 基本メトリクス",
            f"- ノード数: {metrics['node_count']}",
            f"- エッジ数: {metrics['edge_count']}",
            f"- サブグラフ数: {metrics['subgraph_count']}",
            f"- グラフ密度: {metrics['density']:.3f}",
            "",
            "## 構造メトリクス",
            f"- 平均次数: {metrics['avg_degree']:.2f}",
            f"- 最大深さ: {metrics['max_depth']}",
            f"- 循環的複雑度: {metrics['cyclomatic_complexity']}",
            f"- 平均分岐係数: {metrics['branching_factor']:.2f}",
            "",
            "## 複雑度メトリクス",
            f"- 構造的複雑度: {metrics['structural_complexity']:.2f}",
            f"- モジュール性: {metrics['modularity']:.3f}",
            f"- 接続性: {metrics['connectivity']:.3f}",
            f"- クラスタ係数: {metrics['cluster_coefficient']:.3f}",
            "",
            "## コードメトリクス",
            f"- 総行数: {metrics['total_loc']}",
            f"- 実効行数: {metrics['effective_loc']}",
            f"- コメント行数: {metrics['comment_loc']}",
            f"- 空行数: {metrics['blank_loc']}",
            "",
            "## 総合評価",
            f"- 総合複雑度スコア: {metrics['overall_complexity']:.1f}/100",
            f"- 複雑度評価: {complexity_assessment}",
            "",
            "## サブグラフ分析"
        ]
        
        # サブグラフごとの詳細情報を追加
        for subgraph_name, nodes in self.subgraphs.items():
            report.extend([
                f"\n### {subgraph_name}",
                f"- ノード数: {len(nodes)}",
                f"- 含まれるノード: {', '.join(sorted(nodes))}"
            ])
            
        # 改善推奨事項の追加
        report.extend([
            "",
            "## 改善推奨事項"
        ])
        
        if metrics['cyclomatic_complexity'] > 10:
            report.append("- 循環的複雑度が高いため、フローの単純化を検討してください")
        if metrics['modularity'] < 0.3:
            report.append("- モジュール性が低いため、機能のグループ化を検討してください")
        if metrics['density'] > 0.7:
            report.append("- グラフ密度が高いため、依存関係の見直しを検討してください")
        if metrics['max_depth'] > 7:
            report.append("- フローの深さが深いため、階層構造の見直しを検討してください")
            
        return "\n".join(report)

class ResultTable(ttk.Treeview):
    """分析結果表示用のカスタムTreeviewウィジェット"""
    def __init__(self, parent, columns):
        super().__init__(parent, columns=columns, show='headings')
        
        # 列の設定
        for col in columns:
            self.heading(col, text=col, anchor=tk.W)
            # 数値列は右寄せ、テキスト列は左寄せ
            align = tk.E if col not in ['ファイル名'] else tk.W
            self.column(col, anchor=align, width=100)
            
        # スクロールバーの追加
        self.scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.yview)
        self.configure(yscrollcommand=self.scrollbar.set)
        
        # 選択スタイルの設定
        self.style = ttk.Style()
        self.style.configure('Treeview', rowheight=25)
        
    def update_data(self, data: pd.DataFrame):
        """データフレームの内容でテーブルを更新"""
        # 既存の項目をクリア
        for item in self.get_children():
            self.delete(item)
            
        # 新しいデータを挿入
        for _, row in data.iterrows():
            values = [row[col] for col in self.cget('columns')]
            self.insert('', tk.END, values=values)
            
    def get_data(self) -> pd.DataFrame:
        """現在の表示内容をDataFrameとして取得"""
        data = []
        columns = self.cget('columns')
        for item in self.get_children():
            values = self.item(item)['values']
            data.append(dict(zip(columns, values)))
        return pd.DataFrame(data)

class WorkflowAnalyzerGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("ワークフロー分析ツール")
        self.analyzer = WorkflowAnalyzer()
        self.file_paths = []
        self.results_df = None
        self.setup_gui()
        
    def setup_gui(self):
        """GUIの初期設定"""
        # スタイルの設定
        self.style = ttk.Style()
        self.style.configure('Header.TLabel', font=('Helvetica', 12, 'bold'))
        
        # メインフレーム
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # ツールバー
        toolbar = ttk.Frame(main_frame)
        toolbar.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # ファイル操作ボタン
        file_buttons = ttk.LabelFrame(toolbar, text="ファイル操作", padding="5")
        file_buttons.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(file_buttons, text="ファイル追加", command=self.add_files).pack(side=tk.LEFT, padx=2)
        ttk.Button(file_buttons, text="選択クリア", command=self.clear_files).pack(side=tk.LEFT, padx=2)
        
        # 分析ボタン
        analysis_buttons = ttk.LabelFrame(toolbar, text="分析", padding="5")
        analysis_buttons.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(analysis_buttons, text="一括分析", command=self.analyze_files).pack(side=tk.LEFT, padx=2)
        ttk.Button(analysis_buttons, text="メトリクス説明", command=self.show_metrics_description).pack(side=tk.LEFT, padx=2)
        
        # 出力ボタン
        export_buttons = ttk.LabelFrame(toolbar, text="出力", padding="5")
        export_buttons.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(export_buttons, text="CSV出力", command=self.export_csv).pack(side=tk.LEFT, padx=2)
        
        # 選択ファイル一覧
        file_frame = ttk.LabelFrame(main_frame, text="選択されたファイル", padding="5")
        file_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        self.file_list = tk.Listbox(file_frame, height=5)
        self.file_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        file_scroll = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_list.yview)
        file_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_list.configure(yscrollcommand=file_scroll.set)
        
        # 分析結果表示
        result_frame = ttk.LabelFrame(main_frame, text="分析結果", padding="5")
        result_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 結果テーブルの列定義
        columns = [
            'ファイル名',
            'ノード数',
            'エッジ数',
            'サブグラフ数',
            'グラフ密度',
            '平均次数',
            '最大深さ',
            '循環的複雑度',
            '平均分岐係数',
            '構造的複雑度',
            'モジュール性',
            '接続性',
            'クラスタ係数',
            '総行数',
            '実効行数',
            'コメント行数',
            '空行数',
            '総合複雑度'
        ]
        
        self.result_table = ResultTable(result_frame, columns)
        self.result_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.result_table.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # ステータスバー
        self.status_var = tk.StringVar()
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
        
        # グリッドの設定
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(2, weight=1)
        
    def add_files(self):
        """ファイル選択ダイアログを表示"""
        files = filedialog.askopenfilenames(
            filetypes=[("Markdown files", "*.md"), ("All files", "*.*")]
        )
        for file in files:
            if file not in self.file_paths:
                self.file_paths.append(file)
                self.file_list.insert(tk.END, os.path.basename(file))
        self.status_var.set(f"{len(files)}個のファイルが追加されました")
        
    def clear_files(self):
        """選択されたファイルをクリア"""
        self.file_paths = []
        self.file_list.delete(0, tk.END)
        self.result_table.update_data(pd.DataFrame())
        self.status_var.set("ファイル選択をクリアしました")
        
    def analyze_files(self):
        """選択されたファイルを一括分析"""
        if not self.file_paths:
            messagebox.showwarning("警告", "ファイルが選択されていません")
            return
            
        try:
            results = []
            for file_path in self.file_paths:
                self.analyzer = WorkflowAnalyzer()
                self.analyzer.parse_mermaid_file(file_path)
                result = self.analyzer.generate_csv_report()
                result['ファイル名'] = os.path.basename(file_path)
                results.append(result)
            
            # 結果をDataFrameに変換して保存
            self.results_df = pd.DataFrame(results)
            
            # テーブルを更新
            self.result_table.update_data(self.results_df)
            
            self.status_var.set("分析が完了しました")
            
        except Exception as e:
            messagebox.showerror("エラー", f"分析中にエラーが発生しました: {str(e)}")
            self.status_var.set("エラーが発生しました")
            
    def export_csv(self):
        """分析結果をCSVファイルとして保存"""
        if self.results_df is None or self.results_df.empty:
            messagebox.showwarning("警告", "エクスポートする分析結果がありません")
            return
            
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            initial_file = f"workflow_analysis_{timestamp}.csv"
            
            file_path = filedialog.asksaveasfilename(
                defaultextension=".csv",
                filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
                initialfile=initial_file
            )
            
            if file_path:
                self.results_df.to_csv(file_path, index=False, encoding='utf-8-sig')
                self.status_var.set(f"分析結果を{os.path.basename(file_path)}に保存しました")
                
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの保存中にエラーが発生しました: {str(e)}")
            
    def show_metrics_description(self):
        """メトリクスの説明を表示"""
        description_window = tk.Toplevel(self.root)
        description_window.title("メトリクスの説明")
        description_window.geometry("800x600")

        # スタイル設定
        style = ttk.Style()
        style.configure("Heading.TLabel", font=('Helvetica', 12, 'bold'))
        style.configure("Category.TLabel", font=('Helvetica', 11, 'bold'), foreground='#2C3E50')
        style.configure("Description.TLabel", font=('Helvetica', 10), wraplength=700)

        # メインフレーム
        main_frame = ttk.Frame(description_window, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # スクロール可能なキャンバスを作成
        canvas = tk.Canvas(main_frame)
        scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
        scrollable_frame = ttk.Frame(canvas)

        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )

        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)

        # メトリクスの説明を追加
        descriptions = {
            "基本メトリクス": {
                "ノード数": "ワークフロー内の総ステップ数。各処理や判断のポイントを表します。",
                "エッジ数": "ステップ間の接続数。フローの遷移を表す矢印の数です。",
                "サブグラフ数": "独立した処理グループの数。機能的にまとまった部分を示します。",
                "グラフ密度": "可能な接続数に対する実際の接続数の割合（0-1）。\n値が高いほど複雑な依存関係があることを示します。",
            },
            "構造メトリクス": {
                "平均次数": "1つのステップが平均して持つ接続数。\n値が高いほど複雑な分岐構造を持つことを示します。",
                "最大深さ": "開始から終了までの最長パスの長さ。\n処理の階層の深さを表します。",
                "循環的複雑度": "McCabeの循環的複雑度（V(G) = E - N + 2P）。\n分岐やループの複雑さを示す指標です。\n- 1-10: シンプル\n- 11-20: やや複雑\n- 21以上: 非常に複雑",
                "平均分岐係数": "1つのステップから平均して何個の次のステップに進むか。\n意思決定の複雑さを示します。",
            },
            "複雑度メトリクス": {
                "構造的複雑度": "エッジ数と分岐係数から算出される全体的な構造の複雑さ。\n値が高いほどメンテナンスが困難になる可能性があります。",
                "モジュール性": "サブグラフ内の結合度と凝集度のバランス（0-1）。\n1に近いほど良い構造化ができていることを示します。",
                "接続性": "ワークフローの各部分がどの程度密接に接続されているか。\n値が高いほど強い依存関係があることを示します。",
                "クラスタ係数": "ステップのグループ化傾向を示す指標（0-1）。\n1に近いほど明確な機能グループ化ができていることを示します。",
            },
            "コードメトリクス": {
                "総行数": "ファイル全体の行数。コメントと空行を含む全行数です。",
                "実効行数": "コメントと空行を除いた実際のコード行数。\n実質的なコードの量を示します。",
                "コメント行数": "コメント行の数。ドキュメント化の程度を示します。",
                "空行数": "空行の数。コードの可読性に関連します。",
            },
            "総合評価": {
                "総合複雑度": "様々な複雑さの指標を統合した0-100の評価スコア。\n以下の要素を重み付けして算出します：\n- 循環的複雑度（25%）\n- グラフサイズ（20%）\n- 構造的特性（20%）\n- モジュール性（-15%）\n- コードサイズ（20%）\n\n評価基準：\n- 0-20: シンプルで管理が容易\n- 21-40: 適度な複雑さ\n- 41-60: やや複雑、注意が必要\n- 61-80: 複雑、リファクタリングを検討\n- 81-100: 非常に複雑、リファクタリングを推奨",
            }
        }

        row = 0
        for category, metrics in descriptions.items():
            # カテゴリヘッダー
            category_label = ttk.Label(
                scrollable_frame, 
                text=category,
                style="Category.TLabel"
            )
            category_label.grid(row=row, column=0, sticky="w", pady=(20, 10))
            row += 1

            # 各メトリクスの説明
            for metric, description in metrics.items():
                # メトリクス名
                metric_label = ttk.Label(
                    scrollable_frame,
                    text=f"• {metric}:",
                    style="Heading.TLabel"
                )
                metric_label.grid(row=row, column=0, sticky="w", padx=(20, 0), pady=(5, 0))
                
                # 説明
                desc_label = ttk.Label(
                    scrollable_frame,
                    text=description,
                    style="Description.TLabel"
                )
                desc_label.grid(row=row+1, column=0, sticky="w", padx=(40, 20), pady=(0, 10))
                row += 2

        # パック設定
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        # キーボードバインディング
        description_window.bind("<Escape>", lambda e: description_window.destroy())
        description_window.bind("<MouseWheel>", lambda e: canvas.yview_scroll(int(-1*(e.delta/120)), "units"))
        
        # ウィンドウの最小サイズを設定
        description_window.update()
        description_window.minsize(
            description_window.winfo_width(),
            description_window.winfo_height()
        )



def main():
    try:
        root = tk.Tk()
        app = WorkflowAnalyzerGUI(root)
        root.mainloop()
    except Exception as e:
        logging.critical(f"アプリケーション実行中に重大なエラーが発生しました: {str(e)}")
        messagebox.showerror("致命的なエラー", "アプリケーションの起動に失敗しました")

if __name__ == "__main__":
    main()

```