|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | funnel-basic: Basic Funnel Chart |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: highcharts unknown | Python 3.14.4 |
| 4 | +Quality: 89/100 | Updated: 2026-04-26 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import base64 |
| 8 | +import json |
| 9 | +import os |
7 | 10 | import tempfile |
8 | 11 | import time |
9 | 12 | import urllib.request |
10 | 13 | from pathlib import Path |
11 | 14 |
|
12 | | -from highcharts_core.chart import Chart |
13 | | -from highcharts_core.options import HighchartsOptions |
14 | | -from highcharts_core.options.series.funnel import FunnelSeries |
15 | 15 | from selenium import webdriver |
16 | 16 | from selenium.webdriver.chrome.options import Options |
17 | 17 |
|
18 | 18 |
|
19 | | -# Data - Sales funnel example |
20 | | -stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"] |
21 | | -values = [1000, 600, 400, 200, 100] |
22 | | - |
23 | | -# Create chart with container |
24 | | -chart = Chart(container="container") |
25 | | -chart.options = HighchartsOptions() |
26 | | - |
27 | | -# Chart configuration for 4800x2700 px |
28 | | -chart.options.chart = { |
29 | | - "type": "funnel", |
30 | | - "width": 4800, |
31 | | - "height": 2700, |
32 | | - "backgroundColor": "#ffffff", |
33 | | - "marginBottom": 150, |
34 | | -} |
| 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)" |
35 | 25 |
|
36 | | -# Title |
37 | | -chart.options.title = { |
38 | | - "text": "funnel-basic \u00b7 highcharts \u00b7 pyplots.ai", |
39 | | - "style": {"fontSize": "72px", "fontWeight": "bold"}, |
40 | | -} |
| 26 | +# Okabe-Ito categorical palette (first series is always #009E73) |
| 27 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"] |
41 | 28 |
|
42 | | -# Colorblind-safe colors for each stage |
43 | | -colors = ["#306998", "#FFD43B", "#9467BD", "#17BECF", "#8C564B"] |
| 29 | +# Sales funnel: visitors progressing from initial awareness through purchase. |
| 30 | +stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"] |
| 31 | +values = [1000, 600, 400, 200, 100] |
44 | 32 |
|
45 | | -# Create funnel series with data |
46 | | -series = FunnelSeries() |
47 | | -series.name = "Sales Funnel" |
48 | | -series.data = [ |
49 | | - {"name": stage, "y": value, "color": colors[i]} for i, (stage, value) in enumerate(zip(stages, values, strict=True)) |
| 33 | +funnel_data = [ |
| 34 | + {"name": stage, "y": value, "color": OKABE_ITO[i]} |
| 35 | + for i, (stage, value) in enumerate(zip(stages, values, strict=True)) |
50 | 36 | ] |
51 | 37 |
|
52 | | -# Configure data labels to show values and percentages |
53 | | -series.data_labels = { |
54 | | - "enabled": True, |
55 | | - "format": "<b>{point.name}</b>: {point.y:,.0f} ({point.percentage:.1f}%)", |
56 | | - "style": {"fontSize": "36px", "fontWeight": "normal", "textOutline": "2px white"}, |
57 | | - "softConnector": True, |
| 38 | +chart_options = { |
| 39 | + "chart": { |
| 40 | + "type": "funnel", |
| 41 | + "width": 4800, |
| 42 | + "height": 2700, |
| 43 | + "backgroundColor": PAGE_BG, |
| 44 | + "marginTop": 260, |
| 45 | + "marginBottom": 160, |
| 46 | + "spacingLeft": 80, |
| 47 | + "spacingRight": 80, |
| 48 | + "style": {"fontFamily": "Arial, sans-serif", "color": INK}, |
| 49 | + }, |
| 50 | + "title": { |
| 51 | + "text": "funnel-basic · highcharts · anyplot.ai", |
| 52 | + "align": "left", |
| 53 | + "x": 80, |
| 54 | + "y": 80, |
| 55 | + "style": {"fontSize": "56px", "fontWeight": "500", "color": INK}, |
| 56 | + }, |
| 57 | + "subtitle": { |
| 58 | + "text": "Sales funnel: visitor progression from awareness to purchase", |
| 59 | + "align": "left", |
| 60 | + "x": 80, |
| 61 | + "y": 150, |
| 62 | + "style": {"fontSize": "30px", "color": INK_SOFT}, |
| 63 | + }, |
| 64 | + "legend": {"enabled": False}, |
| 65 | + "credits": {"enabled": False}, |
| 66 | + "tooltip": { |
| 67 | + "useHTML": True, |
| 68 | + "backgroundColor": ELEVATED_BG, |
| 69 | + "borderColor": GRID, |
| 70 | + "borderRadius": 8, |
| 71 | + "style": {"color": INK, "fontSize": "22px"}, |
| 72 | + "headerFormat": "", |
| 73 | + "pointFormat": ( |
| 74 | + "<span style='color:{point.color}; font-size:28px'>●</span> " |
| 75 | + "<b>{point.name}</b><br/>" |
| 76 | + "Value: <b>{point.y:,.0f}</b><br/>" |
| 77 | + "Share: <b>{point.percentage:.1f}%</b>" |
| 78 | + ), |
| 79 | + }, |
| 80 | + "plotOptions": { |
| 81 | + "funnel": { |
| 82 | + "neckWidth": "30%", |
| 83 | + "neckHeight": "25%", |
| 84 | + "width": "55%", |
| 85 | + "height": "78%", |
| 86 | + "center": ["42%", "55%"], |
| 87 | + "borderColor": PAGE_BG, |
| 88 | + "borderWidth": 6, |
| 89 | + "states": {"hover": {"brightness": 0.08, "halo": {"size": 0}}}, |
| 90 | + "dataLabels": { |
| 91 | + "enabled": True, |
| 92 | + "softConnector": True, |
| 93 | + "distance": 60, |
| 94 | + "connectorWidth": 3, |
| 95 | + "connectorColor": INK_SOFT, |
| 96 | + "format": "<b>{point.name}</b><br/><span style='font-weight:400'>{point.y:,.0f} ({point.percentage:.1f}%)</span>", |
| 97 | + "style": {"fontSize": "36px", "color": INK, "textOutline": "none"}, |
| 98 | + }, |
| 99 | + } |
| 100 | + }, |
| 101 | + "series": [{"type": "funnel", "name": "Sales funnel", "data": funnel_data}], |
58 | 102 | } |
59 | 103 |
|
60 | | -# Configure funnel appearance |
61 | | -series.neck_width = "30%" |
62 | | -series.neck_height = "25%" |
63 | | -series.width = "80%" |
64 | | - |
65 | | -chart.add_series(series) |
66 | | - |
67 | | -# Legend configuration |
68 | | -chart.options.legend = {"enabled": False} |
69 | | - |
70 | | -# Plot options for funnel |
71 | | -chart.options.plot_options = {"funnel": {"dataLabels": {"enabled": True, "style": {"fontSize": "36px"}}}} |
72 | | - |
73 | | -# Credits |
74 | | -chart.options.credits = {"enabled": False} |
| 104 | +# Highcharts core + funnel module. |
| 105 | +highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@12/highcharts.js" |
| 106 | +funnel_url = "https://cdn.jsdelivr.net/npm/highcharts@12/modules/funnel.js" |
75 | 107 |
|
76 | | -# Download Highcharts JS and funnel module for headless Chrome |
77 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
78 | 108 | with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
79 | 109 | highcharts_js = response.read().decode("utf-8") |
80 | | - |
81 | | -funnel_url = "https://code.highcharts.com/modules/funnel.js" |
82 | 110 | with urllib.request.urlopen(funnel_url, timeout=30) as response: |
83 | 111 | funnel_js = response.read().decode("utf-8") |
84 | 112 |
|
85 | | -# Generate HTML with inline scripts |
86 | | -html_str = chart.to_js_literal() |
| 113 | +chart_options_json = json.dumps(chart_options) |
87 | 114 | html_content = f"""<!DOCTYPE html> |
88 | 115 | <html> |
89 | 116 | <head> |
90 | 117 | <meta charset="utf-8"> |
91 | 118 | <script>{highcharts_js}</script> |
92 | 119 | <script>{funnel_js}</script> |
93 | 120 | </head> |
94 | | -<body style="margin:0;"> |
| 121 | +<body style="margin:0; background:{PAGE_BG};"> |
95 | 122 | <div id="container" style="width: 4800px; height: 2700px;"></div> |
96 | | - <script>{html_str}</script> |
| 123 | + <script> |
| 124 | + Highcharts.chart('container', {chart_options_json}); |
| 125 | + </script> |
97 | 126 | </body> |
98 | 127 | </html>""" |
99 | 128 |
|
100 | | -# Write temp HTML and take screenshot |
| 129 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
| 130 | + f.write(html_content) |
| 131 | + |
101 | 132 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
102 | 133 | f.write(html_content) |
103 | 134 | temp_path = f.name |
104 | 135 |
|
105 | | -# Save HTML for interactive version (use CDN for portability) |
106 | | -html_cdn = f"""<!DOCTYPE html> |
107 | | -<html> |
108 | | -<head> |
109 | | - <meta charset="utf-8"> |
110 | | - <script src="https://code.highcharts.com/highcharts.js"></script> |
111 | | - <script src="https://code.highcharts.com/modules/funnel.js"></script> |
112 | | -</head> |
113 | | -<body style="margin:0;"> |
114 | | - <div id="container" style="width: 100%; height: 100vh;"></div> |
115 | | - <script>{html_str}</script> |
116 | | -</body> |
117 | | -</html>""" |
118 | | -with open("plot.html", "w", encoding="utf-8") as f: |
119 | | - f.write(html_cdn) |
120 | | - |
121 | | -# Configure headless Chrome |
122 | 136 | chrome_options = Options() |
123 | 137 | chrome_options.add_argument("--headless") |
124 | 138 | chrome_options.add_argument("--no-sandbox") |
|
127 | 141 | chrome_options.add_argument("--window-size=4800,2700") |
128 | 142 |
|
129 | 143 | driver = webdriver.Chrome(options=chrome_options) |
| 144 | +driver.execute_cdp_cmd( |
| 145 | + "Emulation.setDeviceMetricsOverride", {"width": 4800, "height": 2700, "deviceScaleFactor": 1, "mobile": False} |
| 146 | +) |
130 | 147 | driver.get(f"file://{temp_path}") |
131 | | -time.sleep(5) # Wait for chart to render |
132 | | -driver.save_screenshot("plot.png") |
| 148 | +time.sleep(5) |
| 149 | + |
| 150 | +result = driver.execute_cdp_cmd( |
| 151 | + "Page.captureScreenshot", |
| 152 | + { |
| 153 | + "captureBeyondViewport": True, |
| 154 | + "clip": {"x": 0, "y": 0, "width": 4800, "height": 2700, "scale": 1}, |
| 155 | + "format": "png", |
| 156 | + }, |
| 157 | +) |
| 158 | +with open(f"plot-{THEME}.png", "wb") as f: |
| 159 | + f.write(base64.b64decode(result["data"])) |
| 160 | + |
133 | 161 | driver.quit() |
134 | 162 |
|
135 | | -# Clean up temp file |
136 | 163 | Path(temp_path).unlink() |
0 commit comments