|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | gantt-dependencies: Gantt Chart with Dependencies |
3 | | -Library: plotly 6.5.2 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-15 |
| 3 | +Library: plotly 6.5.2 | Python 3.14 |
| 4 | +Quality: /100 | Updated: 2026-02-25 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import pandas as pd |
|
42 | 42 | }, |
43 | 43 | { |
44 | 44 | "task": "Database Design", |
45 | | - "start": "2024-03-18", |
46 | | - "end": "2024-03-24", |
| 45 | + "start": "2024-03-20", |
| 46 | + "end": "2024-03-26", |
47 | 47 | "group": "Design", |
48 | 48 | "depends_on": ["System Architecture"], |
49 | 49 | }, |
|
71 | 71 | }, |
72 | 72 | { |
73 | 73 | "task": "Frontend Development", |
74 | | - "start": "2024-04-01", |
75 | | - "end": "2024-04-18", |
| 74 | + "start": "2024-03-30", |
| 75 | + "end": "2024-04-16", |
76 | 76 | "group": "Development", |
77 | 77 | "depends_on": ["Design Review"], |
78 | 78 | }, |
|
85 | 85 | }, |
86 | 86 | { |
87 | 87 | "task": "Integration", |
88 | | - "start": "2024-04-15", |
| 88 | + "start": "2024-04-16", |
89 | 89 | "end": "2024-04-22", |
90 | 90 | "group": "Development", |
91 | 91 | "depends_on": ["Backend API Development", "Frontend Development", "Database Implementation"], |
|
162 | 162 |
|
163 | 163 | # Build task list with groups (groups first, then their tasks) |
164 | 164 | ordered_tasks = [] |
165 | | -task_to_idx = {} |
166 | | -idx = 0 |
| 165 | +task_y_labels = {} |
167 | 166 |
|
168 | 167 | for group_name in group_order: |
169 | 168 | # Add group summary bar |
170 | | - ordered_tasks.append({"name": f"▼ {group_name}", "is_group": True, "group": group_name}) |
171 | | - task_to_idx[f"GROUP_{group_name}"] = idx |
172 | | - idx += 1 |
| 169 | + label = f"\u25bc {group_name}" |
| 170 | + ordered_tasks.append({"name": label, "is_group": True, "group": group_name}) |
173 | 171 | # Add tasks in this group |
174 | 172 | group_tasks = df[df["group"] == group_name].sort_values("start") |
175 | 173 | for _, task in group_tasks.iterrows(): |
176 | | - ordered_tasks.append({"name": f" {task['task']}", "is_group": False, "task_data": task}) |
177 | | - task_to_idx[task["task"]] = idx |
178 | | - idx += 1 |
| 174 | + label = f" {task['task']}" |
| 175 | + ordered_tasks.append({"name": label, "is_group": False, "task_data": task}) |
| 176 | + task_y_labels[task["task"]] = label |
179 | 177 |
|
180 | | -# Build y-axis category order |
| 178 | +# Build y-axis category order (reversed so first task appears at top) |
181 | 179 | y_categories = [item["name"] for item in ordered_tasks] |
182 | 180 |
|
183 | 181 | # Create figure |
184 | 182 | fig = go.Figure() |
185 | 183 |
|
186 | | -# Add task bars using timeline-style scatter plot for proper horizontal bars |
187 | | -for i, item in enumerate(ordered_tasks): |
| 184 | +# Add task bars |
| 185 | +for item in ordered_tasks: |
188 | 186 | if item["is_group"]: |
189 | | - # Group summary bar |
190 | 187 | group_name = item["group"] |
191 | 188 | group_data = groups[groups["group"] == group_name].iloc[0] |
192 | 189 | fig.add_trace( |
193 | 190 | go.Scatter( |
194 | 191 | x=[group_data["start"], group_data["end"]], |
195 | 192 | y=[item["name"], item["name"]], |
196 | 193 | mode="lines", |
197 | | - line=dict(color=group_colors[group_name], width=20), |
| 194 | + line={"color": group_colors[group_name], "width": 20}, |
198 | 195 | name=group_name, |
199 | 196 | showlegend=True, |
200 | 197 | legendgroup=group_name, |
201 | | - hovertemplate=f"<b>{group_name}</b><br>Start: {group_data['start'].strftime('%Y-%m-%d')}<br>End: {group_data['end'].strftime('%Y-%m-%d')}<extra></extra>", |
| 198 | + hovertemplate=( |
| 199 | + f"<b>{group_name}</b><br>" |
| 200 | + f"Start: {group_data['start'].strftime('%Y-%m-%d')}<br>" |
| 201 | + f"End: {group_data['end'].strftime('%Y-%m-%d')}<extra></extra>" |
| 202 | + ), |
202 | 203 | ) |
203 | 204 | ) |
204 | 205 | else: |
205 | | - # Individual task bar |
206 | 206 | task = item["task_data"] |
207 | 207 | group_name = task["group"] |
208 | 208 | duration = (task["end"] - task["start"]).days |
|
211 | 211 | x=[task["start"], task["end"]], |
212 | 212 | y=[item["name"], item["name"]], |
213 | 213 | mode="lines", |
214 | | - line=dict(color=group_colors[group_name], width=14), |
| 214 | + line={"color": group_colors[group_name], "width": 14}, |
215 | 215 | opacity=0.85, |
216 | 216 | showlegend=False, |
217 | 217 | legendgroup=group_name, |
218 | | - hovertemplate=f"<b>{task['task']}</b><br>Start: {task['start'].strftime('%Y-%m-%d')}<br>End: {task['end'].strftime('%Y-%m-%d')}<br>Duration: {duration} days<extra></extra>", |
| 218 | + hovertemplate=( |
| 219 | + f"<b>{task['task']}</b><br>" |
| 220 | + f"Start: {task['start'].strftime('%Y-%m-%d')}<br>" |
| 221 | + f"End: {task['end'].strftime('%Y-%m-%d')}<br>" |
| 222 | + f"Duration: {duration} days<extra></extra>" |
| 223 | + ), |
219 | 224 | ) |
220 | 225 | ) |
221 | 226 |
|
222 | | -# Add dependency arrows using shapes for better control |
223 | | -shapes = [] |
| 227 | +# Add dependency arrows as annotations with arrowheads |
224 | 228 | for item in ordered_tasks: |
225 | 229 | if not item["is_group"]: |
226 | 230 | task = item["task_data"] |
227 | | - task_name = task["task"] |
228 | 231 | depends_on = task["depends_on"] |
229 | 232 | if depends_on: |
230 | 233 | for dep in depends_on: |
231 | | - if dep in task_to_idx: |
232 | | - # Find predecessor task data |
| 234 | + if dep in task_y_labels: |
233 | 235 | pred_task = df[df["task"] == dep].iloc[0] |
234 | 236 | pred_end = pred_task["end"] |
235 | 237 | curr_start = task["start"] |
236 | | - pred_idx = task_to_idx[dep] |
237 | | - curr_idx = task_to_idx[task_name] |
| 238 | + pred_y = task_y_labels[dep] |
| 239 | + curr_y = task_y_labels[task["task"]] |
238 | 240 |
|
239 | | - # Draw line from end of predecessor to start of current task |
240 | | - # Using shapes for connector lines |
241 | | - shapes.append( |
242 | | - dict( |
243 | | - type="line", |
244 | | - x0=pred_end, |
245 | | - y0=pred_idx, |
246 | | - x1=curr_start, |
247 | | - y1=curr_idx, |
248 | | - xref="x", |
249 | | - yref="y", |
250 | | - line=dict(color="#555555", width=1.5, dash="dot"), |
251 | | - opacity=0.5, |
252 | | - ) |
| 241 | + # Annotation arrow from predecessor end to successor start |
| 242 | + fig.add_annotation( |
| 243 | + x=curr_start, |
| 244 | + y=curr_y, |
| 245 | + ax=pred_end, |
| 246 | + ay=pred_y, |
| 247 | + xref="x", |
| 248 | + yref="y", |
| 249 | + axref="x", |
| 250 | + ayref="y", |
| 251 | + showarrow=True, |
| 252 | + arrowhead=3, |
| 253 | + arrowsize=1.2, |
| 254 | + arrowwidth=1.5, |
| 255 | + arrowcolor="#555555", |
| 256 | + opacity=0.7, |
253 | 257 | ) |
254 | 258 |
|
255 | | -# Update layout |
| 259 | +# Layout |
256 | 260 | fig.update_layout( |
257 | | - title=dict( |
258 | | - text="gantt-dependencies · plotly · pyplots.ai", font=dict(size=32, color="#333333"), x=0.5, xanchor="center" |
259 | | - ), |
260 | | - xaxis=dict( |
261 | | - title=dict(text="Timeline (2024)", font=dict(size=24)), |
262 | | - tickfont=dict(size=14), |
263 | | - type="date", |
264 | | - tickformat="%b %d", |
265 | | - gridcolor="rgba(0,0,0,0.08)", |
266 | | - showgrid=True, |
267 | | - dtick=7 * 24 * 60 * 60 * 1000, # Weekly ticks (in milliseconds) |
268 | | - tickangle=45, |
269 | | - ), |
270 | | - yaxis=dict( |
271 | | - title=dict(text="", font=dict(size=22)), |
272 | | - tickfont=dict(size=15), |
273 | | - categoryorder="array", |
274 | | - categoryarray=y_categories[::-1], |
275 | | - showgrid=False, |
276 | | - ), |
| 261 | + title={ |
| 262 | + "text": "gantt-dependencies \u00b7 plotly \u00b7 pyplots.ai", |
| 263 | + "font": {"size": 32, "color": "#333333"}, |
| 264 | + "x": 0.5, |
| 265 | + "xanchor": "center", |
| 266 | + }, |
| 267 | + xaxis={ |
| 268 | + "title": {"text": "Timeline (2024)", "font": {"size": 24}}, |
| 269 | + "tickfont": {"size": 16}, |
| 270 | + "type": "date", |
| 271 | + "tickformat": "%b %d", |
| 272 | + "gridcolor": "rgba(0,0,0,0.08)", |
| 273 | + "showgrid": True, |
| 274 | + "dtick": 7 * 24 * 60 * 60 * 1000, |
| 275 | + "tickangle": 45, |
| 276 | + }, |
| 277 | + yaxis={ |
| 278 | + "title": {"text": "", "font": {"size": 22}}, |
| 279 | + "tickfont": {"size": 15}, |
| 280 | + "categoryorder": "array", |
| 281 | + "categoryarray": y_categories[::-1], |
| 282 | + "showgrid": False, |
| 283 | + }, |
277 | 284 | template="plotly_white", |
278 | | - shapes=shapes, |
279 | | - legend=dict( |
280 | | - title=dict(text="Project Phases", font=dict(size=20)), |
281 | | - font=dict(size=16), |
282 | | - orientation="h", |
283 | | - yanchor="bottom", |
284 | | - y=1.02, |
285 | | - xanchor="center", |
286 | | - x=0.5, |
287 | | - itemwidth=30, |
288 | | - ), |
289 | | - margin=dict(l=220, r=60, t=130, b=100), |
| 285 | + legend={ |
| 286 | + "title": {"text": "Project Phases", "font": {"size": 20}}, |
| 287 | + "font": {"size": 16}, |
| 288 | + "orientation": "h", |
| 289 | + "yanchor": "bottom", |
| 290 | + "y": 1.02, |
| 291 | + "xanchor": "center", |
| 292 | + "x": 0.5, |
| 293 | + "itemwidth": 30, |
| 294 | + }, |
| 295 | + margin={"l": 230, "r": 40, "t": 120, "b": 90}, |
290 | 296 | height=900, |
291 | 297 | width=1600, |
292 | 298 | ) |
293 | 299 |
|
294 | | -# Add annotation for dependency legend |
| 300 | +# Dependency legend annotation |
295 | 301 | fig.add_annotation( |
296 | 302 | x=1.0, |
297 | 303 | y=-0.1, |
298 | 304 | xref="paper", |
299 | 305 | yref="paper", |
300 | | - text="····· Dependency (finish-to-start)", |
| 306 | + text="\u2192 Dependency (finish-to-start)", |
301 | 307 | showarrow=False, |
302 | | - font=dict(size=15, color="#555555"), |
| 308 | + font={"size": 15, "color": "#555555"}, |
303 | 309 | xanchor="right", |
304 | 310 | ) |
305 | 311 |
|
306 | | -# Save outputs |
| 312 | +# Save |
307 | 313 | fig.write_image("plot.png", width=1600, height=900, scale=3) |
308 | 314 | fig.write_html("plot.html") |
0 commit comments