Skip to content

Commit 7e47de1

Browse files
update(band-basic): pygal — comprehensive quality review (#4355)
## Summary Updated **pygal** implementation for **band-basic**. **Changes:** Comprehensive quality review — improved data context, axis labels, 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 752c0f5 commit 7e47de1

2 files changed

Lines changed: 310 additions & 57 deletions

File tree

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,140 @@
11
""" pyplots.ai
22
band-basic: Basic Band Plot
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 88/100 | Created: 2025-12-23
3+
Library: pygal 3.1.0 | Python 3.14
4+
Quality: 89/100 | Updated: 2026-02-23
55
"""
66

77
import numpy as np
88
import pygal
99
from pygal.style import Style
1010

1111

12-
# Data - Time series with 95% confidence interval
12+
# Data - Soil moisture sensor readings with 95% confidence interval
1313
np.random.seed(42)
14-
x = np.linspace(0, 10, 50)
15-
# Central trend line (quadratic curve)
16-
y_center = 2 + 0.5 * x + 0.1 * x**2 + np.random.randn(50) * 0.3
17-
# Smooth the center line
18-
y_center = np.convolve(y_center, np.ones(3) / 3, mode="same")
19-
# Confidence interval widens with x (increasing uncertainty)
20-
uncertainty = 0.5 + 0.15 * x
14+
n_points = 80
15+
hours = np.linspace(0, 48, n_points)
16+
17+
# Realistic soil moisture pattern: starts high after rain, drops during day,
18+
# recovers slightly at night, then dips to a low before a second rain event
19+
base_trend = 38 - 0.3 * hours + 4.0 * np.sin(2 * np.pi * hours / 24) + 8.0 * np.exp(-((hours - 40) ** 2) / 8)
20+
noise = np.random.randn(n_points) * 0.6
21+
y_raw = base_trend + noise
22+
23+
# Smooth with convolution, padding edges to preserve length
24+
kernel = np.ones(7) / 7
25+
y_smooth = np.convolve(y_raw, kernel, mode="valid")
26+
pad_left = (n_points - len(y_smooth)) // 2
27+
pad_right = n_points - len(y_smooth) - pad_left
28+
y_center = np.concatenate([np.full(pad_left, y_smooth[0]), y_smooth, np.full(pad_right, y_smooth[-1])])
29+
30+
# Confidence interval: wider during dry spell, narrower after rain
31+
uncertainty = 1.2 + 0.8 * np.sin(2 * np.pi * hours / 24) ** 2 + 0.04 * hours
2132
y_lower = y_center - uncertainty
2233
y_upper = y_center + uncertainty
2334

24-
# Custom style for 4800x2700 canvas
35+
# Explicit y-axis labels at clean intervals for precise grid control
36+
y_lo = 4 * (int(min(y_lower)) // 4)
37+
y_hi = 4 * (int(max(y_upper)) // 4 + 1)
38+
y_label_values = list(range(y_lo, y_hi + 1, 4))
39+
40+
# Custom style — sans-serif typography, polished palette, refined visual hierarchy
2541
custom_style = Style(
2642
background="white",
2743
plot_background="white",
28-
foreground="#333333",
29-
foreground_strong="#333333",
30-
foreground_subtle="#666666",
31-
guide_stroke_color="#888888", # Darker grid lines for better visibility
32-
colors=("#306998", "#FFD43B"), # Blue for band, Yellow for center line
33-
opacity=".65", # Higher opacity for clearly visible band
34-
opacity_hover=".75",
35-
stroke_width=5, # Thicker lines for better visibility
44+
foreground="#2C3E50",
45+
foreground_strong="#1A252F",
46+
foreground_subtle="#BDC3C7",
47+
guide_stroke_color="#F0F0F0",
48+
guide_stroke_dasharray="2,8",
49+
major_guide_stroke_color="#E8E8E8",
50+
major_guide_stroke_dasharray="4,6",
51+
colors=("#306998", "#B8860B", "#7F8C8D"),
52+
opacity=".20",
53+
opacity_hover=".35",
54+
stroke_opacity="1",
55+
stroke_opacity_hover="1",
56+
stroke_width=4,
3657
title_font_size=60,
3758
label_font_size=42,
3859
major_label_font_size=42,
39-
legend_font_size=42,
60+
legend_font_size=40,
4061
value_font_size=36,
62+
value_colors=("transparent",),
63+
tooltip_font_size=32,
64+
font_family='Helvetica, Arial, "DejaVu Sans", sans-serif',
4165
)
4266

43-
# Create XY chart for precise coordinate control
67+
# Create XY chart with fine-tuned layout and pygal-specific configuration
4468
chart = pygal.XY(
4569
style=custom_style,
4670
width=4800,
4771
height=2700,
48-
title="95% Confidence Interval · band-basic · pygal · pyplots.ai",
49-
x_title="Time (s)",
50-
y_title="Measurement Value",
72+
explicit_size=True,
73+
title="band-basic \u00b7 pygal \u00b7 pyplots.ai",
74+
x_title="Time (hours)",
75+
y_title="Soil Moisture (%)",
5176
show_dots=False,
52-
show_x_guides=True,
77+
show_x_guides=False,
5378
show_y_guides=True,
5479
fill=True,
5580
stroke=True,
5681
legend_at_bottom=True,
82+
legend_at_bottom_columns=3,
83+
legend_box_size=28,
5784
truncate_legend=-1,
85+
x_label_rotation=0,
86+
range=(y_lo - 1, y_hi + 1),
87+
y_labels=y_label_values,
88+
x_labels=[0, 6, 12, 18, 24, 30, 36, 42, 48],
89+
x_labels_major=[0, 12, 24, 36, 48],
90+
show_minor_x_labels=True,
91+
show_minor_y_labels=False,
92+
print_values=False,
93+
x_value_formatter=lambda x: f"{x:.0f}h",
94+
value_formatter=lambda x: f"{x:.1f}%",
95+
tooltip_border_radius=8,
96+
margin_top=30,
97+
margin_bottom=50,
98+
margin_left=30,
99+
margin_right=50,
100+
spacing=18,
101+
js=[],
58102
)
59103

60-
# Create band as a closed polygon: upper boundary forward, then lower backward
61-
# Using fill only (no stroke) to avoid visual artifacts at polygon edges
62-
band_polygon = []
63-
# Upper boundary (forward)
64-
for xi, yi in zip(x, y_upper, strict=True):
65-
band_polygon.append((float(xi), float(yi)))
66-
# Lower boundary (backward to close the polygon smoothly)
67-
for xi, yi in zip(reversed(x), reversed(y_lower), strict=True):
68-
band_polygon.append((float(xi), float(yi)))
104+
# Band as closed polygon: upper boundary forward, then lower boundary reversed
105+
band_polygon = [(float(h), float(y)) for h, y in zip(hours, y_upper, strict=True)]
106+
for h, y in zip(reversed(hours), reversed(y_lower), strict=True):
107+
band_polygon.append((float(h), float(y)))
69108

70-
chart.add("Confidence Band", band_polygon, stroke=False)
109+
chart.add(
110+
"95% Confidence Band",
111+
band_polygon,
112+
stroke_style={"width": 0.5, "color": "#306998", "opacity": 0.15},
113+
show_dots=False,
114+
)
71115

72-
# Add center line (no fill, just stroke) - using a contrasting color
73-
center_data = [(float(xi), float(yi)) for xi, yi in zip(x, y_center, strict=True)]
74-
chart.add("Central Trend", center_data, fill=False, stroke=True, dots_size=0, stroke_style={"width": 6})
116+
# Central trend line — bold stroke with rounded SVG caps for smooth rendering
117+
center_data = [(float(h), float(y)) for h, y in zip(hours, y_center, strict=True)]
118+
chart.add(
119+
"Sensor Mean",
120+
center_data,
121+
fill=False,
122+
stroke=True,
123+
dots_size=0,
124+
stroke_style={"width": 48, "linecap": "round", "linejoin": "round"},
125+
)
126+
127+
# Wilting point reference — threshold below which plants cannot extract moisture
128+
chart.add(
129+
"Wilting Point (25%)",
130+
[(0.0, 25.0), (48.0, 25.0)],
131+
fill=False,
132+
stroke=True,
133+
dots_size=0,
134+
formatter=lambda x: f"{x:.0f}%",
135+
stroke_style={"width": 5, "dasharray": "16,10", "linecap": "round"},
136+
)
75137

76-
# Save outputs
138+
# Save
77139
chart.render_to_png("plot.png")
78140
chart.render_to_file("plot.html")

0 commit comments

Comments
 (0)