Skip to content

Commit 14b051d

Browse files
feat(highcharts): implement polar-basic (#5572)
## Implementation: `polar-basic` - python/highcharts Implements the **python/highcharts** version of `polar-basic`. **File:** `plots/polar-basic/implementations/python/highcharts.py` **Parent Issue:** #800 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25141207935)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 1c6410d commit 14b051d

2 files changed

Lines changed: 286 additions & 194 deletions

File tree

Lines changed: 122 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
polar-basic: Basic Polar Chart
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: 88/100 | Updated: 2026-04-30
55
"""
66

7+
import os
78
import tempfile
89
import time
910
import urllib.request
@@ -12,93 +13,156 @@
1213
import numpy as np
1314
from highcharts_core.chart import Chart
1415
from highcharts_core.options import HighchartsOptions
16+
from highcharts_core.options.series.area import AreaSeries
1517
from highcharts_core.options.series.scatter import ScatterSeries
1618
from selenium import webdriver
1719
from selenium.webdriver.chrome.options import Options
1820

1921

20-
# Data - Hourly temperature readings (24-hour cycle)
22+
# Theme tokens
23+
THEME = os.getenv("ANYPLOT_THEME", "light")
24+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
25+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
26+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
27+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
28+
GRID = "rgba(26,26,23,0.12)" if THEME == "light" else "rgba(240,239,232,0.12)"
29+
30+
BRAND = "#009E73" # Okabe-Ito position 1
31+
PEAK_COLOR = "#D55E00" # Okabe-Ito position 2 - peak highlight
32+
33+
# Data - 24-hour temperature cycle
2134
np.random.seed(42)
22-
hours = np.arange(0, 360, 15) # 24 data points at 15-degree intervals (360/24=15)
23-
# Temperature pattern: cooler at night (0°/midnight), warmer during day (180°/noon)
24-
base_temp = 15 + 10 * np.sin(np.radians(hours - 90)) # Peak at 90° (6 AM shifted to noon)
25-
temperatures = base_temp + np.random.randn(len(hours)) * 2 # Add some noise
35+
hours = np.arange(0, 24)
36+
base_temp = 15 + 10 * np.sin(np.radians(hours / 24 * 360 - 90)) # Peak around noon
37+
temperatures = base_temp + np.random.randn(24) * 2
38+
39+
peak_idx = int(np.argmax(temperatures))
2640

2741
# Create chart
2842
chart = Chart(container="container")
2943
chart.options = HighchartsOptions()
3044

31-
# Chart configuration for polar scatter plot
32-
chart.options.chart = {"polar": True, "width": 4800, "height": 2700, "backgroundColor": "#ffffff"}
45+
# Square canvas - optimal for circular polar chart
46+
chart.options.chart = {
47+
"polar": True,
48+
"width": 3600,
49+
"height": 3600,
50+
"backgroundColor": PAGE_BG,
51+
"style": {"color": INK},
52+
"marginTop": 240,
53+
"marginBottom": 220,
54+
}
3355

34-
# Title
3556
chart.options.title = {
36-
"text": "polar-basic · highcharts · pyplots.ai",
37-
"style": {"fontSize": "72px", "fontWeight": "bold"},
57+
"text": "polar-basic · highcharts · anyplot.ai",
58+
"style": {"fontSize": "72px", "fontWeight": "bold", "color": INK},
3859
}
3960

40-
# Subtitle for context
41-
chart.options.subtitle = {"text": "24-Hour Temperature Pattern", "style": {"fontSize": "48px"}}
61+
chart.options.subtitle = {"text": "24-Hour Temperature Pattern", "style": {"fontSize": "48px", "color": INK_SOFT}}
4262

43-
# X-axis (angular axis - hours around the clock)
63+
# Angular axis (hours)
4464
chart.options.x_axis = {
45-
"tickInterval": 30, # Every 30 degrees (every 2 hours)
4665
"min": 0,
47-
"max": 360,
48-
"labels": {"format": "{value}°", "style": {"fontSize": "36px"}},
49-
"gridLineWidth": 2,
50-
"gridLineColor": "rgba(0, 0, 0, 0.15)",
66+
"max": 24,
67+
"tickInterval": 3,
68+
"labels": {"format": "{value}h", "style": {"fontSize": "36px", "color": INK_SOFT}},
69+
"gridLineWidth": 1,
70+
"gridLineColor": GRID,
71+
"lineColor": INK_SOFT,
72+
"tickColor": INK_SOFT,
5173
}
5274

53-
# Y-axis (radial axis - temperature)
75+
# Radial axis (temperature)
5476
chart.options.y_axis = {
5577
"min": 0,
5678
"max": 35,
5779
"tickInterval": 5,
58-
"labels": {"format": "{value}°C", "style": {"fontSize": "32px"}},
59-
"gridLineWidth": 2,
60-
"gridLineColor": "rgba(0, 0, 0, 0.15)",
61-
"title": {"text": "Temperature (°C)", "style": {"fontSize": "40px"}},
80+
"labels": {"format": "{value}°C", "style": {"fontSize": "28px", "color": INK_SOFT}},
81+
"gridLineWidth": 1,
82+
"gridLineColor": GRID,
83+
"lineColor": INK_SOFT,
6284
}
6385

64-
# Pane settings for polar chart
65-
chart.options.pane = {"size": "70%", "startAngle": 0, "endAngle": 360}
86+
# Pane - generous in square canvas
87+
chart.options.pane = {"size": "78%", "startAngle": 0, "endAngle": 360}
88+
89+
# Custom tooltip with formatted output (distinctive Highcharts feature)
90+
chart.options.tooltip = {
91+
"headerFormat": "",
92+
"pointFormat": "<span style='font-size:24px; font-weight:bold'>{series.name}</span><br/><b>{point.x}h</b>: {point.y:.1f}°C",
93+
"style": {"fontSize": "28px", "color": INK},
94+
"backgroundColor": ELEVATED_BG,
95+
"borderColor": INK_SOFT,
96+
"borderWidth": 1,
97+
"padding": 12,
98+
}
6699

67-
# Plot options for scatter on polar
68100
chart.options.plot_options = {
69-
"scatter": {"marker": {"enabled": True, "radius": 16, "symbol": "circle"}, "lineWidth": 3},
101+
"area": {
102+
"connectEnds": True,
103+
"fillOpacity": 0.28,
104+
"lineWidth": 3,
105+
"marker": {"enabled": True, "radius": 12, "symbol": "circle", "lineWidth": 2, "lineColor": PAGE_BG},
106+
},
107+
"scatter": {"marker": {"radius": 22, "symbol": "circle"}},
70108
"series": {"animation": False},
71109
}
72110

73-
# Legend
74111
chart.options.legend = {
75112
"enabled": True,
76113
"align": "center",
77114
"verticalAlign": "bottom",
78115
"layout": "horizontal",
79-
"itemStyle": {"fontSize": "36px"},
116+
"itemStyle": {"fontSize": "36px", "color": INK_SOFT},
117+
"backgroundColor": ELEVATED_BG,
118+
"borderColor": INK_SOFT,
119+
"borderWidth": 1,
80120
}
81121

82-
# Credits
83122
chart.options.credits = {"enabled": False}
84123

85-
# Create scatter series with polar coordinates
86-
series = ScatterSeries()
87-
series.data = [[float(h), float(t)] for h, t in zip(hours, temperatures, strict=True)]
88-
series.name = "Temperature"
89-
series.color = "rgba(48, 105, 152, 0.8)" # Python Blue with alpha
90-
series.marker = {"radius": 16, "symbol": "circle"}
91-
92-
chart.add_series(series)
124+
# Main temperature series - AreaSeries for filled polar trace
125+
area_series = AreaSeries()
126+
area_series.data = [[float(h), float(t)] for h, t in zip(hours, temperatures, strict=True)]
127+
area_series.name = "Temperature"
128+
area_series.color = BRAND
129+
area_series.fill_opacity = 0.28
130+
area_series.connect_ends = True
131+
132+
chart.add_series(area_series)
133+
134+
# Focal-point marker highlighting the daily maximum temperature
135+
peak_series = ScatterSeries()
136+
peak_series.data = [[float(hours[peak_idx]), float(temperatures[peak_idx])]]
137+
peak_series.name = "Daily Peak"
138+
peak_series.color = PEAK_COLOR
139+
peak_series.marker = {"radius": 22, "symbol": "circle", "lineWidth": 3, "lineColor": INK}
140+
peak_series.data_labels = {
141+
"enabled": True,
142+
"format": "{point.y:.1f}°C",
143+
"style": {"fontSize": "28px", "color": INK, "fontWeight": "bold", "textOutline": "none"},
144+
"y": -50,
145+
}
93146

94-
# Download Highcharts JS and highcharts-more.js (required for polar charts)
95-
highcharts_url = "https://code.highcharts.com/highcharts.js"
96-
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
97-
highcharts_js = response.read().decode("utf-8")
147+
chart.add_series(peak_series)
98148

99-
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
100-
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
101-
highcharts_more_js = response.read().decode("utf-8")
149+
# Download Highcharts JS (inline for headless Chrome, with /tmp cache)
150+
cache_dir = Path("/tmp")
151+
hc_urls = {
152+
"core": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/highcharts.js", cache_dir / "highcharts.js"),
153+
"more": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/highcharts-more.js", cache_dir / "highcharts-more.js"),
154+
}
155+
hc_js = {}
156+
for key, (url, cache_path) in hc_urls.items():
157+
if cache_path.exists() and cache_path.stat().st_size > 1000:
158+
hc_js[key] = cache_path.read_text(encoding="utf-8")
159+
else:
160+
with urllib.request.urlopen(url, timeout=30) as resp:
161+
content = resp.read().decode("utf-8")
162+
cache_path.write_text(content, encoding="utf-8")
163+
hc_js[key] = content
164+
highcharts_js = hc_js["core"]
165+
highcharts_more_js = hc_js["more"]
102166

103167
# Generate HTML with inline scripts
104168
html_str = chart.to_js_literal()
@@ -109,12 +173,16 @@
109173
<script>{highcharts_js}</script>
110174
<script>{highcharts_more_js}</script>
111175
</head>
112-
<body style="margin:0;">
113-
<div id="container" style="width: 4800px; height: 2700px;"></div>
176+
<body style="margin:0; background:{PAGE_BG};">
177+
<div id="container" style="width: 3600px; height: 3600px;"></div>
114178
<script>{html_str}</script>
115179
</body>
116180
</html>"""
117181

182+
# Save HTML artifact
183+
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
184+
f.write(html_content)
185+
118186
# Write temp HTML and take screenshot
119187
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
120188
f.write(html_content)
@@ -125,31 +193,14 @@
125193
chrome_options.add_argument("--no-sandbox")
126194
chrome_options.add_argument("--disable-dev-shm-usage")
127195
chrome_options.add_argument("--disable-gpu")
128-
chrome_options.add_argument("--window-size=5000,3000")
196+
chrome_options.add_argument("--window-size=3800,3800")
129197

130198
driver = webdriver.Chrome(options=chrome_options)
131199
driver.get(f"file://{temp_path}")
132-
time.sleep(5) # Wait for chart to render
200+
time.sleep(5)
133201

134-
# Take screenshot of just the chart container element
135202
container = driver.find_element("id", "container")
136-
container.screenshot("plot.png")
203+
container.screenshot(f"plot-{THEME}.png")
137204
driver.quit()
138205

139-
Path(temp_path).unlink() # Clean up temp file
140-
141-
# Also save HTML for interactive version
142-
with open("plot.html", "w", encoding="utf-8") as f:
143-
interactive_html = f"""<!DOCTYPE html>
144-
<html>
145-
<head>
146-
<meta charset="utf-8">
147-
<script src="https://code.highcharts.com/highcharts.js"></script>
148-
<script src="https://code.highcharts.com/highcharts-more.js"></script>
149-
</head>
150-
<body style="margin:0;">
151-
<div id="container" style="width: 100%; height: 100vh;"></div>
152-
<script>{html_str}</script>
153-
</body>
154-
</html>"""
155-
f.write(interactive_html)
206+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)