|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
7 | 8 | import tempfile |
8 | 9 | import time |
9 | 10 | import urllib.request |
|
12 | 13 | import numpy as np |
13 | 14 | from highcharts_core.chart import Chart |
14 | 15 | from highcharts_core.options import HighchartsOptions |
| 16 | +from highcharts_core.options.series.area import AreaSeries |
15 | 17 | from highcharts_core.options.series.scatter import ScatterSeries |
16 | 18 | from selenium import webdriver |
17 | 19 | from selenium.webdriver.chrome.options import Options |
18 | 20 |
|
19 | 21 |
|
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 |
21 | 34 | 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)) |
26 | 40 |
|
27 | 41 | # Create chart |
28 | 42 | chart = Chart(container="container") |
29 | 43 | chart.options = HighchartsOptions() |
30 | 44 |
|
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 | +} |
33 | 55 |
|
34 | | -# Title |
35 | 56 | 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}, |
38 | 59 | } |
39 | 60 |
|
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}} |
42 | 62 |
|
43 | | -# X-axis (angular axis - hours around the clock) |
| 63 | +# Angular axis (hours) |
44 | 64 | chart.options.x_axis = { |
45 | | - "tickInterval": 30, # Every 30 degrees (every 2 hours) |
46 | 65 | "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, |
51 | 73 | } |
52 | 74 |
|
53 | | -# Y-axis (radial axis - temperature) |
| 75 | +# Radial axis (temperature) |
54 | 76 | chart.options.y_axis = { |
55 | 77 | "min": 0, |
56 | 78 | "max": 35, |
57 | 79 | "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, |
62 | 84 | } |
63 | 85 |
|
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 | +} |
66 | 99 |
|
67 | | -# Plot options for scatter on polar |
68 | 100 | 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"}}, |
70 | 108 | "series": {"animation": False}, |
71 | 109 | } |
72 | 110 |
|
73 | | -# Legend |
74 | 111 | chart.options.legend = { |
75 | 112 | "enabled": True, |
76 | 113 | "align": "center", |
77 | 114 | "verticalAlign": "bottom", |
78 | 115 | "layout": "horizontal", |
79 | | - "itemStyle": {"fontSize": "36px"}, |
| 116 | + "itemStyle": {"fontSize": "36px", "color": INK_SOFT}, |
| 117 | + "backgroundColor": ELEVATED_BG, |
| 118 | + "borderColor": INK_SOFT, |
| 119 | + "borderWidth": 1, |
80 | 120 | } |
81 | 121 |
|
82 | | -# Credits |
83 | 122 | chart.options.credits = {"enabled": False} |
84 | 123 |
|
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 | +} |
93 | 146 |
|
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) |
98 | 148 |
|
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"] |
102 | 166 |
|
103 | 167 | # Generate HTML with inline scripts |
104 | 168 | html_str = chart.to_js_literal() |
|
109 | 173 | <script>{highcharts_js}</script> |
110 | 174 | <script>{highcharts_more_js}</script> |
111 | 175 | </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> |
114 | 178 | <script>{html_str}</script> |
115 | 179 | </body> |
116 | 180 | </html>""" |
117 | 181 |
|
| 182 | +# Save HTML artifact |
| 183 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
| 184 | + f.write(html_content) |
| 185 | + |
118 | 186 | # Write temp HTML and take screenshot |
119 | 187 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
120 | 188 | f.write(html_content) |
|
125 | 193 | chrome_options.add_argument("--no-sandbox") |
126 | 194 | chrome_options.add_argument("--disable-dev-shm-usage") |
127 | 195 | chrome_options.add_argument("--disable-gpu") |
128 | | -chrome_options.add_argument("--window-size=5000,3000") |
| 196 | +chrome_options.add_argument("--window-size=3800,3800") |
129 | 197 |
|
130 | 198 | driver = webdriver.Chrome(options=chrome_options) |
131 | 199 | driver.get(f"file://{temp_path}") |
132 | | -time.sleep(5) # Wait for chart to render |
| 200 | +time.sleep(5) |
133 | 201 |
|
134 | | -# Take screenshot of just the chart container element |
135 | 202 | container = driver.find_element("id", "container") |
136 | | -container.screenshot("plot.png") |
| 203 | +container.screenshot(f"plot-{THEME}.png") |
137 | 204 | driver.quit() |
138 | 205 |
|
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