ZahirJS commited on
Commit
11561b8
·
verified ·
1 Parent(s): 00ba522

Create wbs_diagram_generator.py

Browse files
Files changed (1) hide show
  1. wbs_diagram_generator.py +166 -0
wbs_diagram_generator.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import graphviz
2
+ import json
3
+ from tempfile import NamedTemporaryFile
4
+ import os
5
+ from graph_generator_utils import add_nodes_and_edges # Reusing common utility
6
+
7
+ def generate_wbs_diagram(json_input: str, base_color: str) -> str:
8
+ """
9
+ Generates a Work Breakdown Structure (WBS) Diagram from JSON input.
10
+
11
+ Args:
12
+ json_input (str): A JSON string describing the WBS structure.
13
+ It must follow the Expected JSON Format Example below.
14
+ base_color (str): The hexadecimal color string (e.g., '#19191a') for the base
15
+ color of the nodes, from which a gradient will be generated.
16
+
17
+ Returns:
18
+ str: The filepath to the generated PNG image file.
19
+
20
+ Expected JSON Format Example:
21
+ {
22
+ "project_title": "Software Development Project",
23
+ "phases": [
24
+ {
25
+ "id": "phase_prep",
26
+ "label": "1. Preparation",
27
+ "tasks": [
28
+ {"id": "task_vision", "label": "1.1. Identify Vision"},
29
+ {"id": "task_design", "label": "1.2. Design & Staffing"}
30
+ ]
31
+ },
32
+ {
33
+ "id": "phase_plan",
34
+ "label": "2. Planning",
35
+ "tasks": [
36
+ {"id": "task_cost", "label": "2.1. Cost Analysis"},
37
+ {"id": "task_benefit", "label": "2.2. Benefit Analysis"},
38
+ {"id": "task_risk", "label": "2.3. Risk Assessment"}
39
+ ]
40
+ },
41
+ {
42
+ "id": "phase_dev",
43
+ "label": "3. Development",
44
+ "tasks": [
45
+ {"id": "task_change", "label": "3.1. Change Management"},
46
+ {"id": "task_impl", "label": "3.2. Implementation"},
47
+ {"id": "task_beta", "label": "3.3. Beta Testing"}
48
+ ]
49
+ }
50
+ ]
51
+ }
52
+ """
53
+ try:
54
+ if not json_input.strip():
55
+ return "Error: Empty input"
56
+
57
+ data = json.loads(json_input)
58
+
59
+ if 'project_title' not in data or 'phases' not in data:
60
+ raise ValueError("Missing required fields: project_title or phases")
61
+
62
+ dot = graphviz.Digraph(
63
+ name='WBSDiagram',
64
+ format='png',
65
+ graph_attr={
66
+ 'rankdir': 'TB', # Top-to-Bottom hierarchy
67
+ 'splines': 'ortho', # Straight lines
68
+ 'bgcolor': 'white', # White background
69
+ 'pad': '0.5', # Padding
70
+ 'ranksep': '0.8', # Adjust vertical separation between ranks
71
+ 'nodesep': '0.5' # Adjust horizontal separation between nodes
72
+ }
73
+ )
74
+
75
+ # Project Title node (main node)
76
+ dot.node(
77
+ 'project_root',
78
+ data['project_title'],
79
+ shape='box',
80
+ style='filled,rounded',
81
+ fillcolor=base_color, # Use the selected base color
82
+ fontcolor='white',
83
+ fontsize='18'
84
+ )
85
+
86
+ # Add phases and their tasks
87
+ current_depth = 1 # Start depth for phases
88
+ for phase in data['phases']:
89
+ phase_id = phase.get('id')
90
+ phase_label = phase.get('label')
91
+ tasks = phase.get('tasks', [])
92
+
93
+ if not all([phase_id, phase_label]):
94
+ raise ValueError(f"Invalid phase: {phase}")
95
+
96
+ # Calculate color for phase node
97
+ # This logic is adapted from add_nodes_and_edges but applied to phases/tasks
98
+ # to keep consistency with the color gradient.
99
+ lightening_factor = 0.12
100
+ base_r = int(base_color[1:3], 16)
101
+ base_g = int(base_color[3:5], 16)
102
+ base_b = int(base_color[5:7], 16)
103
+
104
+ phase_r = base_r + int((255 - base_r) * current_depth * lightening_factor)
105
+ phase_g = base_g + int((255 - base_g) * current_depth * lightening_factor)
106
+ phase_b = base_b + int((255 - base_b) * current_depth * lightening_factor)
107
+ phase_fill_color = f'#{min(255, phase_r):02x}{min(255, phase_g):02x}{min(255, phase_b):02x}'
108
+ phase_font_color = 'white' if current_depth * lightening_factor < 0.6 else 'black'
109
+
110
+ dot.node(
111
+ phase_id,
112
+ phase_label,
113
+ shape='box',
114
+ style='filled,rounded',
115
+ fillcolor=phase_fill_color,
116
+ fontcolor=phase_font_color,
117
+ fontsize='14'
118
+ )
119
+ dot.edge('project_root', phase_id, color='#4a4a4a', arrowhead='none') # Connect to root
120
+
121
+ task_depth = current_depth + 1
122
+ task_r = base_r + int((255 - base_r) * task_depth * lightening_factor)
123
+ task_g = base_g + int((255 - base_g) * task_depth * lightening_factor)
124
+ task_b = base_b + int((255 - base_b) * task_depth * lightening_factor)
125
+ task_fill_color = f'#{min(255, task_r):02x}{min(255, task_g):02x}{min(255, task_b):02x}'
126
+ task_font_color = 'white' if task_depth * lightening_factor < 0.6 else 'black'
127
+ task_font_size = max(9, 14 - (task_depth * 2))
128
+
129
+ for task in tasks:
130
+ task_id = task.get('id')
131
+ task_label = task.get('label')
132
+ if not all([task_id, task_label]):
133
+ raise ValueError(f"Invalid task: {task}")
134
+
135
+ dot.node(
136
+ task_id,
137
+ task_label,
138
+ shape='box',
139
+ style='filled,rounded',
140
+ fillcolor=task_fill_color,
141
+ fontcolor=task_font_color,
142
+ fontsize=str(task_font_size)
143
+ )
144
+ dot.edge(phase_id, task_id, color='#4a4a4a', arrowhead='none') # Connect task to phase
145
+
146
+ # Use subgraph to enforce vertical alignment for tasks within a phase
147
+ # This makes columns in the WBS
148
+ if tasks: # Only create subgraph if there are tasks
149
+ with dot.subgraph(name=f'cluster_{phase_id}') as c:
150
+ c.attr(rank='same') # Try to keep tasks in same rank/column if possible
151
+ # Adding invisible nodes to link the phase to its tasks vertically
152
+ # This helps in aligning tasks under their phase header more cleanly.
153
+ c.node(phase_id)
154
+ for task in tasks:
155
+ c.node(task['id'])
156
+
157
+ # Save to temporary file
158
+ with NamedTemporaryFile(delete=False, suffix='.png') as tmp:
159
+ dot.render(tmp.name, format='png', cleanup=True)
160
+ return tmp.name + '.png'
161
+
162
+ except json.JSONDecodeError:
163
+ return "Error: Invalid JSON format"
164
+ except Exception as e:
165
+ return f"Error: {str(e)}"
166
+