|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | polar-bar: Polar Bar Chart (Wind Rose) |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-30 |
| 3 | +Library: highcharts unknown | Python 3.13.13 |
| 4 | +Quality: 92/100 | Updated: 2026-05-13 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
7 | 8 | import tempfile |
8 | 9 | import time |
9 | | -import urllib.request |
10 | 10 | from pathlib import Path |
11 | 11 |
|
12 | 12 | import numpy as np |
| 13 | +import requests |
13 | 14 | from highcharts_core.chart import Chart |
14 | 15 | from highcharts_core.options import HighchartsOptions |
15 | 16 | from selenium import webdriver |
16 | 17 | from selenium.webdriver.chrome.options import Options |
17 | 18 |
|
18 | 19 |
|
| 20 | +# Theme tokens |
| 21 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 22 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 23 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 24 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 25 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 26 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 27 | + |
| 28 | +# Okabe-Ito palette (first series is always #009E73) |
| 29 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"] |
| 30 | + |
19 | 31 | # Data - Wind direction frequency by speed category |
20 | 32 | np.random.seed(42) |
21 | 33 |
|
|
29 | 41 | moderate = [5, 3, 4, 2, 3, 8, 15, 8] # 10-20 mph |
30 | 42 | strong = [2, 1, 2, 1, 1, 4, 8, 5] # 20+ mph |
31 | 43 |
|
32 | | -# Download Highcharts JS modules |
33 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
34 | | -with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
35 | | - highcharts_js = response.read().decode("utf-8") |
| 44 | +# Download Highcharts JS modules from jsDelivr CDN |
| 45 | +headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"} |
| 46 | +highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@11.4.3/highcharts.min.js" |
| 47 | +response = requests.get(highcharts_url, timeout=30, headers=headers) |
| 48 | +response.raise_for_status() |
| 49 | +highcharts_js = response.text |
36 | 50 |
|
37 | | -highcharts_more_url = "https://code.highcharts.com/highcharts-more.js" |
38 | | -with urllib.request.urlopen(highcharts_more_url, timeout=30) as response: |
39 | | - highcharts_more_js = response.read().decode("utf-8") |
| 51 | +highcharts_more_url = "https://cdn.jsdelivr.net/npm/highcharts@11.4.3/highcharts-more.min.js" |
| 52 | +response_more = requests.get(highcharts_more_url, timeout=30, headers=headers) |
| 53 | +response_more.raise_for_status() |
| 54 | +highcharts_more_js = response_more.text |
40 | 55 |
|
41 | 56 | # Create chart |
42 | 57 | chart = Chart(container="container") |
|
46 | 61 | chart.options.chart = { |
47 | 62 | "polar": True, |
48 | 63 | "type": "column", |
49 | | - "width": 3600, |
50 | | - "height": 3800, |
51 | | - "backgroundColor": "#ffffff", |
52 | | - "marginBottom": 250, # space for legend |
| 64 | + "width": 4800, |
| 65 | + "height": 2700, |
| 66 | + "backgroundColor": PAGE_BG, |
| 67 | + "marginBottom": 250, |
53 | 68 | } |
54 | 69 |
|
55 | 70 | # Title |
56 | 71 | chart.options.title = { |
57 | | - "text": "polar-bar · highcharts · pyplots.ai", |
58 | | - "style": {"fontSize": "48px", "fontWeight": "bold"}, |
| 72 | + "text": "polar-bar · highcharts · anyplot.ai", |
| 73 | + "style": {"fontSize": "28px", "fontWeight": "bold", "color": INK}, |
59 | 74 | } |
60 | 75 |
|
61 | | -chart.options.subtitle = {"text": "Wind Speed Distribution by Direction", "style": {"fontSize": "32px"}} |
| 76 | +chart.options.subtitle = { |
| 77 | + "text": "Wind Speed Distribution by Direction", |
| 78 | + "style": {"fontSize": "22px", "color": INK_SOFT}, |
| 79 | +} |
62 | 80 |
|
63 | 81 | # Pane configuration for polar chart |
64 | | -chart.options.pane = { |
65 | | - "size": "60%", |
66 | | - "startAngle": 0, |
67 | | - "endAngle": 360, |
68 | | - "center": ["40%", "50%"], # move chart left to make room for legend on right |
69 | | -} |
| 82 | +chart.options.pane = {"size": "70%", "startAngle": 0, "endAngle": 360, "center": ["50%", "50%"]} |
70 | 83 |
|
71 | 84 | # X axis (angular - directions) |
72 | 85 | chart.options.x_axis = { |
73 | 86 | "categories": directions, |
74 | 87 | "tickmarkPlacement": "on", |
75 | 88 | "lineWidth": 0, |
76 | | - "labels": {"style": {"fontSize": "36px", "fontWeight": "bold"}, "distance": 25}, |
| 89 | + "labels": {"style": {"fontSize": "22px", "color": INK_SOFT}, "distance": 30}, |
77 | 90 | } |
78 | 91 |
|
79 | 92 | # Y axis (radial - frequency) |
80 | 93 | chart.options.y_axis = { |
81 | 94 | "min": 0, |
82 | 95 | "endOnTick": False, |
83 | 96 | "showLastLabel": True, |
84 | | - "title": {"text": "Frequency (%)", "style": {"fontSize": "28px"}}, |
85 | | - "labels": {"style": {"fontSize": "24px"}}, |
| 97 | + "title": {"text": "Frequency", "style": {"fontSize": "22px", "color": INK}}, |
| 98 | + "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}}, |
86 | 99 | "reversedStacks": False, |
87 | | - "gridLineColor": "#cccccc", |
| 100 | + "gridLineColor": GRID, |
88 | 101 | "gridLineWidth": 1, |
| 102 | + "lineColor": INK_SOFT, |
| 103 | + "tickColor": INK_SOFT, |
89 | 104 | } |
90 | 105 |
|
91 | 106 | # Legend |
|
94 | 109 | "align": "right", |
95 | 110 | "verticalAlign": "middle", |
96 | 111 | "layout": "vertical", |
97 | | - "itemStyle": {"fontSize": "32px"}, |
| 112 | + "itemStyle": {"fontSize": "18px", "color": INK_SOFT}, |
98 | 113 | "symbolRadius": 0, |
99 | | - "symbolHeight": 28, |
100 | | - "symbolWidth": 40, |
101 | | - "itemMarginBottom": 20, |
| 114 | + "symbolHeight": 20, |
| 115 | + "symbolWidth": 28, |
| 116 | + "itemMarginBottom": 15, |
102 | 117 | "x": -50, |
| 118 | + "backgroundColor": ELEVATED_BG, |
| 119 | + "borderColor": INK_SOFT, |
| 120 | + "borderWidth": 1, |
103 | 121 | } |
104 | 122 |
|
105 | 123 | # Plot options for stacked column |
106 | 124 | chart.options.plot_options = { |
107 | 125 | "series": {"stacking": "normal", "shadow": False, "groupPadding": 0, "pointPlacement": "on"}, |
108 | | - "column": {"pointPadding": 0, "groupPadding": 0, "borderWidth": 2, "borderColor": "#ffffff"}, |
| 126 | + "column": {"pointPadding": 0, "groupPadding": 0, "borderWidth": 1, "borderColor": PAGE_BG}, |
109 | 127 | } |
110 | 128 |
|
111 | | -# Colors - colorblind safe palette |
112 | | -colors = ["#306998", "#FFD43B", "#9467BD", "#17BECF"] |
113 | | - |
114 | | -# Add series (stacked from bottom to top) |
| 129 | +# Add series with Okabe-Ito colors |
115 | 130 | series_data = [ |
116 | | - {"name": "Calm (<1 mph)", "data": calm, "color": colors[0]}, |
117 | | - {"name": "Light (1-10 mph)", "data": light, "color": colors[1]}, |
118 | | - {"name": "Moderate (10-20 mph)", "data": moderate, "color": colors[2]}, |
119 | | - {"name": "Strong (>20 mph)", "data": strong, "color": colors[3]}, |
| 131 | + {"name": "Calm (<1 mph)", "data": calm, "color": OKABE_ITO[0]}, |
| 132 | + {"name": "Light (1-10 mph)", "data": light, "color": OKABE_ITO[1]}, |
| 133 | + {"name": "Moderate (10-20 mph)", "data": moderate, "color": OKABE_ITO[2]}, |
| 134 | + {"name": "Strong (>20 mph)", "data": strong, "color": OKABE_ITO[3]}, |
120 | 135 | ] |
121 | 136 |
|
122 | 137 | chart.options.series = series_data |
|
130 | 145 | <script>{highcharts_js}</script> |
131 | 146 | <script>{highcharts_more_js}</script> |
132 | 147 | </head> |
133 | | -<body style="margin:0; background:#ffffff;"> |
134 | | - <div id="container" style="width: 3600px; height: 3800px;"></div> |
| 148 | +<body style="margin:0; background:{PAGE_BG};"> |
| 149 | + <div id="container" style="width: 4800px; height: 2700px;"></div> |
135 | 150 | <script>{html_str}</script> |
136 | 151 | </body> |
137 | 152 | </html>""" |
138 | 153 |
|
139 | | -# Write temp HTML and take screenshot |
| 154 | +# Save theme-suffixed HTML |
| 155 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
| 156 | + f.write(html_content) |
| 157 | + |
| 158 | +# Write temp HTML and take screenshot for PNG |
140 | 159 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
141 | 160 | f.write(html_content) |
142 | 161 | temp_path = f.name |
|
146 | 165 | chrome_options.add_argument("--no-sandbox") |
147 | 166 | chrome_options.add_argument("--disable-dev-shm-usage") |
148 | 167 | chrome_options.add_argument("--disable-gpu") |
149 | | -chrome_options.add_argument("--window-size=3600,3800") # extra height for legend |
| 168 | +chrome_options.add_argument("--window-size=4800,2700") |
150 | 169 |
|
151 | 170 | driver = webdriver.Chrome(options=chrome_options) |
152 | 171 | driver.get(f"file://{temp_path}") |
153 | 172 | time.sleep(5) |
154 | | -driver.save_screenshot("plot.png") |
| 173 | +driver.save_screenshot(f"plot-{THEME}.png") |
155 | 174 | driver.quit() |
156 | 175 |
|
157 | 176 | Path(temp_path).unlink() |
158 | | - |
159 | | -# Also save HTML for interactive version |
160 | | -html_export = f"""<!DOCTYPE html> |
161 | | -<html> |
162 | | -<head> |
163 | | - <meta charset="utf-8"> |
164 | | - <title>polar-bar · highcharts · pyplots.ai</title> |
165 | | - <script src="https://code.highcharts.com/highcharts.js"></script> |
166 | | - <script src="https://code.highcharts.com/highcharts-more.js"></script> |
167 | | -</head> |
168 | | -<body style="margin:0; background:#ffffff;"> |
169 | | - <div id="container" style="width: 100%; height: 100vh;"></div> |
170 | | - <script>{html_str}</script> |
171 | | -</body> |
172 | | -</html>""" |
173 | | - |
174 | | -with open("plot.html", "w", encoding="utf-8") as f: |
175 | | - f.write(html_export) |
|
0 commit comments