|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | lollipop-basic: Basic Lollipop Chart |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: highcharts unknown | Python 3.14.4 |
| 4 | +Quality: 87/100 | Updated: 2026-04-26 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import json |
| 8 | +import os |
8 | 9 | import tempfile |
9 | 10 | import time |
10 | 11 | import urllib.request |
|
14 | 15 | from selenium.webdriver.chrome.options import Options |
15 | 16 |
|
16 | 17 |
|
17 | | -# Data - Product sales by category, sorted by value for readability |
18 | | -categories = ["Electronics", "Clothing", "Home & Garden", "Sports", "Books", "Toys", "Jewelry", "Automotive"] |
19 | | -values = [8500, 6200, 5100, 4300, 3800, 2900, 2400, 1800] |
| 18 | +# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome") |
| 19 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 20 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 21 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 22 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 23 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 24 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 25 | +BRAND = "#009E73" # Okabe-Ito position 1 — ALWAYS first series |
20 | 26 |
|
21 | | -# Sort data by value (descending) for better visualization |
22 | | -sorted_data = sorted(zip(categories, values, strict=True), key=lambda x: x[1], reverse=True) |
23 | | -categories = [item[0] for item in sorted_data] |
24 | | -values = [item[1] for item in sorted_data] |
| 27 | +# Data — Product sales by category (deterministic, sorted descending) |
| 28 | +categories = [ |
| 29 | + "Electronics", |
| 30 | + "Clothing", |
| 31 | + "Home & Garden", |
| 32 | + "Sports", |
| 33 | + "Books", |
| 34 | + "Toys", |
| 35 | + "Beauty", |
| 36 | + "Automotive", |
| 37 | + "Food & Grocery", |
| 38 | + "Health", |
| 39 | +] |
| 40 | +values = [124820, 97340, 86715, 75260, 64480, 53905, 47620, 41370, 37815, 30945] |
25 | 41 |
|
26 | | -# Chart options - using scatter for dots and column with very thin width for stems |
| 42 | +# Plot — combine column (thin stems) and scatter (round markers) for the lollipop look |
27 | 43 | chart_options = { |
28 | 44 | "chart": { |
29 | 45 | "type": "scatter", |
30 | 46 | "width": 4800, |
31 | 47 | "height": 2700, |
32 | | - "backgroundColor": "#ffffff", |
33 | | - "marginBottom": 250, |
34 | | - "style": {"fontFamily": "Arial, sans-serif"}, |
| 48 | + "backgroundColor": PAGE_BG, |
| 49 | + "spacingTop": 60, |
| 50 | + "spacingBottom": 60, |
| 51 | + "spacingLeft": 60, |
| 52 | + "spacingRight": 80, |
| 53 | + "marginBottom": 220, |
| 54 | + "style": {"fontFamily": "Inter, system-ui, sans-serif", "color": INK}, |
| 55 | + }, |
| 56 | + "title": { |
| 57 | + "text": "Product Sales by Category · lollipop-basic · highcharts · anyplot.ai", |
| 58 | + "style": {"fontSize": "56px", "color": INK, "fontWeight": "500"}, |
| 59 | + "margin": 40, |
35 | 60 | }, |
36 | | - "title": {"text": "lollipop-basic · highcharts · pyplots.ai", "style": {"fontSize": "48px", "fontWeight": "bold"}}, |
37 | 61 | "xAxis": { |
38 | 62 | "categories": categories, |
39 | | - "title": {"text": "Product Category", "style": {"fontSize": "36px"}}, |
40 | | - "labels": {"style": {"fontSize": "28px"}}, |
| 63 | + "title": { |
| 64 | + "text": "Product Category", |
| 65 | + "style": {"fontSize": "36px", "fontWeight": "500", "color": INK}, |
| 66 | + "margin": 28, |
| 67 | + }, |
| 68 | + "labels": {"style": {"fontSize": "28px", "color": INK_SOFT}, "y": 50}, |
| 69 | + "lineColor": INK_SOFT, |
| 70 | + "tickColor": INK_SOFT, |
| 71 | + "gridLineWidth": 0, |
41 | 72 | }, |
42 | 73 | "yAxis": { |
43 | 74 | "min": 0, |
44 | | - "title": {"text": "Sales (Units)", "style": {"fontSize": "36px"}}, |
45 | | - "labels": {"style": {"fontSize": "28px"}}, |
46 | | - "gridLineColor": "#e0e0e0", |
47 | | - "gridLineDashStyle": "Dash", |
| 75 | + "tickAmount": 8, |
| 76 | + "title": { |
| 77 | + "text": "Sales (Units)", |
| 78 | + "style": {"fontSize": "36px", "fontWeight": "500", "color": INK}, |
| 79 | + "margin": 28, |
| 80 | + }, |
| 81 | + "labels": {"style": {"fontSize": "26px", "color": INK_SOFT}, "format": "{value:,.0f}", "x": -12}, |
| 82 | + "gridLineColor": GRID, |
| 83 | + "gridLineWidth": 1, |
| 84 | + "lineColor": INK_SOFT, |
| 85 | + "tickColor": INK_SOFT, |
48 | 86 | }, |
49 | 87 | "legend": {"enabled": False}, |
| 88 | + "credits": {"enabled": False}, |
50 | 89 | "plotOptions": { |
51 | | - "scatter": {"marker": {"radius": 20, "symbol": "circle"}}, |
52 | | - "column": {"pointWidth": 4, "borderWidth": 0}, |
| 90 | + "scatter": {"marker": {"radius": 36, "symbol": "circle"}}, |
| 91 | + "column": {"pointWidth": 8, "borderWidth": 0}, |
53 | 92 | }, |
54 | 93 | "series": [ |
55 | | - # Stems (thin columns from 0 to value) |
56 | | - {"type": "column", "name": "Sales", "data": values, "color": "#306998", "enableMouseTracking": False}, |
57 | | - # Dots (scatter points at the top of each stem) |
| 94 | + {"type": "column", "name": "Sales", "data": values, "color": BRAND, "enableMouseTracking": False}, |
58 | 95 | { |
59 | 96 | "type": "scatter", |
60 | 97 | "name": "Sales", |
61 | 98 | "data": values, |
62 | | - "color": "#306998", |
63 | | - "marker": {"radius": 20, "fillColor": "#306998", "lineWidth": 3, "lineColor": "#ffffff"}, |
| 99 | + "color": BRAND, |
| 100 | + "marker": {"radius": 36, "fillColor": BRAND, "lineWidth": 5, "lineColor": PAGE_BG}, |
64 | 101 | "dataLabels": { |
65 | 102 | "enabled": True, |
66 | 103 | "format": "{y:,.0f}", |
67 | | - "style": {"fontSize": "22px", "fontWeight": "bold"}, |
| 104 | + "style": {"fontSize": "24px", "color": INK, "fontWeight": "500", "textOutline": "none"}, |
68 | 105 | "verticalAlign": "bottom", |
69 | | - "y": -15, |
| 106 | + "y": -28, |
70 | 107 | }, |
71 | 108 | }, |
72 | 109 | ], |
73 | 110 | } |
74 | 111 |
|
75 | | -# Download Highcharts JS for inline embedding |
76 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
77 | | -with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
78 | | - highcharts_js = response.read().decode("utf-8") |
| 112 | +# Download Highcharts JS for inline embedding (headless Chrome can't load CDN from file://) |
| 113 | +cdn_urls = ["https://code.highcharts.com/highcharts.js", "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"] |
| 114 | +highcharts_js = None |
| 115 | +for url in cdn_urls: |
| 116 | + try: |
| 117 | + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) |
| 118 | + with urllib.request.urlopen(req, timeout=30) as response: |
| 119 | + highcharts_js = response.read().decode("utf-8") |
| 120 | + break |
| 121 | + except Exception: |
| 122 | + continue |
| 123 | +if highcharts_js is None: |
| 124 | + raise RuntimeError("Failed to download Highcharts JS from all CDN sources") |
79 | 125 |
|
80 | 126 | # Generate HTML with inline scripts |
81 | 127 | chart_options_json = json.dumps(chart_options) |
|
85 | 131 | <meta charset="utf-8"> |
86 | 132 | <script>{highcharts_js}</script> |
87 | 133 | </head> |
88 | | -<body style="margin:0;"> |
| 134 | +<body style="margin:0; background:{PAGE_BG};"> |
89 | 135 | <div id="container" style="width: 4800px; height: 2700px;"></div> |
90 | 136 | <script> |
91 | 137 | document.addEventListener('DOMContentLoaded', function() {{ |
|
95 | 141 | </body> |
96 | 142 | </html>""" |
97 | 143 |
|
98 | | -# Write temp HTML file |
99 | | -with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
| 144 | +# Save HTML artifact (theme-suffixed) |
| 145 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
100 | 146 | f.write(html_content) |
101 | | - temp_path = f.name |
102 | 147 |
|
103 | | -# Also save the HTML for interactive viewing |
104 | | -with open("plot.html", "w", encoding="utf-8") as f: |
| 148 | +# Render PNG via headless Chrome |
| 149 | +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
105 | 150 | f.write(html_content) |
| 151 | + temp_path = f.name |
106 | 152 |
|
107 | | -# Take screenshot with headless Chrome |
108 | 153 | chrome_options = Options() |
109 | 154 | chrome_options.add_argument("--headless") |
110 | 155 | chrome_options.add_argument("--no-sandbox") |
|
115 | 160 | driver = webdriver.Chrome(options=chrome_options) |
116 | 161 | driver.get(f"file://{temp_path}") |
117 | 162 | time.sleep(5) |
118 | | -driver.save_screenshot("plot.png") |
| 163 | +driver.save_screenshot(f"plot-{THEME}.png") |
119 | 164 | driver.quit() |
120 | 165 |
|
121 | | -# Clean up temp file |
122 | 166 | Path(temp_path).unlink() |
0 commit comments