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)
|