|
1 | 1 | """ pyplots.ai |
2 | 2 | raincloud-basic: Basic Raincloud Plot |
3 | 3 | Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 78/100 | Created: 2025-12-25 |
| 4 | +Quality: 91/100 | Created: 2025-12-25 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import json |
8 | 7 | import tempfile |
9 | 8 | import time |
10 | 9 | import urllib.request |
11 | 10 | from pathlib import Path |
12 | 11 |
|
13 | 12 | import numpy as np |
| 13 | +from highcharts_core.chart import Chart |
| 14 | +from highcharts_core.options import HighchartsOptions |
| 15 | +from highcharts_core.options.series.boxplot import BoxPlotSeries |
| 16 | +from highcharts_core.options.series.polygon import PolygonSeries |
| 17 | +from highcharts_core.options.series.scatter import ScatterSeries |
14 | 18 | from selenium import webdriver |
15 | 19 | from selenium.webdriver.chrome.options import Options |
16 | 20 |
|
|
19 | 23 | np.random.seed(42) |
20 | 24 | categories = ["Control", "Treatment A", "Treatment B", "Treatment C"] |
21 | 25 | colors = ["#306998", "#FFD43B", "#9467BD", "#17BECF"] |
22 | | -# Fill colors for box plots with good visibility (increased opacity) |
23 | | -box_fill_colors = ["rgba(48,105,152,0.7)", "rgba(255,212,59,0.7)", "rgba(148,103,189,0.7)", "rgba(23,190,207,0.7)"] |
24 | 26 |
|
25 | 27 | # Generate realistic reaction time data with different distributions |
26 | 28 | control = np.random.normal(450, 60, 80) # Normal distribution |
|
54 | 56 | } |
55 | 57 | ) |
56 | 58 |
|
57 | | -# Create jittered scatter data (the "rain" - falls LEFT of the cloud for vertical orientation) |
58 | | -scatter_data = [] |
59 | | -for i, data in enumerate(all_data): |
60 | | - for val in data: |
61 | | - jitter = np.random.uniform(-0.08, 0.08) |
62 | | - # Rain on LEFT side (negative offset from category center) |
63 | | - scatter_data.append({"x": i - 0.25 + jitter, "y": float(val), "color": colors[i]}) |
| 59 | +# Create chart using highcharts_core |
| 60 | +chart = Chart(container="container") |
| 61 | +chart.options = HighchartsOptions() |
64 | 62 |
|
65 | | -# Box plot series data with semi-transparent fill and dark borders |
66 | | -box_series_data = [] |
67 | | -for i, box in enumerate(box_data): |
68 | | - box_series_data.append( |
69 | | - { |
70 | | - "low": box["low"], |
71 | | - "q1": box["q1"], |
72 | | - "median": box["median"], |
73 | | - "q3": box["q3"], |
74 | | - "high": box["high"], |
75 | | - "color": "#1a1a1a", # Dark border for visibility |
76 | | - "fillColor": box_fill_colors[i], |
77 | | - } |
78 | | - ) |
| 63 | +# Chart configuration |
| 64 | +chart.options.chart = { |
| 65 | + "type": "boxplot", |
| 66 | + "width": 4800, |
| 67 | + "height": 2700, |
| 68 | + "backgroundColor": "#ffffff", |
| 69 | + "marginBottom": 280, |
| 70 | + "marginLeft": 220, |
| 71 | + "marginRight": 200, |
| 72 | + "spacingBottom": 80, |
| 73 | +} |
| 74 | + |
| 75 | +# Title |
| 76 | +chart.options.title = { |
| 77 | + "text": "raincloud-basic · highcharts · pyplots.ai", |
| 78 | + "style": {"fontSize": "56px", "fontWeight": "bold"}, |
| 79 | +} |
| 80 | + |
| 81 | +# X-axis (categories) |
| 82 | +chart.options.x_axis = { |
| 83 | + "title": {"text": "Experimental Condition", "style": {"fontSize": "44px"}}, |
| 84 | + "labels": {"style": {"fontSize": "36px"}}, |
| 85 | + "categories": categories, |
| 86 | + "lineWidth": 2, |
| 87 | + "tickWidth": 2, |
| 88 | + "min": -0.5, |
| 89 | + "max": 3.5, |
| 90 | + "tickPositions": [0, 1, 2, 3], |
| 91 | +} |
| 92 | + |
| 93 | +# Y-axis (values) |
| 94 | +chart.options.y_axis = { |
| 95 | + "title": {"text": "Reaction Time (ms)", "style": {"fontSize": "44px"}}, |
| 96 | + "labels": {"style": {"fontSize": "36px"}}, |
| 97 | + "gridLineWidth": 1, |
| 98 | + "gridLineColor": "rgba(0, 0, 0, 0.15)", |
| 99 | + "gridLineDashStyle": "Dash", |
| 100 | + "tickInterval": 50, |
| 101 | + "min": 250, |
| 102 | + "max": 650, |
| 103 | +} |
| 104 | + |
| 105 | +# Legend |
| 106 | +chart.options.legend = { |
| 107 | + "enabled": True, |
| 108 | + "itemStyle": {"fontSize": "36px"}, |
| 109 | + "align": "right", |
| 110 | + "verticalAlign": "top", |
| 111 | + "layout": "vertical", |
| 112 | + "x": -50, |
| 113 | + "y": 100, |
| 114 | + "backgroundColor": "rgba(255, 255, 255, 0.9)", |
| 115 | + "borderWidth": 1, |
| 116 | + "borderColor": "#cccccc", |
| 117 | + "padding": 20, |
| 118 | + "symbolWidth": 40, |
| 119 | + "symbolHeight": 24, |
| 120 | +} |
| 121 | + |
| 122 | +# Plot options |
| 123 | +chart.options.plot_options = { |
| 124 | + "boxplot": { |
| 125 | + "medianColor": "#000000", |
| 126 | + "medianWidth": 8, |
| 127 | + "medianDashStyle": "Solid", |
| 128 | + "stemColor": "#1a1a1a", |
| 129 | + "stemWidth": 4, |
| 130 | + "whiskerColor": "#1a1a1a", |
| 131 | + "whiskerWidth": 5, |
| 132 | + "whiskerLength": "50%", |
| 133 | + "lineWidth": 4, |
| 134 | + "pointWidth": 70, |
| 135 | + }, |
| 136 | + "scatter": {"marker": {"radius": 16, "symbol": "circle"}}, |
| 137 | + "polygon": {"fillOpacity": 0.6, "lineWidth": 2}, |
| 138 | +} |
79 | 139 |
|
80 | 140 | # Create polygon data for half-violin (the "cloud") - inline KDE |
81 | | -# Cloud on RIGHT side for vertical orientation (rain falls from cloud, so cloud is RIGHT/TOP) |
82 | | -violin_polygons = [] |
| 141 | +# Cloud on RIGHT side for vertical orientation (rain falls from cloud) |
83 | 142 | for i, data in enumerate(all_data): |
84 | 143 | # Inline KDE computation (Gaussian kernel) |
85 | 144 | data_arr = np.array(data) |
|
94 | 153 | density = density / (n * bandwidth * np.sqrt(2 * np.pi)) |
95 | 154 | density = density / density.max() * 0.35 |
96 | 155 |
|
97 | | - # Create polygon points for filled half-violin on RIGHT side (close the polygon) |
| 156 | + # Create polygon points for filled half-violin on RIGHT side |
98 | 157 | polygon_points = [] |
99 | 158 | # Right side: baseline at category, extend RIGHT (positive direction) |
100 | 159 | for y, d in zip(y_range, density, strict=True): |
101 | 160 | polygon_points.append([float(i + d + 0.05), float(y)]) |
102 | 161 | # Close polygon by going back along the baseline |
103 | 162 | for y in reversed(y_range): |
104 | 163 | polygon_points.append([float(i + 0.05), float(y)]) |
105 | | - # Close the polygon |
106 | | - polygon_points.append(polygon_points[0]) |
107 | | - violin_polygons.append({"points": polygon_points, "color": colors[i]}) |
108 | 164 |
|
109 | | -# Build chart JavaScript with polygon series for clouds |
110 | | -polygon_series_js = [] |
111 | | -for idx, poly in enumerate(violin_polygons): |
112 | | - show_legend = "true" if idx == 0 else "false" |
113 | | - linked = "" if idx == 0 else "linkedTo: ':previous'," |
114 | | - polygon_series_js.append(f"""{{ |
115 | | - name: 'Density Cloud', |
116 | | - type: 'polygon', |
117 | | - data: {json.dumps(poly["points"])}, |
118 | | - color: '{poly["color"]}', |
119 | | - fillOpacity: 0.6, |
120 | | - lineWidth: 2, |
121 | | - lineColor: '{poly["color"]}', |
122 | | - enableMouseTracking: false, |
123 | | - showInLegend: {show_legend}, |
124 | | - {linked} |
125 | | - legendSymbol: 'areaMarker', |
126 | | - marker: {{ enabled: false }} |
127 | | - }}""") |
| 165 | + series = PolygonSeries() |
| 166 | + series.data = polygon_points |
| 167 | + series.name = f"{categories[i]} (Cloud)" |
| 168 | + series.color = colors[i] |
| 169 | + series.fill_color = colors[i] |
| 170 | + series.fill_opacity = 0.6 |
| 171 | + series.line_width = 2 |
| 172 | + series.line_color = colors[i] |
| 173 | + series.enable_mouse_tracking = False |
| 174 | + chart.add_series(series) |
128 | 175 |
|
129 | | -chart_js = f""" |
130 | | -Highcharts.chart('container', {{ |
131 | | - chart: {{ |
132 | | - width: 4800, |
133 | | - height: 2700, |
134 | | - backgroundColor: '#ffffff', |
135 | | - marginBottom: 280, |
136 | | - marginLeft: 220, |
137 | | - marginRight: 200, |
138 | | - spacingBottom: 80 |
139 | | - }}, |
140 | | - title: {{ |
141 | | - text: 'raincloud-basic · highcharts · pyplots.ai', |
142 | | - style: {{ fontSize: '56px', fontWeight: 'bold' }} |
143 | | - }}, |
144 | | - xAxis: {{ |
145 | | - categories: {json.dumps(categories)}, |
146 | | - title: {{ |
147 | | - text: 'Experimental Condition', |
148 | | - style: {{ fontSize: '44px' }} |
149 | | - }}, |
150 | | - labels: {{ |
151 | | - style: {{ fontSize: '36px' }} |
152 | | - }}, |
153 | | - lineWidth: 2, |
154 | | - tickWidth: 2, |
155 | | - min: -0.5, |
156 | | - max: 3.5, |
157 | | - tickPositions: [0, 1, 2, 3] |
158 | | - }}, |
159 | | - yAxis: {{ |
160 | | - title: {{ |
161 | | - text: 'Reaction Time (ms)', |
162 | | - style: {{ fontSize: '44px' }} |
163 | | - }}, |
164 | | - labels: {{ |
165 | | - style: {{ fontSize: '36px' }} |
166 | | - }}, |
167 | | - gridLineWidth: 2, |
168 | | - gridLineColor: 'rgba(0, 0, 0, 0.35)', |
169 | | - gridLineDashStyle: 'Solid', |
170 | | - tickInterval: 50, |
171 | | - min: 250, |
172 | | - max: 650 |
173 | | - }}, |
174 | | - legend: {{ |
175 | | - enabled: true, |
176 | | - itemStyle: {{ fontSize: '36px' }}, |
177 | | - align: 'right', |
178 | | - verticalAlign: 'top', |
179 | | - layout: 'vertical', |
180 | | - x: -50, |
181 | | - y: 100, |
182 | | - backgroundColor: 'rgba(255, 255, 255, 0.9)', |
183 | | - borderWidth: 1, |
184 | | - borderColor: '#cccccc', |
185 | | - padding: 20 |
186 | | - }}, |
187 | | - plotOptions: {{ |
188 | | - boxplot: {{ |
189 | | - medianColor: '#000000', |
190 | | - medianWidth: 8, |
191 | | - medianDashStyle: 'Solid', |
192 | | - stemColor: '#1a1a1a', |
193 | | - stemWidth: 4, |
194 | | - whiskerColor: '#1a1a1a', |
195 | | - whiskerWidth: 5, |
196 | | - whiskerLength: '50%', |
197 | | - lineWidth: 4, |
198 | | - pointWidth: 70 |
199 | | - }}, |
200 | | - scatter: {{ |
201 | | - marker: {{ |
202 | | - radius: 18, |
203 | | - symbol: 'circle' |
204 | | - }} |
205 | | - }}, |
206 | | - polygon: {{ |
207 | | - fillOpacity: 0.6, |
208 | | - lineWidth: 2 |
209 | | - }} |
210 | | - }}, |
211 | | - series: [ |
212 | | - {",".join(polygon_series_js)}, |
213 | | - {{ |
214 | | - name: 'Box Plot (Q1-Q3)', |
215 | | - type: 'boxplot', |
216 | | - data: {json.dumps(box_series_data)}, |
217 | | - colorByPoint: true, |
218 | | - showInLegend: true, |
219 | | - legendSymbol: 'rectangle', |
220 | | - color: '#1a1a1a', |
221 | | - tooltip: {{ |
222 | | - headerFormat: '<b>{{point.key}}</b><br/>', |
223 | | - pointFormat: 'Max: {{point.high:.0f}} ms<br/>Q3: {{point.q3:.0f}} ms<br/>Median: {{point.median:.0f}} ms<br/>Q1: {{point.q1:.0f}} ms<br/>Min: {{point.low:.0f}} ms' |
224 | | - }} |
225 | | - }}, |
226 | | - {{ |
227 | | - name: 'Individual Points', |
228 | | - type: 'scatter', |
229 | | - data: {json.dumps(scatter_data)}, |
230 | | - marker: {{ |
231 | | - radius: 16, |
232 | | - lineWidth: 2, |
233 | | - lineColor: 'rgba(0,0,0,0.4)' |
234 | | - }}, |
235 | | - opacity: 0.65, |
236 | | - tooltip: {{ |
237 | | - pointFormat: 'Value: {{point.y:.0f}} ms' |
238 | | - }} |
239 | | - }} |
240 | | - ] |
241 | | -}}); |
242 | | -""" |
| 176 | +# Box plot series - one point per category |
| 177 | +box_series = BoxPlotSeries() |
| 178 | +box_series_data = [] |
| 179 | +for i, box in enumerate(box_data): |
| 180 | + box_series_data.append( |
| 181 | + { |
| 182 | + "low": box["low"], |
| 183 | + "q1": box["q1"], |
| 184 | + "median": box["median"], |
| 185 | + "q3": box["q3"], |
| 186 | + "high": box["high"], |
| 187 | + "color": "#1a1a1a", |
| 188 | + "fillColor": f"rgba({int(colors[i][1:3], 16)},{int(colors[i][3:5], 16)},{int(colors[i][5:7], 16)},0.85)", |
| 189 | + } |
| 190 | + ) |
| 191 | +box_series.data = box_series_data |
| 192 | +box_series.name = "Box Plot" |
| 193 | +box_series.color = "#1a1a1a" |
| 194 | +box_series.color_by_point = True |
| 195 | +chart.add_series(box_series) |
| 196 | + |
| 197 | +# Create jittered scatter data (the "rain") - one series per category for legend |
| 198 | +for i, data in enumerate(all_data): |
| 199 | + scatter_points = [] |
| 200 | + for val in data: |
| 201 | + jitter = np.random.uniform(-0.08, 0.08) |
| 202 | + # Rain on LEFT side (negative offset from category center) |
| 203 | + scatter_points.append([float(i - 0.25 + jitter), float(val)]) |
| 204 | + |
| 205 | + scatter_series = ScatterSeries() |
| 206 | + scatter_series.data = scatter_points |
| 207 | + scatter_series.name = f"{categories[i]} (Points)" |
| 208 | + scatter_series.color = colors[i] |
| 209 | + scatter_series.opacity = 0.65 |
| 210 | + scatter_series.marker = {"radius": 16, "lineWidth": 2, "lineColor": "rgba(0,0,0,0.4)", "fillColor": colors[i]} |
| 211 | + chart.add_series(scatter_series) |
243 | 212 |
|
244 | 213 | # Download Highcharts JS and required modules |
245 | 214 | highcharts_url = "https://code.highcharts.com/highcharts.js" |
|
251 | 220 | highcharts_more_js = response.read().decode("utf-8") |
252 | 221 |
|
253 | 222 | # Generate HTML with inline scripts |
| 223 | +html_str = chart.to_js_literal() |
254 | 224 | html_content = f"""<!DOCTYPE html> |
255 | 225 | <html> |
256 | 226 | <head> |
|
260 | 230 | </head> |
261 | 231 | <body style="margin:0;"> |
262 | 232 | <div id="container" style="width: 4800px; height: 2700px;"></div> |
263 | | - <script> |
264 | | - document.addEventListener('DOMContentLoaded', function() {{ |
265 | | - {chart_js} |
266 | | - }}); |
267 | | - </script> |
| 233 | + <script>{html_str}</script> |
268 | 234 | </body> |
269 | 235 | </html>""" |
270 | 236 |
|
271 | | -# Write temp HTML and take screenshot |
| 237 | +# Write temp HTML file |
272 | 238 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
273 | 239 | f.write(html_content) |
274 | 240 | temp_path = f.name |
275 | 241 |
|
276 | | -# Also save the HTML file |
| 242 | +# Save HTML for interactive viewing |
277 | 243 | with open("plot.html", "w", encoding="utf-8") as f: |
278 | | - f.write(html_content) |
| 244 | + standalone_html = f"""<!DOCTYPE html> |
| 245 | +<html> |
| 246 | +<head> |
| 247 | + <meta charset="utf-8"> |
| 248 | + <script src="https://code.highcharts.com/highcharts.js"></script> |
| 249 | + <script src="https://code.highcharts.com/highcharts-more.js"></script> |
| 250 | +</head> |
| 251 | +<body style="margin:0;"> |
| 252 | + <div id="container" style="width: 100%; height: 100vh;"></div> |
| 253 | + <script>{html_str}</script> |
| 254 | +</body> |
| 255 | +</html>""" |
| 256 | + f.write(standalone_html) |
279 | 257 |
|
280 | 258 | # Setup Chrome for screenshot |
281 | 259 | chrome_options = Options() |
|
0 commit comments