|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | strip-basic: Basic Strip Plot |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: highcharts unknown | Python 3.13.13 |
| 4 | +Quality: 89/100 | Updated: 2026-05-04 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
7 | 8 | import tempfile |
8 | 9 | import time |
9 | 10 | import urllib.request |
|
17 | 18 | from selenium.webdriver.chrome.options import Options |
18 | 19 |
|
19 | 20 |
|
20 | | -# Data - student test scores by subject |
| 21 | +# Theme tokens |
| 22 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 23 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 24 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 25 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 26 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 27 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 28 | +NEUTRAL = "#1A1A1A" if THEME == "light" else "#E8E8E0" |
| 29 | + |
| 30 | +# Okabe-Ito palette — first series is always #009E73 |
| 31 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"] |
| 32 | + |
| 33 | +# Data — student test scores by subject |
21 | 34 | np.random.seed(42) |
22 | 35 | categories = ["Mathematics", "Science", "Literature", "History"] |
23 | | -colors = ["#306998", "#FFD43B", "#9467BD", "#17BECF"] |
24 | 36 |
|
25 | | -# Generate realistic test score data (different distributions per subject) |
26 | 37 | raw_data = { |
27 | 38 | "Mathematics": np.concatenate( |
28 | 39 | [ |
29 | | - np.random.normal(72, 12, 35), # Wider spread |
30 | | - np.random.normal(90, 5, 10), # High performers |
| 40 | + np.random.normal(72, 12, 35), |
| 41 | + np.random.normal(90, 5, 10), # high-performer cluster (bimodal) |
31 | 42 | ] |
32 | 43 | ), |
33 | | - "Science": np.random.normal(75, 10, 40), # Normal distribution |
| 44 | + "Science": np.random.normal(75, 10, 40), |
34 | 45 | "Literature": np.concatenate( |
35 | 46 | [ |
36 | | - np.random.normal(65, 8, 25), # Lower cluster |
37 | | - np.random.normal(82, 6, 20), # Upper cluster (bimodal) |
| 47 | + np.random.normal(65, 8, 25), # lower cluster |
| 48 | + np.random.normal(82, 6, 20), # upper cluster (bimodal) |
38 | 49 | ] |
39 | 50 | ), |
40 | | - "History": np.random.normal(78, 9, 38), # Slightly higher scores |
| 51 | + "History": np.random.normal(78, 9, 38), |
41 | 52 | } |
42 | 53 |
|
43 | | -# Clip values to realistic range (0-100) |
44 | 54 | for cat in categories: |
45 | 55 | raw_data[cat] = np.clip(raw_data[cat], 30, 100) |
46 | 56 |
|
47 | | -# Create strip plot data with random jitter |
48 | | -jitter_width = 0.25 # Moderate jitter as per spec recommendation (0.1-0.3) |
| 57 | +jitter_width = 0.25 |
49 | 58 | strip_data = [] |
50 | 59 | for cat_idx, cat in enumerate(categories): |
51 | 60 | values = raw_data[cat] |
52 | | - # Random horizontal jitter within jitter_width |
53 | 61 | x_jitter = np.random.uniform(-jitter_width, jitter_width, len(values)) |
54 | | - |
55 | 62 | for val, x_off in zip(values, x_jitter, strict=True): |
56 | | - strip_data.append({"x": cat_idx + x_off, "y": float(val), "category": cat, "color": colors[cat_idx]}) |
| 63 | + strip_data.append({"x": cat_idx + x_off, "y": float(val), "category": cat}) |
57 | 64 |
|
58 | | -# Calculate mean for each category (for reference markers) |
59 | 65 | means = {cat: float(np.mean(raw_data[cat])) for cat in categories} |
60 | 66 |
|
61 | | -# Create chart |
| 67 | +# Plot |
62 | 68 | chart = Chart(container="container") |
63 | 69 | chart.options = HighchartsOptions() |
64 | 70 |
|
65 | | -# Chart configuration |
66 | 71 | chart.options.chart = { |
67 | 72 | "type": "scatter", |
68 | 73 | "width": 4800, |
69 | 74 | "height": 2700, |
70 | | - "backgroundColor": "#ffffff", |
| 75 | + "backgroundColor": PAGE_BG, |
71 | 76 | "marginBottom": 200, |
| 77 | + "style": {"fontFamily": "-apple-system, system-ui, sans-serif"}, |
72 | 78 | } |
73 | 79 |
|
74 | | -# Title |
75 | 80 | chart.options.title = { |
76 | | - "text": "strip-basic · highcharts · pyplots.ai", |
77 | | - "style": {"fontSize": "72px", "fontWeight": "bold"}, |
| 81 | + "text": "strip-basic · highcharts · anyplot.ai", |
| 82 | + "style": {"fontSize": "64px", "fontWeight": "bold", "color": INK}, |
78 | 83 | } |
79 | 84 |
|
80 | | -# Subtitle describing data |
81 | | -chart.options.subtitle = {"text": "Student Test Scores by Subject", "style": {"fontSize": "48px"}} |
| 85 | +chart.options.subtitle = {"text": "Student Test Scores by Subject", "style": {"fontSize": "40px", "color": INK_SOFT}} |
82 | 86 |
|
83 | | -# X-axis (categorical) |
84 | 87 | chart.options.x_axis = { |
85 | 88 | "categories": categories, |
86 | | - "title": {"text": "Subject", "style": {"fontSize": "48px"}}, |
87 | | - "labels": {"style": {"fontSize": "36px"}}, |
| 89 | + "title": {"text": "Subject", "style": {"fontSize": "40px", "color": INK}}, |
| 90 | + "labels": {"style": {"fontSize": "32px", "color": INK_SOFT}}, |
88 | 91 | "tickWidth": 0, |
| 92 | + "lineColor": INK_SOFT, |
89 | 93 | "lineWidth": 2, |
90 | 94 | "min": -0.5, |
91 | 95 | "max": len(categories) - 0.5, |
92 | 96 | "tickPositions": [0, 1, 2, 3], |
93 | 97 | } |
94 | 98 |
|
95 | | -# Y-axis |
96 | 99 | chart.options.y_axis = { |
97 | | - "title": {"text": "Test Score", "style": {"fontSize": "48px"}}, |
98 | | - "labels": {"style": {"fontSize": "36px"}}, |
| 100 | + "title": {"text": "Test Score (%)", "style": {"fontSize": "40px", "color": INK}}, |
| 101 | + "labels": {"style": {"fontSize": "32px", "color": INK_SOFT}}, |
99 | 102 | "gridLineWidth": 1, |
100 | | - "gridLineColor": "rgba(0, 0, 0, 0.1)", |
101 | | - "gridLineDashStyle": "Dash", |
| 103 | + "gridLineColor": GRID, |
| 104 | + "lineColor": INK_SOFT, |
102 | 105 | "min": 40, |
103 | 106 | "max": 105, |
104 | 107 | } |
105 | 108 |
|
106 | | -# Legend - position at top right to avoid cutting off |
107 | 109 | chart.options.legend = { |
108 | 110 | "enabled": True, |
109 | 111 | "align": "right", |
110 | 112 | "verticalAlign": "top", |
111 | 113 | "layout": "vertical", |
112 | 114 | "x": -50, |
113 | 115 | "y": 100, |
114 | | - "itemStyle": {"fontSize": "36px"}, |
| 116 | + "itemStyle": {"fontSize": "30px", "color": INK_SOFT}, |
| 117 | + "backgroundColor": ELEVATED_BG, |
| 118 | + "borderColor": INK_SOFT, |
| 119 | + "borderWidth": 1, |
115 | 120 | } |
116 | 121 |
|
117 | | -# Credits |
118 | 122 | chart.options.credits = {"enabled": False} |
119 | 123 |
|
120 | | -# Tooltip |
121 | 124 | chart.options.tooltip = { |
122 | 125 | "headerFormat": "", |
123 | | - "pointFormat": "<b>{point.category}</b><br/>Score: {point.y:.1f}", |
| 126 | + "pointFormat": "<b>{series.name}</b><br/>Score: {point.y:.1f}%", |
124 | 127 | "style": {"fontSize": "24px"}, |
125 | 128 | } |
126 | 129 |
|
127 | | -# Add scatter series for each category (for legend) |
| 130 | +# Add scatter series per category |
128 | 131 | for cat_idx, cat in enumerate(categories): |
129 | 132 | series = ScatterSeries() |
130 | 133 | series.name = cat |
131 | | - series.color = colors[cat_idx] |
132 | | - series.data = [{"x": float(pt["x"]), "y": pt["y"], "category": cat} for pt in strip_data if pt["category"] == cat] |
133 | | - series.marker = { |
134 | | - "radius": 14, |
135 | | - "symbol": "circle", |
136 | | - "fillColor": colors[cat_idx], |
137 | | - "fillOpacity": 0.6, # Transparency for overlapping points |
138 | | - "lineWidth": 2, |
139 | | - "lineColor": "#ffffff", |
140 | | - } |
| 134 | + series.color = OKABE_ITO[cat_idx] |
| 135 | + series.data = [{"x": float(pt["x"]), "y": pt["y"]} for pt in strip_data if pt["category"] == cat] |
| 136 | + series.marker = {"radius": 12, "symbol": "circle", "fillOpacity": 0.65, "lineWidth": 1, "lineColor": PAGE_BG} |
141 | 137 | chart.add_series(series) |
142 | 138 |
|
143 | | -# Add mean markers as horizontal line segments |
| 139 | +# Mean markers — adaptive neutral (Okabe-Ito position 8) |
144 | 140 | mean_series = ScatterSeries() |
145 | 141 | mean_series.name = "Mean" |
146 | 142 | mean_series.data = [{"x": float(i), "y": means[cat]} for i, cat in enumerate(categories)] |
147 | | -mean_series.marker = {"radius": 18, "symbol": "diamond", "fillColor": "#E74C3C", "lineWidth": 3, "lineColor": "#ffffff"} |
148 | | -mean_series.color = "#E74C3C" |
| 143 | +mean_series.color = NEUTRAL |
| 144 | +mean_series.marker = {"radius": 20, "symbol": "diamond", "lineWidth": 3, "lineColor": PAGE_BG} |
149 | 145 | chart.add_series(mean_series) |
150 | 146 |
|
151 | | -# Download Highcharts JS (required for headless Chrome) |
152 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
| 147 | +# Download Highcharts JS inline (headless Chrome cannot load CDN from file://) |
| 148 | +highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts/highcharts.js" |
153 | 149 | with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
154 | 150 | highcharts_js = response.read().decode("utf-8") |
155 | 151 |
|
156 | | -# Generate HTML with inline scripts |
157 | | -html_str = chart.to_js_literal() |
158 | | -html_content = f"""<!DOCTYPE html> |
| 152 | +js_literal = chart.to_js_literal() |
| 153 | + |
| 154 | +html_inline = f"""<!DOCTYPE html> |
159 | 155 | <html> |
160 | 156 | <head> |
161 | 157 | <meta charset="utf-8"> |
162 | 158 | <script>{highcharts_js}</script> |
163 | 159 | </head> |
164 | | -<body style="margin:0;"> |
165 | | - <div id="container" style="width: 4800px; height: 2700px;"></div> |
166 | | - <script>{html_str}</script> |
| 160 | +<body style="margin:0; background:{PAGE_BG};"> |
| 161 | + <div id="container" style="width:4800px; height:2700px;"></div> |
| 162 | + <script>{js_literal}</script> |
| 163 | +</body> |
| 164 | +</html>""" |
| 165 | + |
| 166 | +# Site artifact — CDN keeps file size small for served pages |
| 167 | +html_cdn = f"""<!DOCTYPE html> |
| 168 | +<html> |
| 169 | +<head> |
| 170 | + <meta charset="utf-8"> |
| 171 | + <script src="https://cdn.jsdelivr.net/npm/highcharts/highcharts.js"></script> |
| 172 | +</head> |
| 173 | +<body style="margin:0; background:{PAGE_BG};"> |
| 174 | + <div id="container" style="width:100%; height:100vh;"></div> |
| 175 | + <script>{js_literal}</script> |
167 | 176 | </body> |
168 | 177 | </html>""" |
169 | 178 |
|
170 | | -# Write temp HTML and take screenshot |
| 179 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
| 180 | + f.write(html_cdn) |
| 181 | + |
| 182 | +# Save |
171 | 183 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
172 | | - f.write(html_content) |
| 184 | + f.write(html_inline) |
173 | 185 | temp_path = f.name |
174 | 186 |
|
175 | 187 | chrome_options = Options() |
176 | | -chrome_options.add_argument("--headless") |
| 188 | +chrome_options.add_argument("--headless=new") |
177 | 189 | chrome_options.add_argument("--no-sandbox") |
178 | 190 | chrome_options.add_argument("--disable-dev-shm-usage") |
179 | 191 | chrome_options.add_argument("--disable-gpu") |
180 | | -chrome_options.add_argument("--window-size=4800,2800") |
| 192 | +chrome_options.add_argument("--window-size=4800,2900") |
| 193 | +chrome_options.add_argument("--hide-scrollbars") |
181 | 194 |
|
182 | 195 | driver = webdriver.Chrome(options=chrome_options) |
| 196 | +driver.set_window_size(4800, 2900) |
183 | 197 | driver.get(f"file://{temp_path}") |
184 | 198 | time.sleep(5) |
185 | | - |
186 | | -# Take screenshot of just the chart container element |
187 | 199 | container = driver.find_element("id", "container") |
188 | | -container.screenshot("plot.png") |
| 200 | +container.screenshot(f"plot-{THEME}.png") |
189 | 201 | driver.quit() |
190 | 202 |
|
191 | 203 | Path(temp_path).unlink() |
192 | | - |
193 | | -# Also save HTML for interactive version |
194 | | -with open("plot.html", "w", encoding="utf-8") as f: |
195 | | - interactive_html = f"""<!DOCTYPE html> |
196 | | -<html> |
197 | | -<head> |
198 | | - <meta charset="utf-8"> |
199 | | - <script src="https://code.highcharts.com/highcharts.js"></script> |
200 | | -</head> |
201 | | -<body style="margin:0;"> |
202 | | - <div id="container" style="width: 100%; height: 100vh;"></div> |
203 | | - <script>{html_str}</script> |
204 | | -</body> |
205 | | -</html>""" |
206 | | - f.write(interactive_html) |
0 commit comments