Update wbs_diagram_generator.py
Browse files- wbs_diagram_generator.py +64 -55
wbs_diagram_generator.py
CHANGED
|
@@ -2,9 +2,9 @@ import graphviz
|
|
| 2 |
import json
|
| 3 |
from tempfile import NamedTemporaryFile
|
| 4 |
import os
|
| 5 |
-
from graph_generator_utils import add_nodes_and_edges
|
| 6 |
|
| 7 |
-
def generate_wbs_diagram(json_input: str) -> str:
|
| 8 |
"""
|
| 9 |
Generates a Work Breakdown Structure (WBS) Diagram from JSON input.
|
| 10 |
|
|
@@ -23,17 +23,23 @@ def generate_wbs_diagram(json_input: str) -> str: # Removed base_color parameter
|
|
| 23 |
"id": "phase_prep",
|
| 24 |
"label": "1. Preparation",
|
| 25 |
"tasks": [
|
| 26 |
-
{"id": "task_vision", "label": "1.1. Identify Vision"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
]
|
| 38 |
}
|
| 39 |
]
|
|
@@ -69,13 +75,12 @@ def generate_wbs_diagram(json_input: str) -> str: # Removed base_color parameter
|
|
| 69 |
data['project_title'],
|
| 70 |
shape='box',
|
| 71 |
style='filled,rounded',
|
| 72 |
-
fillcolor=base_color,
|
| 73 |
fontcolor='white',
|
| 74 |
fontsize='18'
|
| 75 |
)
|
| 76 |
|
| 77 |
-
#
|
| 78 |
-
# This ensures the gradient works correctly with the hardcoded base_color
|
| 79 |
def get_gradient_color(depth, base_hex_color, lightening_factor=0.12):
|
| 80 |
base_r = int(base_hex_color[1:3], 16)
|
| 81 |
base_g = int(base_hex_color[3:5], 16)
|
|
@@ -88,8 +93,6 @@ def generate_wbs_diagram(json_input: str) -> str: # Removed base_color parameter
|
|
| 88 |
return f'#{min(255, current_r):02x}{min(255, current_g):02x}{min(255, current_b):02x}'
|
| 89 |
|
| 90 |
def get_font_color_for_background(depth, base_hex_color, lightening_factor=0.12):
|
| 91 |
-
# Calculate brightness/lightness of the node color at this depth
|
| 92 |
-
# and return black/white for text accordingly
|
| 93 |
base_r = int(base_hex_color[1:3], 16)
|
| 94 |
base_g = int(base_hex_color[3:5], 16)
|
| 95 |
base_b = int(base_hex_color[5:7], 16)
|
|
@@ -97,23 +100,53 @@ def generate_wbs_diagram(json_input: str) -> str: # Removed base_color parameter
|
|
| 97 |
current_g = base_g + (255 - base_g) * depth * lightening_factor
|
| 98 |
current_b = base_b + (255 - base_b) * depth * lightening_factor
|
| 99 |
|
| 100 |
-
# Simple luminance check (ITU-R BT.709 coefficients)
|
| 101 |
luminance = (0.2126 * current_r + 0.7152 * current_g + 0.0722 * current_b) / 255
|
| 102 |
return 'white' if luminance < 0.5 else 'black'
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
for phase in data['phases']:
|
| 107 |
phase_id = phase.get('id')
|
| 108 |
phase_label = phase.get('label')
|
| 109 |
-
|
| 110 |
-
|
| 111 |
if not all([phase_id, phase_label]):
|
| 112 |
-
raise ValueError(f"Invalid phase: {phase}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
phase_fill_color = get_gradient_color(current_depth, base_color)
|
| 115 |
-
phase_font_color = get_font_color_for_background(current_depth, base_color)
|
| 116 |
-
|
| 117 |
dot.node(
|
| 118 |
phase_id,
|
| 119 |
phase_label,
|
|
@@ -121,38 +154,14 @@ def generate_wbs_diagram(json_input: str) -> str: # Removed base_color parameter
|
|
| 121 |
style='filled,rounded',
|
| 122 |
fillcolor=phase_fill_color,
|
| 123 |
fontcolor=phase_font_color,
|
| 124 |
-
fontsize=
|
| 125 |
)
|
| 126 |
-
dot.edge('project_root', phase_id, color='#4a4a4a', arrowhead='none')
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
|
|
|
| 130 |
|
| 131 |
-
task_nodes_in_phase = []
|
| 132 |
-
for task in tasks:
|
| 133 |
-
task_id = task.get('id')
|
| 134 |
-
task_label = task.get('label')
|
| 135 |
-
if not all([task_id, task_label]):
|
| 136 |
-
raise ValueError(f"Invalid task: {task}")
|
| 137 |
-
|
| 138 |
-
task_fill_color = get_gradient_color(task_depth, base_color)
|
| 139 |
-
task_font_color = get_font_color_for_background(task_depth, base_color)
|
| 140 |
-
|
| 141 |
-
dot.node(
|
| 142 |
-
task_id,
|
| 143 |
-
task_label,
|
| 144 |
-
shape='box',
|
| 145 |
-
style='filled,rounded',
|
| 146 |
-
fillcolor=task_fill_color,
|
| 147 |
-
fontcolor=task_font_color,
|
| 148 |
-
fontsize=str(task_font_size)
|
| 149 |
-
)
|
| 150 |
-
dot.edge(phase_id, task_id, color='#4a4a4a', arrowhead='none') # Connect task to phase
|
| 151 |
-
task_nodes_in_phase.append(task_id)
|
| 152 |
-
|
| 153 |
-
# Use subgraph to enforce vertical alignment for tasks within a phase
|
| 154 |
-
if task_nodes_in_phase: # Only create subgraph if there are tasks
|
| 155 |
-
dot.subgraph(name=f'cluster_{phase_id}')
|
| 156 |
# Save to temporary file
|
| 157 |
with NamedTemporaryFile(delete=False, suffix='.png') as tmp:
|
| 158 |
dot.render(tmp.name, format='png', cleanup=True)
|
|
|
|
| 2 |
import json
|
| 3 |
from tempfile import NamedTemporaryFile
|
| 4 |
import os
|
| 5 |
+
from graph_generator_utils import add_nodes_and_edges # Keeping this import for consistency, though WBS will use its own recursive logic.
|
| 6 |
|
| 7 |
+
def generate_wbs_diagram(json_input: str) -> str:
|
| 8 |
"""
|
| 9 |
Generates a Work Breakdown Structure (WBS) Diagram from JSON input.
|
| 10 |
|
|
|
|
| 23 |
"id": "phase_prep",
|
| 24 |
"label": "1. Preparation",
|
| 25 |
"tasks": [
|
| 26 |
+
{"id": "task_vision", "label": "1.1. Identify Vision",
|
| 27 |
+
"subtasks": [
|
| 28 |
+
{"id": "subtask_1_1_1", "label": "1.1.1. Problem Definition",
|
| 29 |
+
"sub_subtasks": [
|
| 30 |
+
{"id": "ss_task_1_1_1_1", "label": "1.1.1.1. Req. Analysis",
|
| 31 |
+
"sub_sub_subtasks": [
|
| 32 |
+
{"id": "sss_task_1_1_1_1_1", "label": "1.1.1.1.1. User Stories",
|
| 33 |
+
"final_level_tasks": [
|
| 34 |
+
{"id": "ft_1_1_1_1_1_1", "label": "1.1.1.1.1.1. Interview Users"}
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
]
|
| 38 |
+
}
|
| 39 |
+
]
|
| 40 |
+
}
|
| 41 |
+
]
|
| 42 |
+
}
|
| 43 |
]
|
| 44 |
}
|
| 45 |
]
|
|
|
|
| 75 |
data['project_title'],
|
| 76 |
shape='box',
|
| 77 |
style='filled,rounded',
|
| 78 |
+
fillcolor=base_color,
|
| 79 |
fontcolor='white',
|
| 80 |
fontsize='18'
|
| 81 |
)
|
| 82 |
|
| 83 |
+
# Helper for color and font based on depth for WBS
|
|
|
|
| 84 |
def get_gradient_color(depth, base_hex_color, lightening_factor=0.12):
|
| 85 |
base_r = int(base_hex_color[1:3], 16)
|
| 86 |
base_g = int(base_hex_color[3:5], 16)
|
|
|
|
| 93 |
return f'#{min(255, current_r):02x}{min(255, current_g):02x}{min(255, current_b):02x}'
|
| 94 |
|
| 95 |
def get_font_color_for_background(depth, base_hex_color, lightening_factor=0.12):
|
|
|
|
|
|
|
| 96 |
base_r = int(base_hex_color[1:3], 16)
|
| 97 |
base_g = int(base_hex_color[3:5], 16)
|
| 98 |
base_b = int(base_hex_color[5:7], 16)
|
|
|
|
| 100 |
current_g = base_g + (255 - base_g) * depth * lightening_factor
|
| 101 |
current_b = base_b + (255 - base_b) * depth * lightening_factor
|
| 102 |
|
|
|
|
| 103 |
luminance = (0.2126 * current_r + 0.7152 * current_g + 0.0722 * current_b) / 255
|
| 104 |
return 'white' if luminance < 0.5 else 'black'
|
| 105 |
|
| 106 |
+
def _add_wbs_nodes_recursive(parent_id, current_level_tasks, current_depth):
|
| 107 |
+
for task_data in current_level_tasks:
|
| 108 |
+
task_id = task_data.get('id')
|
| 109 |
+
task_label = task_data.get('label')
|
| 110 |
+
|
| 111 |
+
if not all([task_id, task_label]):
|
| 112 |
+
raise ValueError(f"Invalid task data at depth {current_depth}: {task_data}")
|
| 113 |
+
|
| 114 |
+
node_fill_color = get_gradient_color(current_depth, base_color)
|
| 115 |
+
node_font_color = get_font_color_for_background(current_depth, base_color)
|
| 116 |
+
font_size = max(9, 14 - (current_depth * 2))
|
| 117 |
|
| 118 |
+
dot.node(
|
| 119 |
+
task_id,
|
| 120 |
+
task_label,
|
| 121 |
+
shape='box',
|
| 122 |
+
style='filled,rounded',
|
| 123 |
+
fillcolor=node_fill_color,
|
| 124 |
+
fontcolor=node_font_color,
|
| 125 |
+
fontsize=str(font_size)
|
| 126 |
+
)
|
| 127 |
+
dot.edge(parent_id, task_id, color='#4a4a4a', arrowhead='none')
|
| 128 |
+
|
| 129 |
+
# Recursively call for next level of tasks (subtasks, sub_subtasks, etc.)
|
| 130 |
+
# This handles arbitrary nested keys like 'subtasks', 'sub_subtasks', 'final_level_tasks'
|
| 131 |
+
next_level_keys = ['tasks', 'subtasks', 'sub_subtasks', 'sub_sub_subtasks', 'final_level_tasks']
|
| 132 |
+
for key_idx, key in enumerate(next_level_keys):
|
| 133 |
+
if key in task_data and isinstance(task_data[key], list):
|
| 134 |
+
_add_wbs_nodes_recursive(task_id, task_data[key], current_depth + 1)
|
| 135 |
+
break # Only process the first found sub-level key
|
| 136 |
+
|
| 137 |
+
# Process phases (level 1 from project_root)
|
| 138 |
+
phase_depth = 1
|
| 139 |
for phase in data['phases']:
|
| 140 |
phase_id = phase.get('id')
|
| 141 |
phase_label = phase.get('label')
|
| 142 |
+
|
|
|
|
| 143 |
if not all([phase_id, phase_label]):
|
| 144 |
+
raise ValueError(f"Invalid phase data: {phase}")
|
| 145 |
+
|
| 146 |
+
phase_fill_color = get_gradient_color(phase_depth, base_color)
|
| 147 |
+
phase_font_color = get_font_color_for_background(phase_depth, base_color)
|
| 148 |
+
font_size_phase = max(9, 14 - (phase_depth * 2))
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
dot.node(
|
| 151 |
phase_id,
|
| 152 |
phase_label,
|
|
|
|
| 154 |
style='filled,rounded',
|
| 155 |
fillcolor=phase_fill_color,
|
| 156 |
fontcolor=phase_font_color,
|
| 157 |
+
fontsize=str(font_size_phase)
|
| 158 |
)
|
| 159 |
+
dot.edge('project_root', phase_id, color='#4a4a4a', arrowhead='none')
|
| 160 |
|
| 161 |
+
# Start recursion for tasks under this phase
|
| 162 |
+
if 'tasks' in phase and isinstance(phase['tasks'], list):
|
| 163 |
+
_add_wbs_nodes_recursive(phase_id, phase['tasks'], phase_depth + 1)
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
# Save to temporary file
|
| 166 |
with NamedTemporaryFile(delete=False, suffix='.png') as tmp:
|
| 167 |
dot.render(tmp.name, format='png', cleanup=True)
|