Skip to content

Commit 6e56842

Browse files
feat(highcharts): implement ridgeline-basic (#549)
## Summary Implements `ridgeline-basic` for **highcharts**. **Parent Issue:** #539 **Base Branch:** `plot/ridgeline-basic` ## Files - `plots/ridgeline-basic/implementations/highcharts.py` ## Preview Preview will be uploaded to GCS staging after this workflow completes. Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 345dee1 commit 6e56842

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""
2+
ridgeline-basic: Ridgeline Plot
3+
Library: highcharts
4+
"""
5+
6+
import tempfile
7+
import time
8+
import urllib.request
9+
from pathlib import Path
10+
11+
import numpy as np
12+
from highcharts_core.chart import Chart
13+
from highcharts_core.options import HighchartsOptions
14+
from highcharts_core.options.series.area import AreaSplineSeries
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Simple KDE implementation using Gaussian kernel
20+
def gaussian_kde(data, x_points, bandwidth=None):
21+
"""Compute kernel density estimate using Gaussian kernel."""
22+
n = len(data)
23+
if bandwidth is None:
24+
bandwidth = 1.06 * np.std(data) * n ** (-1 / 5)
25+
density = np.zeros_like(x_points, dtype=float)
26+
for xi in data:
27+
density += np.exp(-0.5 * ((x_points - xi) / bandwidth) ** 2)
28+
density /= n * bandwidth * np.sqrt(2 * np.pi)
29+
return density
30+
31+
32+
# Data - Monthly temperature distributions
33+
np.random.seed(42)
34+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
35+
base_temps = [5, 7, 12, 16, 20, 24, 26, 25, 21, 15, 10, 6]
36+
37+
# Generate distributions for each month
38+
n_samples = 200
39+
distributions = {}
40+
for i, month in enumerate(months):
41+
distributions[month] = np.random.normal(base_temps[i], 3, n_samples)
42+
43+
# KDE parameters - temperature range
44+
x_range = np.linspace(-5, 35, 150)
45+
46+
# Color palette (gradient from cool to warm colors)
47+
colors = [
48+
"#306998", # Python Blue (Jan - winter)
49+
"#3B7CA5", # Feb
50+
"#4B8FB0", # Mar
51+
"#5CA2B5", # Apr
52+
"#6DB5B8", # May
53+
"#7EC8B0", # Jun
54+
"#059669", # Jul - Teal Green (summer)
55+
"#5CA87A", # Aug
56+
"#8FB85A", # Sep
57+
"#C2C83A", # Oct
58+
"#FFD43B", # Nov - Python Yellow
59+
"#306998", # Dec - back to winter
60+
]
61+
62+
# Create chart with container
63+
chart = Chart(container="container")
64+
chart.options = HighchartsOptions()
65+
66+
# Chart configuration - standard (not inverted) for horizontal ridgelines
67+
chart.options.chart = {
68+
"type": "areaspline",
69+
"width": 4800,
70+
"height": 2700,
71+
"backgroundColor": "#ffffff",
72+
"spacing": [80, 80, 80, 150],
73+
}
74+
75+
# Title
76+
chart.options.title = {
77+
"text": "Monthly Temperature Distribution",
78+
"style": {"fontSize": "56px", "fontWeight": "bold"},
79+
"y": 40,
80+
}
81+
82+
# Subtitle
83+
chart.options.subtitle = {
84+
"text": "Daily temperature variation by month (Ridgeline Plot)",
85+
"style": {"fontSize": "36px", "color": "#666666"},
86+
"y": 90,
87+
}
88+
89+
# X-axis - Temperature values
90+
chart.options.x_axis = {
91+
"title": {"text": "Temperature (°C)", "style": {"fontSize": "40px"}},
92+
"labels": {"style": {"fontSize": "32px"}},
93+
"min": -5,
94+
"max": 35,
95+
"tickInterval": 5,
96+
"gridLineWidth": 1,
97+
"gridLineColor": "rgba(0, 0, 0, 0.08)",
98+
}
99+
100+
# Build y-axis labels for months
101+
y_axis_labels = []
102+
for i, month in enumerate(months):
103+
y_pos = (len(months) - 1 - i) * 0.7 + 0.3 # overlap_factor
104+
y_axis_labels.append(
105+
{
106+
"value": y_pos,
107+
"width": 0,
108+
"label": {
109+
"text": month,
110+
"align": "right",
111+
"x": -20,
112+
"style": {"fontSize": "32px", "fontWeight": "bold", "color": colors[i]},
113+
},
114+
}
115+
)
116+
117+
# Y-axis - Hidden, distributions use relative positioning
118+
chart.options.y_axis = {
119+
"title": {"text": None},
120+
"labels": {"enabled": False},
121+
"gridLineWidth": 0,
122+
"min": 0,
123+
"max": 12, # 12 months
124+
"plotLines": y_axis_labels,
125+
}
126+
127+
# Plot options for ridgeline appearance
128+
chart.options.plot_options = {
129+
"areaspline": {"fillOpacity": 0.7, "lineWidth": 2, "marker": {"enabled": False}, "trackByArea": True},
130+
"series": {"states": {"hover": {"enabled": True, "lineWidthPlus": 1}}, "animation": False},
131+
}
132+
133+
# Create series for each month (Jan at top, Dec at bottom)
134+
overlap_factor = 0.7 # Controls vertical overlap between distributions
135+
scale_factor = 0.9 # Controls height of each distribution
136+
137+
for i, month in enumerate(months):
138+
# Calculate KDE for this month's distribution
139+
density = gaussian_kde(distributions[month], x_range)
140+
# Normalize density to [0, 1]
141+
density = density / density.max()
142+
143+
# Calculate y-offset for this month (Jan at top = index 11, Dec at bottom = index 0)
144+
y_offset = (len(months) - 1 - i) * overlap_factor
145+
146+
# Create data points: [x, y] where y = offset + scaled_density
147+
data_points = []
148+
for x_val, d_val in zip(x_range, density, strict=True):
149+
data_points.append([float(x_val), float(y_offset + d_val * scale_factor)])
150+
151+
series = AreaSplineSeries()
152+
series.name = month
153+
series.data = data_points
154+
series.color = colors[i]
155+
series.fill_opacity = 0.75
156+
series.line_color = colors[i]
157+
series.line_width = 2
158+
series.threshold = float(y_offset) # Fill from this baseline
159+
160+
chart.add_series(series)
161+
162+
# Legend
163+
chart.options.legend = {"enabled": False}
164+
165+
# Credits
166+
chart.options.credits = {"enabled": False}
167+
168+
# Tooltip
169+
chart.options.tooltip = {
170+
"enabled": True,
171+
"headerFormat": "<b>{series.name}</b><br/>",
172+
"pointFormat": "Temperature: {point.x:.1f}°C",
173+
"style": {"fontSize": "28px"},
174+
}
175+
176+
# Download Highcharts JS
177+
highcharts_url = "https://code.highcharts.com/highcharts.js"
178+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
179+
highcharts_js = response.read().decode("utf-8")
180+
181+
# Generate HTML with inline scripts
182+
html_str = chart.to_js_literal()
183+
html_content = f"""<!DOCTYPE html>
184+
<html>
185+
<head>
186+
<meta charset="utf-8">
187+
<script>{highcharts_js}</script>
188+
</head>
189+
<body style="margin:0;">
190+
<div id="container" style="width: 4800px; height: 2700px;"></div>
191+
<script>{html_str}</script>
192+
</body>
193+
</html>"""
194+
195+
# Save HTML file for interactive version
196+
with open("plot.html", "w", encoding="utf-8") as f:
197+
html_output = (
198+
"""<!DOCTYPE html>
199+
<html>
200+
<head>
201+
<meta charset="utf-8">
202+
<script src="https://code.highcharts.com/highcharts.js"></script>
203+
</head>
204+
<body style="margin:0;">
205+
<div id="container" style="width: 100%; height: 100vh;"></div>
206+
<script>"""
207+
+ html_str
208+
+ """</script>
209+
</body>
210+
</html>"""
211+
)
212+
f.write(html_output)
213+
214+
# Write temp HTML and take screenshot
215+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
216+
f.write(html_content)
217+
temp_path = f.name
218+
219+
chrome_options = Options()
220+
chrome_options.add_argument("--headless")
221+
chrome_options.add_argument("--no-sandbox")
222+
chrome_options.add_argument("--disable-dev-shm-usage")
223+
chrome_options.add_argument("--disable-gpu")
224+
chrome_options.add_argument("--window-size=4800,2700")
225+
226+
driver = webdriver.Chrome(options=chrome_options)
227+
driver.get(f"file://{temp_path}")
228+
time.sleep(5)
229+
driver.save_screenshot("plot.png")
230+
driver.quit()
231+
232+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)