Skip to content

Commit a6e029c

Browse files
update(arc-basic): bokeh — comprehensive quality review (#4370)
## Summary Updated **bokeh** implementation for **arc-basic**. **Changes:** Comprehensive quality review and update ### Changes - Updated implementation with improved code quality and visual design ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 1998bd9 commit a6e029c

2 files changed

Lines changed: 304 additions & 194 deletions

File tree

plots/arc-basic/implementations/bokeh.py

Lines changed: 147 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
""" pyplots.ai
22
arc-basic: Basic Arc Diagram
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: bokeh 3.8.2 | Python 3.14.3
4+
Quality: 90/100 | Updated: 2026-02-23
55
"""
66

77
import numpy as np
88
from bokeh.io import export_png, save
9-
from bokeh.models import ColumnDataSource, Label, Legend, LegendItem
9+
from bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem
1010
from bokeh.plotting import figure
1111

1212

1313
# Data - Character interactions in a story chapter
1414
nodes = ["Alice", "Bob", "Carol", "David", "Eve", "Frank", "Grace", "Henry"]
15-
# Edges as (source_idx, target_idx, weight) - character conversation connections
1615
edges = [
1716
(0, 1, 3), # Alice-Bob: frequent
1817
(0, 2, 2), # Alice-Carol: moderate
@@ -30,95 +29,175 @@
3029

3130
# Node positions along horizontal axis
3231
n_nodes = len(nodes)
33-
x_positions = np.linspace(0, 10, n_nodes)
32+
x_positions = np.linspace(0.5, 10.5, n_nodes)
3433
y_baseline = 0
3534

36-
# Create figure
35+
# Compute weighted degree per node for data storytelling (hub visibility)
36+
weighted_degrees = np.zeros(n_nodes)
37+
for src, tgt, w in edges:
38+
weighted_degrees[src] += w
39+
weighted_degrees[tgt] += w
40+
41+
# Node sizes scaled by weighted degree — hub characters appear larger
42+
min_wd, max_wd = weighted_degrees.min(), weighted_degrees.max()
43+
node_sizes = 28 + (weighted_degrees - min_wd) / (max_wd - min_wd) * 28
44+
45+
# Node colors: darker fill for higher connectivity
46+
light_rgb = np.array([0x93, 0xB5, 0xCF])
47+
dark_rgb = np.array([0x1A, 0x3F, 0x5C])
48+
node_colors = []
49+
for wd in weighted_degrees:
50+
t = (wd - min_wd) / (max_wd - min_wd)
51+
rgb = (light_rgb + t * (dark_rgb - light_rgb)).astype(int)
52+
node_colors.append(f"#{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}")
53+
54+
# Weight-based styling: darker and thicker for stronger connections
55+
weight_colors = {1: "#93B5CF", 2: "#306998", 3: "#1A3F5C"}
56+
weight_labels = {1: "Brief", 2: "Moderate", 3: "Frequent"}
57+
58+
# Create figure with tighter vertical framing
3759
p = figure(
3860
width=4800,
3961
height=2700,
40-
title="arc-basic · bokeh · pyplots.ai",
41-
x_axis_label="Characters",
42-
y_axis_label="",
43-
x_range=(-0.5, 10.5),
44-
y_range=(-1.5, 4.5),
62+
title="arc-basic \u00b7 bokeh \u00b7 pyplots.ai",
63+
x_range=(-0.2, 11.2),
64+
y_range=(-0.65, 3.15),
65+
toolbar_location=None,
4566
)
4667

47-
# Style the figure
48-
p.title.text_font_size = "28pt"
49-
p.xaxis.axis_label_text_font_size = "22pt"
50-
p.yaxis.axis_label_text_font_size = "22pt"
51-
p.xaxis.major_label_text_font_size = "18pt"
52-
p.yaxis.major_label_text_font_size = "18pt"
53-
54-
# Hide y-axis (not meaningful for arc diagram)
68+
# Style — subtle background tint complements blue palette
69+
p.background_fill_color = "#F7F9FB"
70+
p.border_fill_color = "#F7F9FB"
71+
p.title.text_font_size = "32pt"
72+
p.title.text_color = "#1A3F5C"
73+
p.xaxis.visible = False
5574
p.yaxis.visible = False
75+
p.xgrid.visible = False
5676
p.ygrid.visible = False
77+
p.outline_line_color = None
78+
79+
# Subtitle providing narrative context
80+
subtitle = Label(
81+
x=-0.1,
82+
y=2.95,
83+
text="Character Interaction Frequency in Chapter 1",
84+
text_font_size="22pt",
85+
text_font_style="italic",
86+
text_color="#666666",
87+
text_align="left",
88+
)
89+
p.add_layout(subtitle)
5790

58-
# Draw arcs as bezier curves, collect renderers for legend
59-
long_range_renderers = []
60-
short_range_renderers = []
91+
# Draw arcs using Bokeh's native bezier glyph
92+
arc_renderers_by_weight = {1: [], 2: [], 3: []}
6193

6294
for src_idx, tgt_idx, weight in edges:
6395
x_src = x_positions[src_idx]
6496
x_tgt = x_positions[tgt_idx]
6597

66-
# Arc height proportional to distance between nodes
6798
distance = abs(x_tgt - x_src)
68-
arc_height = distance * 0.5
99+
arc_height = distance * 0.35
69100

70-
# Generate arc points using quadratic bezier
71-
t = np.linspace(0, 1, 50)
72-
# Control point at midpoint, elevated by arc_height
73-
cx = (x_src + x_tgt) / 2
101+
# Control points at 1/3 and 2/3 for smooth, rounded arcs
102+
cx0 = x_src + (x_tgt - x_src) / 3
103+
cx1 = x_src + 2 * (x_tgt - x_src) / 3
74104
cy = arc_height
75105

76-
# Quadratic bezier: B(t) = (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2
77-
arc_x = (1 - t) ** 2 * x_src + 2 * (1 - t) * t * cx + t**2 * x_tgt
78-
arc_y = (1 - t) ** 2 * y_baseline + 2 * (1 - t) * t * cy + t**2 * y_baseline
79-
80-
# Line width based on weight
81-
line_width = weight * 2
82-
83-
# Color based on connection type (long-range vs short-range)
84-
if distance > 5:
85-
color = "#FFD43B" # Python Yellow for long-range
86-
alpha = 0.7
87-
else:
88-
color = "#306998" # Python Blue for short-range
89-
alpha = 0.5
90-
91-
arc_source = ColumnDataSource(data={"x": arc_x, "y": arc_y})
92-
renderer = p.line(x="x", y="y", source=arc_source, line_width=line_width, line_color=color, line_alpha=alpha)
93-
94-
# Collect renderers for legend
95-
if distance > 5:
96-
long_range_renderers.append(renderer)
97-
else:
98-
short_range_renderers.append(renderer)
106+
line_width = 1.0 + weight * 2.5
107+
color = weight_colors[weight]
108+
alpha = 0.45 + weight * 0.13
109+
110+
arc_source = ColumnDataSource(
111+
data={
112+
"x0": [x_src],
113+
"y0": [y_baseline],
114+
"x1": [x_tgt],
115+
"y1": [y_baseline],
116+
"cx0": [cx0],
117+
"cy0": [cy],
118+
"cx1": [cx1],
119+
"cy1": [cy],
120+
"source_name": [nodes[src_idx]],
121+
"target_name": [nodes[tgt_idx]],
122+
"weight_label": [weight_labels[weight]],
123+
}
124+
)
125+
renderer = p.bezier(
126+
x0="x0",
127+
y0="y0",
128+
x1="x1",
129+
y1="y1",
130+
cx0="cx0",
131+
cy0="cy0",
132+
cx1="cx1",
133+
cy1="cy1",
134+
source=arc_source,
135+
line_width=line_width,
136+
line_color=color,
137+
line_alpha=alpha,
138+
)
139+
arc_renderers_by_weight[weight].append(renderer)
140+
141+
# HoverTool for edge details (Bokeh-distinctive interactivity)
142+
hover = HoverTool(
143+
tooltips=[("Connection", "@source_name \u2194 @target_name"), ("Frequency", "@weight_label")], line_policy="interp"
144+
)
145+
p.add_tools(hover)
146+
147+
# Draw nodes with degree-based sizing and coloring
148+
node_source = ColumnDataSource(
149+
data={
150+
"x": x_positions,
151+
"y": [y_baseline] * n_nodes,
152+
"name": nodes,
153+
"size": node_sizes,
154+
"color": node_colors,
155+
"connections": [int(wd) for wd in weighted_degrees],
156+
}
157+
)
158+
node_renderer = p.scatter(
159+
x="x", y="y", source=node_source, size="size", fill_color="color", line_color="white", line_width=3
160+
)
99161

100-
# Draw nodes along baseline
101-
node_source = ColumnDataSource(data={"x": x_positions, "y": [y_baseline] * n_nodes, "name": nodes})
102-
p.scatter(x="x", y="y", source=node_source, size=25, fill_color="#306998", line_color="white", line_width=2)
162+
# Node hover with connection strength
163+
node_hover = HoverTool(
164+
tooltips=[("Character", "@name"), ("Connection Strength", "@connections")], renderers=[node_renderer]
165+
)
166+
p.add_tools(node_hover)
103167

104-
# Add node labels below the baseline
168+
# Node labels
105169
for i, name in enumerate(nodes):
106-
label = Label(x=x_positions[i], y=-0.5, text=name, text_font_size="20pt", text_align="center", text_baseline="top")
170+
label = Label(
171+
x=x_positions[i],
172+
y=-0.22,
173+
text=name,
174+
text_font_size="20pt",
175+
text_align="center",
176+
text_baseline="top",
177+
text_color="#333333",
178+
)
107179
p.add_layout(label)
108180

109-
# Add subtle grid only for x
110-
p.xgrid.grid_line_alpha = 0.3
111-
p.xgrid.grid_line_dash = [6, 4]
112-
113-
# Add legend for connection types
181+
# Legend by interaction frequency
114182
legend_items = []
115-
if long_range_renderers:
116-
legend_items.append(LegendItem(label="Long-range (distance > 5)", renderers=[long_range_renderers[0]]))
117-
if short_range_renderers:
118-
legend_items.append(LegendItem(label="Short-range (distance ≤ 5)", renderers=[short_range_renderers[0]]))
119-
120-
legend = Legend(items=legend_items, location="top_right", label_text_font_size="18pt")
121-
p.add_layout(legend, "right")
183+
for weight, label_text in [(3, "Frequent"), (2, "Moderate"), (1, "Brief")]:
184+
if arc_renderers_by_weight[weight]:
185+
legend_items.append(LegendItem(label=label_text, renderers=[arc_renderers_by_weight[weight][0]]))
186+
187+
legend = Legend(
188+
items=legend_items,
189+
location="top_right",
190+
label_text_font_size="20pt",
191+
label_text_color="#444444",
192+
border_line_color=None,
193+
background_fill_color="#F7F9FB",
194+
background_fill_alpha=0.9,
195+
glyph_width=40,
196+
glyph_height=8,
197+
spacing=12,
198+
padding=18,
199+
)
200+
p.add_layout(legend)
122201

123202
# Save outputs
124203
export_png(p, filename="plot.png")

0 commit comments

Comments
 (0)