|
1 | 1 | """ pyplots.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
8 | 8 | import pygal |
9 | 9 | from pygal.style import Style |
10 | 10 |
|
11 | 11 |
|
12 | | -# Data - Time series with 95% confidence interval |
| 12 | +# Data - Soil moisture sensor readings with 95% confidence interval |
13 | 13 | 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 |
21 | 32 | y_lower = y_center - uncertainty |
22 | 33 | y_upper = y_center + uncertainty |
23 | 34 |
|
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 |
25 | 41 | custom_style = Style( |
26 | 42 | background="white", |
27 | 43 | 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, |
36 | 57 | title_font_size=60, |
37 | 58 | label_font_size=42, |
38 | 59 | major_label_font_size=42, |
39 | | - legend_font_size=42, |
| 60 | + legend_font_size=40, |
40 | 61 | value_font_size=36, |
| 62 | + value_colors=("transparent",), |
| 63 | + tooltip_font_size=32, |
| 64 | + font_family='Helvetica, Arial, "DejaVu Sans", sans-serif', |
41 | 65 | ) |
42 | 66 |
|
43 | | -# Create XY chart for precise coordinate control |
| 67 | +# Create XY chart with fine-tuned layout and pygal-specific configuration |
44 | 68 | chart = pygal.XY( |
45 | 69 | style=custom_style, |
46 | 70 | width=4800, |
47 | 71 | 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 (%)", |
51 | 76 | show_dots=False, |
52 | | - show_x_guides=True, |
| 77 | + show_x_guides=False, |
53 | 78 | show_y_guides=True, |
54 | 79 | fill=True, |
55 | 80 | stroke=True, |
56 | 81 | legend_at_bottom=True, |
| 82 | + legend_at_bottom_columns=3, |
| 83 | + legend_box_size=28, |
57 | 84 | 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=[], |
58 | 102 | ) |
59 | 103 |
|
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))) |
69 | 108 |
|
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 | +) |
71 | 115 |
|
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 | +) |
75 | 137 |
|
76 | | -# Save outputs |
| 138 | +# Save |
77 | 139 | chart.render_to_png("plot.png") |
78 | 140 | chart.render_to_file("plot.html") |
0 commit comments