Skip to content

Commit 8ec44b0

Browse files
feat(highcharts): implement funnel-basic (#5430)
## Implementation: `funnel-basic` - python/highcharts Implements the **python/highcharts** version of `funnel-basic`. **File:** `plots/funnel-basic/implementations/python/highcharts.py` **Parent Issue:** #789 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24949445580)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 063cf9c commit 8ec44b0

2 files changed

Lines changed: 268 additions & 223 deletions

File tree

Lines changed: 111 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,138 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import base64
8+
import json
9+
import os
710
import tempfile
811
import time
912
import urllib.request
1013
from pathlib import Path
1114

12-
from highcharts_core.chart import Chart
13-
from highcharts_core.options import HighchartsOptions
14-
from highcharts_core.options.series.funnel import FunnelSeries
1515
from selenium import webdriver
1616
from selenium.webdriver.chrome.options import Options
1717

1818

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)"
3525

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"]
4128

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]
4432

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))
5036
]
5137

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}],
58102
}
59103

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"
75107

76-
# Download Highcharts JS and funnel module for headless Chrome
77-
highcharts_url = "https://code.highcharts.com/highcharts.js"
78108
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
79109
highcharts_js = response.read().decode("utf-8")
80-
81-
funnel_url = "https://code.highcharts.com/modules/funnel.js"
82110
with urllib.request.urlopen(funnel_url, timeout=30) as response:
83111
funnel_js = response.read().decode("utf-8")
84112

85-
# Generate HTML with inline scripts
86-
html_str = chart.to_js_literal()
113+
chart_options_json = json.dumps(chart_options)
87114
html_content = f"""<!DOCTYPE html>
88115
<html>
89116
<head>
90117
<meta charset="utf-8">
91118
<script>{highcharts_js}</script>
92119
<script>{funnel_js}</script>
93120
</head>
94-
<body style="margin:0;">
121+
<body style="margin:0; background:{PAGE_BG};">
95122
<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>
97126
</body>
98127
</html>"""
99128

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+
101132
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
102133
f.write(html_content)
103134
temp_path = f.name
104135

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
122136
chrome_options = Options()
123137
chrome_options.add_argument("--headless")
124138
chrome_options.add_argument("--no-sandbox")
@@ -127,10 +141,23 @@
127141
chrome_options.add_argument("--window-size=4800,2700")
128142

129143
driver = webdriver.Chrome(options=chrome_options)
144+
driver.execute_cdp_cmd(
145+
"Emulation.setDeviceMetricsOverride", {"width": 4800, "height": 2700, "deviceScaleFactor": 1, "mobile": False}
146+
)
130147
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+
133161
driver.quit()
134162

135-
# Clean up temp file
136163
Path(temp_path).unlink()

0 commit comments

Comments
 (0)