Skip to content

Commit 09e093e

Browse files
update(gantt-dependencies): bokeh — Comprehensive review: verify dependency logic, improve quality
1 parent e8c0489 commit 09e093e

4 files changed

Lines changed: 123 additions & 68 deletions

File tree

plots/gantt-dependencies/implementations/bokeh.py

Lines changed: 114 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
gantt-dependencies: Gantt Chart with Dependencies
3-
Library: bokeh 3.8.2 | Python 3.13.11
4-
Quality: 90/100 | Created: 2026-01-15
3+
Library: bokeh 3.8.2 | Python 3.14
4+
Quality: /100 | Updated: 2026-02-25
55
"""
66

77
import pandas as pd
88
from bokeh.io import export_png, output_file, save
9-
from bokeh.models import ColumnDataSource, LabelSet, Legend, LegendItem
9+
from bokeh.models import BoxAnnotation, ColumnDataSource, LabelSet, Legend, LegendItem
1010
from bokeh.plotting import figure
1111

1212

@@ -120,148 +120,200 @@
120120
group_df = df[df["group"] == group]
121121
group_spans[group] = {"start": group_df["start"].min(), "end": group_df["end"].max()}
122122

123-
# Build y-positions using numeric indices
123+
# Build y-positions with group headers
124124
y_positions = {}
125125
y_labels = []
126+
y_is_group = []
126127
current_y = 0
127128

128129
for group in groups:
129-
# Group header
130130
y_positions[f"__group__{group}"] = current_y
131131
y_labels.append((current_y, group))
132+
y_is_group.append(True)
132133
current_y += 1
133-
# Tasks in this group (indented)
134134
group_tasks = df[df["group"] == group]["task"].tolist()
135135
for task in group_tasks:
136136
y_positions[task] = current_y
137-
y_labels.append((current_y, f" {task}"))
137+
y_labels.append((current_y, f" {task}"))
138+
y_is_group.append(False)
138139
current_y += 1
139140

140141
max_y = current_y
141142

142-
# Create figure with numeric y-axis
143+
# Create figure
143144
p = figure(
144145
width=4800,
145146
height=2700,
146147
x_axis_type="datetime",
147148
y_range=(max_y + 0.5, -0.5),
148-
title="gantt-dependencies · bokeh · pyplots.ai",
149-
x_axis_label="Timeline",
149+
title="gantt-dependencies \u00b7 bokeh \u00b7 pyplots.ai",
150+
x_axis_label="Timeline (Weeks)",
150151
tools="",
151152
)
152153

153-
# Style - larger fonts for 4800x2700 canvas
154+
# Style
154155
p.title.text_font_size = "42pt"
156+
p.title.text_color = "#333333"
155157
p.xaxis.axis_label_text_font_size = "28pt"
156158
p.xaxis.major_label_text_font_size = "22pt"
157159
p.yaxis.visible = False
158-
p.xgrid.grid_line_alpha = 0.3
160+
p.xgrid.grid_line_alpha = 0.15
161+
p.xgrid.grid_line_dash = [6, 4]
159162
p.ygrid.grid_line_alpha = 0.0
163+
p.outline_line_color = None
164+
p.background_fill_color = "#FAFAFA"
165+
166+
# Alternating row bands for readability
167+
for i in range(max_y):
168+
if i % 2 == 0:
169+
p.add_layout(
170+
BoxAnnotation(
171+
bottom=i - 0.5, top=i + 0.5, fill_color="#F0F0F0", fill_alpha=0.5, level="underlay", line_color=None
172+
)
173+
)
160174

161175
# Draw group span bars (lighter background)
162176
group_renderers = {}
163177
for group in groups:
164178
span = group_spans[group]
165179
y = y_positions[f"__group__{group}"]
166180
source = ColumnDataSource(data={"y": [y], "left": [span["start"]], "right": [span["end"]], "height": [0.7]})
167-
r = p.hbar(y="y", left="left", right="right", height="height", color=group_colors[group], alpha=0.35, source=source)
181+
r = p.hbar(y="y", left="left", right="right", height="height", color=group_colors[group], alpha=0.3, source=source)
168182
group_renderers[group] = r
169183

170184
# Draw task bars
171185
for _, row in df.iterrows():
172186
y = y_positions[row["task"]]
173-
source = ColumnDataSource(data={"y": [y], "left": [row["start"]], "right": [row["end"]], "height": [0.55]})
187+
source = ColumnDataSource(data={"y": [y], "left": [row["start"]], "right": [row["end"]], "height": [0.5]})
174188
p.hbar(
175-
y="y", left="left", right="right", height="height", color=group_colors[row["group"]], alpha=0.9, source=source
189+
y="y",
190+
left="left",
191+
right="right",
192+
height="height",
193+
color=group_colors[row["group"]],
194+
alpha=0.9,
195+
line_color="white",
196+
line_width=1,
197+
source=source,
176198
)
177199

178-
# Draw dependency arrows using multi_line with arrowheads
179-
# Route arrows to avoid passing through task bars by going below/above them
200+
# Draw dependency arrows (finish-to-start)
180201
arrow_xs = []
181202
arrow_ys = []
182203
arrowhead_xs = []
183204
arrowhead_ys = []
184205

185-
bar_offset = 0.4 # Offset to route around bars
206+
dep_color = "#666666"
186207

187208
for _, row in df.iterrows():
188209
task_name = row["task"]
189210
task_y = y_positions[task_name]
190-
task_start = row["start"].value / 1e6 # Convert to ms for plotting
211+
task_start_ms = row["start"].value / 1e6
191212

192213
for dep_name in row["depends_on"]:
193214
if dep_name in task_lookup:
194215
dep_row = df.iloc[task_lookup[dep_name]]
195-
dep_end = dep_row["end"].value / 1e6
216+
dep_end_ms = dep_row["end"].value / 1e6
196217
dep_y = y_positions[dep_name]
197218

198-
# Route around bars: go from end of dep bar, drop down/up outside bar area, then connect
199-
if task_y > dep_y:
200-
# Target is below: route via bottom edges of bars
201-
route_y = max(dep_y, task_y) + bar_offset
202-
arrow_xs.append([dep_end, dep_end, task_start, task_start])
203-
arrow_ys.append([dep_y + bar_offset * 0.7, route_y, route_y, task_y])
204-
elif task_y < dep_y:
205-
# Target is above: route via top edges
206-
route_y = min(dep_y, task_y) - bar_offset
207-
arrow_xs.append([dep_end, dep_end, task_start, task_start])
208-
arrow_ys.append([dep_y - bar_offset * 0.7, route_y, route_y, task_y])
219+
# Horizontal offset for the vertical drop segment
220+
h_offset = 1.0 * 24 * 60 * 60 * 1000 # 1 day in ms
221+
222+
if task_y != dep_y:
223+
# Route: right from dep end → down/up → horizontal → into successor start
224+
mid_x = dep_end_ms + h_offset
225+
arrow_xs.append([dep_end_ms, mid_x, mid_x, task_start_ms])
226+
arrow_ys.append([dep_y, dep_y, task_y, task_y])
209227
else:
210-
# Same row (shouldn't happen)
211-
arrow_xs.append([dep_end, task_start])
228+
arrow_xs.append([dep_end_ms, task_start_ms])
212229
arrow_ys.append([dep_y, task_y])
213230

214-
# Arrowhead (small triangle pointing right)
215-
arrow_size = 2 * 24 * 60 * 60 * 1000 # 2 days in ms for arrowhead
216-
arrowhead_xs.append([task_start - arrow_size, task_start, task_start - arrow_size])
231+
# Arrowhead pointing right into the successor bar
232+
arrow_size = 1.5 * 24 * 60 * 60 * 1000 # 1.5 days in ms
233+
arrowhead_xs.append([task_start_ms - arrow_size, task_start_ms, task_start_ms - arrow_size])
217234
arrowhead_ys.append([task_y - 0.12, task_y, task_y + 0.12])
218235

219236
# Draw dependency lines
237+
dep_renderer = None
220238
if arrow_xs:
221-
p.multi_line(xs=arrow_xs, ys=arrow_ys, line_color="#555555", line_width=2.5, line_alpha=0.7)
239+
dep_renderer = p.multi_line(xs=arrow_xs, ys=arrow_ys, line_color=dep_color, line_width=3, line_alpha=0.6)
222240

223241
# Draw arrowheads
224242
if arrowhead_xs:
225243
p.patches(
226-
xs=arrowhead_xs, ys=arrowhead_ys, fill_color="#555555", fill_alpha=0.8, line_color="#555555", line_width=1
244+
xs=arrowhead_xs, ys=arrowhead_ys, fill_color=dep_color, fill_alpha=0.7, line_color=dep_color, line_width=1
245+
)
246+
247+
# Y-axis labels — group headers bold, task names regular
248+
group_label_ys = []
249+
group_label_texts = []
250+
task_label_ys = []
251+
task_label_texts = []
252+
253+
for (y, label), is_group in zip(y_labels, y_is_group, strict=True):
254+
if is_group:
255+
group_label_ys.append(y)
256+
group_label_texts.append(label)
257+
else:
258+
task_label_ys.append(y)
259+
task_label_texts.append(label)
260+
261+
label_x = df["start"].min() - pd.Timedelta(days=1)
262+
263+
# Group header labels (bold, larger)
264+
group_label_source = ColumnDataSource(
265+
data={"y": group_label_ys, "text": group_label_texts, "x": [label_x] * len(group_label_ys)}
266+
)
267+
p.add_layout(
268+
LabelSet(
269+
x="x",
270+
y="y",
271+
text="text",
272+
source=group_label_source,
273+
text_font_size="24pt",
274+
text_font_style="bold",
275+
text_align="right",
276+
x_offset=-10,
277+
text_baseline="middle",
278+
text_color="#222222",
227279
)
280+
)
228281

229-
# Add y-axis labels manually with larger font for 4800x2700 canvas
230-
label_source = ColumnDataSource(
231-
data={
232-
"y": [y for y, _ in y_labels],
233-
"text": [label for _, label in y_labels],
234-
"x": [df["start"].min() - pd.Timedelta(days=1)] * len(y_labels),
235-
}
282+
# Task labels (regular, slightly smaller)
283+
task_label_source = ColumnDataSource(
284+
data={"y": task_label_ys, "text": task_label_texts, "x": [label_x] * len(task_label_ys)}
236285
)
237-
labels = LabelSet(
238-
x="x",
239-
y="y",
240-
text="text",
241-
source=label_source,
242-
text_font_size="22pt",
243-
text_align="right",
244-
x_offset=-10,
245-
y_offset=0,
246-
text_baseline="middle",
286+
p.add_layout(
287+
LabelSet(
288+
x="x",
289+
y="y",
290+
text="text",
291+
source=task_label_source,
292+
text_font_size="20pt",
293+
text_align="right",
294+
x_offset=-10,
295+
text_baseline="middle",
296+
text_color="#444444",
297+
)
247298
)
248-
p.add_layout(labels)
249299

250-
# Adjust x range to make room for labels
251-
x_min = df["start"].min() - pd.Timedelta(days=12)
300+
# Adjust x range for labels
301+
x_min = df["start"].min() - pd.Timedelta(days=14)
252302
x_max = df["end"].max() + pd.Timedelta(days=3)
253303
p.x_range.start = x_min
254304
p.x_range.end = x_max
255305

256-
# Add legend inside the plot area (top right corner) for better proximity to content
306+
# Legend with phase colors and dependency line
257307
legend_items = []
258308
for group in groups:
259309
legend_items.append(LegendItem(label=group, renderers=[group_renderers[group]]))
310+
if dep_renderer:
311+
legend_items.append(LegendItem(label="Dependency (finish-to-start)", renderers=[dep_renderer]))
260312

261313
legend = Legend(
262314
items=legend_items,
263315
location="top_right",
264-
label_text_font_size="22pt",
316+
label_text_font_size="20pt",
265317
spacing=12,
266318
padding=20,
267319
background_fill_alpha=0.85,
@@ -277,5 +329,5 @@
277329
export_png(p, filename="plot.png")
278330

279331
# Save HTML (interactive)
280-
output_file("plot.html", title="gantt-dependencies · bokeh · pyplots.ai")
332+
output_file("plot.html", title="gantt-dependencies \u00b7 bokeh \u00b7 pyplots.ai")
281333
save(p)

plots/gantt-dependencies/metadata/bokeh.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: bokeh
22
specification_id: gantt-dependencies
33
created: '2026-01-15T21:03:23Z'
4-
updated: '2026-01-15T21:12:38Z'
5-
generated_by: claude-opus-4-5-20251101
4+
updated: '2026-02-25T14:40:00Z'
5+
generated_by: claude-opus-4-6
66
workflow_run: 21046152112
77
issue: 3830
8-
python_version: 3.13.11
8+
python_version: '3.14'
99
library_version: 3.8.2
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/gantt-dependencies/bokeh/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/gantt-dependencies/bokeh/plot_thumb.png
1212
preview_html: https://storage.googleapis.com/pyplots-images/plots/gantt-dependencies/bokeh/plot.html
13-
quality_score: 90
13+
quality_score: null
1414
review:
1515
strengths:
1616
- Excellent use of Bokeh-specific features (ColumnDataSource, multi_line, patches,

plots/gantt-dependencies/specification.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ A Gantt chart that visualizes project schedules with task dependencies and group
2323

2424
## Notes
2525

26-
- Draw dependency arrows/connectors from the end of predecessor tasks to the start of successor tasks
26+
- Draw dependency arrows from the right edge (end date) of each predecessor bar to the left edge (start date) of the successor bar
2727
- Use different visual styles for dependency types (finish-to-start is most common)
2828
- Group headers should show aggregate timeline spanning from earliest to latest task in the group
2929
- Consider indentation or color coding to distinguish groups from individual tasks
3030
- Arrows should avoid overlapping task bars where possible
3131
- Include a legend explaining dependency line styles if multiple types are used
32+
- Dependent tasks must be scheduled to start at or after the end of their predecessor — never before
3233
- Vertical alignment should clearly show task hierarchy (groups above their child tasks)

plots/gantt-dependencies/specification.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Gantt Chart with Dependencies
66

77
# Specification tracking
88
created: 2026-01-15T20:43:22Z
9-
updated: 2026-01-15T20:43:22Z
9+
updated: 2026-02-25T12:00:00Z
1010
issue: 3830
1111
suggested: Eifi1
1212

@@ -27,3 +27,5 @@ tags:
2727
- grouped
2828
- hierarchical
2929
- annotated
30+
- temporal
31+
- dependencies

0 commit comments

Comments
 (0)