Skip to content

Commit 529134d

Browse files
feat(highcharts): implement raincloud-basic (#1956)
## Implementation: `raincloud-basic` - highcharts Implements the **highcharts** version of `raincloud-basic`. **File:** `plots/raincloud-basic/implementations/highcharts.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20501867092)* --------- 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 0d2a8cd commit 529134d

2 files changed

Lines changed: 164 additions & 186 deletions

File tree

plots/raincloud-basic/implementations/highcharts.py

Lines changed: 150 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
""" pyplots.ai
22
raincloud-basic: Basic Raincloud Plot
33
Library: highcharts unknown | Python 3.13.11
4-
Quality: 78/100 | Created: 2025-12-25
4+
Quality: 91/100 | Created: 2025-12-25
55
"""
66

7-
import json
87
import tempfile
98
import time
109
import urllib.request
1110
from pathlib import Path
1211

1312
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
1418
from selenium import webdriver
1519
from selenium.webdriver.chrome.options import Options
1620

@@ -19,8 +23,6 @@
1923
np.random.seed(42)
2024
categories = ["Control", "Treatment A", "Treatment B", "Treatment C"]
2125
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)"]
2426

2527
# Generate realistic reaction time data with different distributions
2628
control = np.random.normal(450, 60, 80) # Normal distribution
@@ -54,32 +56,89 @@
5456
}
5557
)
5658

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()
6462

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+
}
79139

80140
# 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)
83142
for i, data in enumerate(all_data):
84143
# Inline KDE computation (Gaussian kernel)
85144
data_arr = np.array(data)
@@ -94,152 +153,62 @@
94153
density = density / (n * bandwidth * np.sqrt(2 * np.pi))
95154
density = density / density.max() * 0.35
96155

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
98157
polygon_points = []
99158
# Right side: baseline at category, extend RIGHT (positive direction)
100159
for y, d in zip(y_range, density, strict=True):
101160
polygon_points.append([float(i + d + 0.05), float(y)])
102161
# Close polygon by going back along the baseline
103162
for y in reversed(y_range):
104163
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]})
108164

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)
128175

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)
243212

244213
# Download Highcharts JS and required modules
245214
highcharts_url = "https://code.highcharts.com/highcharts.js"
@@ -251,6 +220,7 @@
251220
highcharts_more_js = response.read().decode("utf-8")
252221

253222
# Generate HTML with inline scripts
223+
html_str = chart.to_js_literal()
254224
html_content = f"""<!DOCTYPE html>
255225
<html>
256226
<head>
@@ -260,22 +230,30 @@
260230
</head>
261231
<body style="margin:0;">
262232
<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>
268234
</body>
269235
</html>"""
270236

271-
# Write temp HTML and take screenshot
237+
# Write temp HTML file
272238
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
273239
f.write(html_content)
274240
temp_path = f.name
275241

276-
# Also save the HTML file
242+
# Save HTML for interactive viewing
277243
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)
279257

280258
# Setup Chrome for screenshot
281259
chrome_options = Options()

0 commit comments

Comments
 (0)