|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | bar-race-animated: Animated Bar Chart Race |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-11 |
| 3 | +Library: highcharts unknown | Python 3.13.13 |
| 4 | +Quality: 83/100 | Updated: 2026-05-19 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
7 | 8 | import tempfile |
8 | 9 | import time |
9 | 10 | import urllib.request |
|
18 | 19 | from selenium.webdriver.chrome.options import Options |
19 | 20 |
|
20 | 21 |
|
| 22 | +# Theme |
| 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 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
| 29 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 30 | + |
| 31 | +# Okabe-Ito palette (positions 1–7) + adaptive neutral for 8 entities |
| 32 | +OI_COLORS = [ |
| 33 | + "#009E73", # position 1: bluish green |
| 34 | + "#D55E00", # position 2: vermillion |
| 35 | + "#0072B2", # position 3: blue |
| 36 | + "#CC79A7", # position 4: reddish purple |
| 37 | + "#E69F00", # position 5: orange |
| 38 | + "#56B4E9", # position 6: sky blue |
| 39 | + "#F0E442", # position 7: yellow |
| 40 | + "#1A1A1A" if THEME == "light" else "#E8E8E0", # position 8: adaptive neutral |
| 41 | +] |
| 42 | + |
21 | 43 | # Data - Global Technology Companies Market Value (in $B) 2019-2024 |
22 | 44 | np.random.seed(42) |
23 | 45 |
|
|
30 | 52 | "QuantumByte", |
31 | 53 | "NetPrime", |
32 | 54 | "DigiWave", |
33 | | - "SmartSystems", |
34 | | - "ByteForce", |
35 | | - "NexGen", |
36 | | - "CoreLogic", |
37 | 55 | ] |
38 | 56 |
|
39 | 57 | years = [2019, 2020, 2021, 2022, 2023, 2024] |
40 | 58 |
|
41 | | -# Generate realistic market value evolution |
42 | | -base_values = np.array([180, 150, 120, 100, 90, 80, 70, 60, 50, 45, 40, 35]) |
| 59 | +base_values = np.array([180, 150, 120, 100, 90, 80, 70, 60]) |
43 | 60 | data = [] |
44 | 61 | for i, year in enumerate(years): |
45 | 62 | growth = 1 + 0.15 * i + np.random.randn(len(companies)) * 0.2 |
46 | 63 | values = base_values * growth * (1 + np.random.randn(len(companies)) * 0.1) |
47 | | - # Add some shuffling over time to make rankings change |
48 | 64 | shuffle_factor = np.random.randn(len(companies)) * (20 + i * 10) |
49 | 65 | values = values + shuffle_factor |
50 | | - values = np.maximum(values, 10) # Minimum value |
| 66 | + values = np.maximum(values, 10) |
51 | 67 | for j, company in enumerate(companies): |
52 | 68 | data.append({"company": company, "year": year, "value": values[j]}) |
53 | 69 |
|
54 | 70 | df = pd.DataFrame(data) |
55 | | - |
56 | | -# Colors - consistent per company |
57 | | -colors = [ |
58 | | - "#306998", |
59 | | - "#FFD43B", |
60 | | - "#9467BD", |
61 | | - "#17BECF", |
62 | | - "#8C564B", |
63 | | - "#E377C2", |
64 | | - "#7F7F7F", |
65 | | - "#BCBD22", |
66 | | - "#1F77B4", |
67 | | - "#FF7F0E", |
68 | | - "#2CA02C", |
69 | | - "#D62728", |
70 | | -] |
71 | | -company_colors = dict(zip(companies, colors, strict=False)) |
72 | | - |
73 | | -# Select 6 key time snapshots for small multiples grid (2x3) |
74 | | -snapshot_years = years |
| 71 | +company_colors = dict(zip(companies, OI_COLORS, strict=True)) |
75 | 72 |
|
76 | 73 | # Download Highcharts JS |
77 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
78 | | -with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
| 74 | +highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@12/highcharts.js" |
| 75 | +with urllib.request.urlopen(highcharts_url, timeout=60) as response: |
79 | 76 | highcharts_js = response.read().decode("utf-8") |
80 | 77 |
|
81 | | -# Generate individual charts for each year |
| 78 | +# Generate individual charts for each year snapshot |
| 79 | +CHART_W = 1550 |
| 80 | +CHART_H = 1150 |
| 81 | +max_val = df["value"].max() * 1.1 |
| 82 | + |
82 | 83 | chart_scripts = [] |
83 | | -for idx, year in enumerate(snapshot_years): |
84 | | - year_data = df[df["year"] == year].sort_values("value", ascending=True).tail(10) |
| 84 | +for idx, year in enumerate(years): |
| 85 | + year_data = df[df["year"] == year].sort_values("value", ascending=True) |
85 | 86 |
|
86 | 87 | chart = Chart(container=f"chart{idx}") |
87 | 88 | chart.options = HighchartsOptions() |
88 | 89 |
|
89 | 90 | chart.options.chart = { |
90 | 91 | "type": "bar", |
91 | | - "width": 1520, |
92 | | - "height": 1280, |
93 | | - "backgroundColor": "#ffffff", |
94 | | - "marginLeft": 200, |
95 | | - "marginRight": 50, |
| 92 | + "width": CHART_W, |
| 93 | + "height": CHART_H, |
| 94 | + "backgroundColor": ELEVATED_BG, |
| 95 | + "marginLeft": 230, |
| 96 | + "marginRight": 110, |
96 | 97 | "marginBottom": 80, |
97 | | - "marginTop": 100, |
| 98 | + "marginTop": 80, |
98 | 99 | } |
99 | 100 |
|
100 | | - chart.options.title = {"text": f"Year {year}", "style": {"fontSize": "32px", "fontWeight": "bold"}} |
| 101 | + chart.options.title = {"text": str(year), "style": {"fontSize": "38px", "fontWeight": "bold", "color": INK}} |
101 | 102 |
|
102 | 103 | chart.options.x_axis = { |
103 | 104 | "categories": year_data["company"].tolist(), |
104 | 105 | "title": {"text": None}, |
105 | | - "labels": {"style": {"fontSize": "20px"}}, |
| 106 | + "labels": {"style": {"fontSize": "22px", "color": INK_SOFT}}, |
| 107 | + "lineColor": INK_SOFT, |
| 108 | + "tickColor": INK_SOFT, |
106 | 109 | } |
107 | 110 |
|
108 | 111 | chart.options.y_axis = { |
109 | | - "title": {"text": "Market Value ($B)", "style": {"fontSize": "20px"}}, |
110 | | - "labels": {"style": {"fontSize": "18px"}}, |
| 112 | + "title": {"text": "Market Value ($B)", "style": {"fontSize": "22px", "color": INK}}, |
| 113 | + "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}}, |
111 | 114 | "min": 0, |
112 | | - "max": 400, |
| 115 | + "max": max_val, |
| 116 | + "gridLineColor": GRID, |
113 | 117 | } |
114 | 118 |
|
115 | 119 | chart.options.legend = {"enabled": False} |
116 | | - |
117 | 120 | chart.options.credits = {"enabled": False} |
118 | 121 |
|
119 | | - # Create series with individual colors |
120 | 122 | series = BarSeries() |
121 | 123 | series.name = "Market Value" |
122 | 124 | series.data = [ |
|
125 | 127 | series.data_labels = { |
126 | 128 | "enabled": True, |
127 | 129 | "format": "${point.y:.0f}B", |
128 | | - "style": {"fontSize": "16px", "fontWeight": "normal"}, |
| 130 | + "style": {"fontSize": "19px", "fontWeight": "normal", "color": INK, "textOutline": "none"}, |
129 | 131 | } |
130 | | - |
131 | 132 | chart.add_series(series) |
132 | 133 |
|
133 | | - chart.options.plot_options = {"bar": {"borderWidth": 0, "pointWidth": 50}} |
| 134 | + chart.options.plot_options = {"bar": {"borderWidth": 0, "pointWidth": 58}} |
134 | 135 |
|
135 | 136 | chart_scripts.append(chart.to_js_literal()) |
136 | 137 |
|
137 | | -# Create combined HTML with 2x3 grid |
| 138 | +# Assemble full-page HTML with 2×3 small-multiples grid |
| 139 | +GRID_GAP = 28 |
138 | 140 | html_content = f"""<!DOCTYPE html> |
139 | 141 | <html> |
140 | 142 | <head> |
|
144 | 146 | body {{ |
145 | 147 | margin: 0; |
146 | 148 | padding: 40px; |
147 | | - background: #ffffff; |
| 149 | + background: {PAGE_BG}; |
148 | 150 | font-family: Arial, sans-serif; |
149 | 151 | }} |
150 | 152 | .main-title {{ |
151 | 153 | text-align: center; |
152 | | - font-size: 48px; |
| 154 | + font-size: 52px; |
153 | 155 | font-weight: bold; |
154 | | - margin-bottom: 10px; |
155 | | - color: #333; |
| 156 | + margin-bottom: 8px; |
| 157 | + color: {INK}; |
156 | 158 | }} |
157 | 159 | .subtitle {{ |
158 | 160 | text-align: center; |
159 | | - font-size: 28px; |
160 | | - color: #666; |
161 | | - margin-bottom: 40px; |
| 161 | + font-size: 30px; |
| 162 | + color: {INK_MUTED}; |
| 163 | + margin-bottom: 36px; |
162 | 164 | }} |
163 | 165 | .grid {{ |
164 | 166 | display: grid; |
165 | | - grid-template-columns: repeat(3, 1fr); |
166 | | - grid-template-rows: repeat(2, 1fr); |
167 | | - gap: 30px; |
168 | | - width: 4720px; |
| 167 | + grid-template-columns: repeat(3, {CHART_W}px); |
| 168 | + grid-template-rows: repeat(2, {CHART_H}px); |
| 169 | + gap: {GRID_GAP}px; |
| 170 | + width: {3 * CHART_W + 2 * GRID_GAP}px; |
169 | 171 | margin: 0 auto; |
170 | 172 | }} |
171 | 173 | .chart-container {{ |
172 | | - background: #fafafa; |
173 | | - border: 2px solid #e0e0e0; |
174 | | - border-radius: 10px; |
| 174 | + background: {ELEVATED_BG}; |
| 175 | + border-radius: 8px; |
175 | 176 | overflow: hidden; |
176 | 177 | }} |
177 | 178 | </style> |
178 | 179 | </head> |
179 | 180 | <body> |
180 | | - <div class="main-title">bar-race-animated · highcharts · pyplots.ai</div> |
181 | | - <div class="subtitle">Technology Companies Market Value Evolution (2019-2024)</div> |
| 181 | + <div class="main-title">bar-race-animated · python · highcharts · anyplot.ai</div> |
| 182 | + <div class="subtitle">Technology Companies Market Value Evolution (2019–2024)</div> |
182 | 183 | <div class="grid"> |
183 | 184 | <div class="chart-container"><div id="chart0"></div></div> |
184 | 185 | <div class="chart-container"><div id="chart1"></div></div> |
|
198 | 199 | </body> |
199 | 200 | </html>""" |
200 | 201 |
|
201 | | -# Write temp HTML and take screenshot |
202 | | -with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
| 202 | +# Save HTML artifact |
| 203 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
203 | 204 | f.write(html_content) |
204 | | - temp_path = f.name |
205 | 205 |
|
206 | | -# Also save as plot.html for interactive version |
207 | | -with open("plot.html", "w", encoding="utf-8") as f: |
| 206 | +# Screenshot via headless Chrome |
| 207 | +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
208 | 208 | f.write(html_content) |
| 209 | + temp_path = f.name |
209 | 210 |
|
210 | 211 | chrome_options = Options() |
211 | 212 | chrome_options.add_argument("--headless") |
|
216 | 217 |
|
217 | 218 | driver = webdriver.Chrome(options=chrome_options) |
218 | 219 | driver.get(f"file://{temp_path}") |
219 | | -time.sleep(8) # Wait for all charts to render |
220 | | -driver.save_screenshot("plot.png") |
| 220 | +time.sleep(8) |
| 221 | +driver.save_screenshot(f"plot-{THEME}.png") |
221 | 222 | driver.quit() |
222 | 223 |
|
223 | 224 | Path(temp_path).unlink() |
0 commit comments