|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | scatter-basic: Basic Scatter Plot |
3 | | -Library: pygal 3.1.0 | Python 3.14 |
4 | | -Quality: 87/100 | Created: 2025-12-22 |
| 3 | +Library: pygal 3.1.0 | Python 3.14.4 |
| 4 | +Quality: 87/100 | Created: 2026-04-23 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import numpy as np |
8 | | -import pygal |
9 | | -from pygal.style import Style |
| 7 | +import os |
| 8 | +import sys |
10 | 9 |
|
11 | 10 |
|
12 | | -# Data — study hours vs exam scores with realistic positive correlation |
13 | | -np.random.seed(42) |
14 | | -n = 115 |
15 | | -study_hours = np.random.uniform(2, 14, n) |
16 | | -exam_scores = study_hours * 4.5 + np.random.normal(0, 5.5, n) + 25 |
17 | | -exam_scores = np.clip(exam_scores, 15, 100) |
| 11 | +# Script filename shadows the installed `pygal` package when run as `python pygal.py`; |
| 12 | +# dropping the script directory from sys.path lets the real package resolve. |
| 13 | +sys.path.pop(0) |
| 14 | + |
| 15 | +import numpy as np # noqa: E402 |
| 16 | +import pygal # noqa: E402 |
| 17 | +from pygal.style import Style # noqa: E402 |
| 18 | + |
18 | 19 |
|
19 | | -# Add deliberate outliers showcasing scatter diversity (high/low performers) |
20 | | -outlier_hours = np.array([3.0, 12.5, 7.0, 11.0, 4.5]) |
21 | | -outlier_scores = np.array([82.0, 42.0, 95.0, 48.0, 78.0]) |
22 | | -study_hours = np.concatenate([study_hours, outlier_hours]) |
23 | | -exam_scores = np.concatenate([exam_scores, outlier_scores]) |
| 20 | +# Theme tokens |
| 21 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 22 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 23 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 24 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 25 | +INK_MUTED = "#8A8A82" if THEME == "light" else "#6E6D66" |
24 | 26 |
|
25 | | -# Compute trend line (linear regression) for data storytelling |
26 | | -coeffs = np.polyfit(study_hours, exam_scores, 1) |
27 | | -slope, intercept = coeffs |
28 | | -r = np.corrcoef(study_hours, exam_scores)[0, 1] |
29 | | -trend_x = np.linspace(study_hours.min(), study_hours.max(), 50) |
30 | | -trend_y = slope * trend_x + intercept |
| 27 | +OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442") |
| 28 | + |
| 29 | +# Data — study hours vs exam scores, moderate positive correlation |
| 30 | +np.random.seed(42) |
| 31 | +n = 180 |
| 32 | +study_hours = np.random.uniform(1.5, 13.5, n) |
| 33 | +exam_scores = study_hours * 4.8 + np.random.normal(0, 6.5, n) + 26 |
| 34 | +exam_scores = np.clip(exam_scores, 20, 100) |
31 | 35 |
|
32 | | -# Identify notable outliers for annotation |
33 | | -residuals = exam_scores - (slope * study_hours + intercept) |
34 | | -top_outlier_idx = int(np.argmax(residuals)) |
35 | | -bottom_outlier_idx = int(np.argmin(residuals)) |
| 36 | +# Visual hierarchy: split at conventional 70% passing threshold to guide the |
| 37 | +# viewer's eye and convey the "hours → outcome" narrative beyond a raw cloud. |
| 38 | +PASSING = 70.0 |
| 39 | +above = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s >= PASSING] |
| 40 | +below = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s < PASSING] |
36 | 41 |
|
37 | | -# Shared font family |
38 | 42 | font = "DejaVu Sans, Helvetica, Arial, sans-serif" |
39 | 43 |
|
40 | | -# Refined style for 4800x2700 px canvas — subtle, professional palette |
41 | 44 | custom_style = Style( |
42 | | - background="white", |
43 | | - plot_background="#f7f7f7", |
44 | | - foreground="#2a2a2a", |
45 | | - foreground_strong="#2a2a2a", |
46 | | - foreground_subtle="#e0e0e0", |
47 | | - guide_stroke_color="#e0e0e0", |
48 | | - guide_stroke_dasharray="4, 4", |
49 | | - colors=("#306998", "#d64541", "#e8a838"), |
| 45 | + background=PAGE_BG, |
| 46 | + plot_background=PAGE_BG, |
| 47 | + foreground=INK_SOFT, |
| 48 | + foreground_strong=INK, |
| 49 | + foreground_subtle=INK_MUTED, |
| 50 | + colors=OKABE_ITO, |
50 | 51 | font_family=font, |
51 | 52 | title_font_family=font, |
52 | | - title_font_size=56, |
53 | | - label_font_size=42, |
54 | | - major_label_font_size=38, |
55 | | - legend_font_size=34, |
| 53 | + label_font_family=font, |
| 54 | + major_label_font_family=font, |
56 | 55 | legend_font_family=font, |
57 | | - value_font_size=28, |
58 | | - tooltip_font_size=28, |
59 | 56 | tooltip_font_family=font, |
60 | | - opacity=0.65, |
| 57 | + title_font_size=52, |
| 58 | + label_font_size=40, |
| 59 | + major_label_font_size=36, |
| 60 | + legend_font_size=34, |
| 61 | + tooltip_font_size=28, |
| 62 | + value_font_size=26, |
| 63 | + opacity=0.7, |
61 | 64 | opacity_hover=0.95, |
62 | 65 | stroke_opacity=1, |
63 | 66 | stroke_opacity_hover=1, |
64 | 67 | ) |
65 | 68 |
|
66 | | -# Axis range tightened to data bounds for better canvas utilization |
67 | | -x_min, x_max = float(np.floor(study_hours.min())), float(np.ceil(study_hours.max())) |
68 | | -y_min = float(max(0, np.floor(exam_scores.min() / 5) * 5)) |
69 | | -y_max = float(min(100, np.ceil(exam_scores.max() / 5) * 5 + 5)) |
70 | | - |
71 | | -# Create XY chart |
72 | 69 | chart = pygal.XY( |
73 | 70 | width=4800, |
74 | 71 | height=2700, |
75 | 72 | style=custom_style, |
76 | | - title="scatter-basic \u00b7 pygal \u00b7 pyplots.ai", |
| 73 | + title="scatter-basic · pygal · anyplot.ai", |
77 | 74 | x_title="Study Hours per Week (hrs)", |
78 | 75 | y_title="Exam Score (%)", |
| 76 | + stroke=False, |
| 77 | + dots_size=17, |
79 | 78 | show_legend=True, |
80 | 79 | legend_at_bottom=True, |
81 | | - legend_at_bottom_columns=3, |
82 | | - legend_box_size=24, |
83 | | - stroke=False, |
84 | | - dots_size=9, |
| 80 | + legend_at_bottom_columns=2, |
| 81 | + legend_box_size=32, |
85 | 82 | show_x_guides=True, |
86 | 83 | show_y_guides=True, |
87 | | - x_value_formatter=lambda x: f"{x:.0f}", |
88 | | - value_formatter=lambda y: f"{y:.0f}%", |
89 | | - margin_bottom=100, |
90 | | - margin_left=60, |
91 | | - margin_right=40, |
92 | | - margin_top=50, |
93 | | - x_label_rotation=0, |
94 | | - truncate_legend=-1, |
95 | | - range=(y_min, y_max), |
96 | | - xrange=(x_min, x_max), |
| 84 | + x_value_formatter=lambda v: f"{v:.0f}", |
| 85 | + value_formatter=lambda v: f"{v:.0f}%", |
| 86 | + range=(15, 100), |
| 87 | + xrange=(1, 14), |
97 | 88 | x_labels_major_count=7, |
98 | 89 | y_labels_major_count=9, |
| 90 | + margin=60, |
99 | 91 | print_values=False, |
100 | | - print_zeroes=False, |
101 | 92 | js=[], |
102 | 93 | ) |
103 | 94 |
|
104 | | -# Add scatter data as list of (x, y) tuples |
105 | | -points = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True)] |
106 | | -chart.add( |
107 | | - f"Students (n={len(points)})", |
108 | | - points, |
109 | | - stroke=False, |
110 | | - formatter=lambda x: f"({x[0]:.1f} hrs, {x[1]:.0f}%)" if isinstance(x, (tuple, list)) else f"{x:.0f}", |
111 | | -) |
112 | | - |
113 | | -# Add trend line — dashed stroke for visual contrast |
114 | | -trend_points = [(float(x), float(y)) for x, y in zip(trend_x, trend_y, strict=True)] |
115 | | -chart.add( |
116 | | - f"Trend (r = {r:.2f})", |
117 | | - trend_points, |
118 | | - stroke=True, |
119 | | - show_dots=False, |
120 | | - stroke_style={"width": 14, "dasharray": "32, 14", "linecap": "round", "linejoin": "round"}, |
121 | | -) |
122 | | - |
123 | | -# Annotate notable outliers — pygal per-point metadata with label styling |
124 | | -oh = float(study_hours[top_outlier_idx]) |
125 | | -os_ = float(exam_scores[top_outlier_idx]) |
126 | | -bh = float(study_hours[bottom_outlier_idx]) |
127 | | -bs = float(exam_scores[bottom_outlier_idx]) |
128 | | -chart.add( |
129 | | - "Outliers", |
130 | | - [ |
131 | | - {"value": (oh, os_), "label": f"High performer ({oh:.0f}h \u2192 {os_:.0f}%)"}, |
132 | | - {"value": (bh, bs), "label": f"Low performer ({bh:.0f}h \u2192 {bs:.0f}%)"}, |
133 | | - ], |
134 | | - stroke=False, |
135 | | - dots_size=16, |
136 | | - formatter=lambda x: f"{x[1]:.0f}%" if isinstance(x, (tuple, list)) else f"{x:.0f}", |
137 | | -) |
| 95 | +chart.add("Passing (≥ 70%)", above) |
| 96 | +chart.add("Below 70%", below) |
138 | 97 |
|
139 | | -# Save outputs — dual format leverages pygal's SVG-native + PNG capability |
140 | | -chart.render_to_png("plot.png") |
141 | | -chart.render_to_file("plot.html") |
| 98 | +chart.render_to_png(f"plot-{THEME}.png") |
| 99 | +with open(f"plot-{THEME}.html", "wb") as f: |
| 100 | + f.write(chart.render()) |
0 commit comments