Skip to content

Commit 416faf4

Browse files
feat(letsplot): implement funnel-basic (#5431)
## Implementation: `funnel-basic` - python/letsplot Implements the **python/letsplot** version of `funnel-basic`. **File:** `plots/funnel-basic/implementations/python/letsplot.py` **Parent Issue:** #789 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24949525450)* --------- 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 8ec44b0 commit 416faf4

2 files changed

Lines changed: 260 additions & 210 deletions

File tree

Lines changed: 82 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,104 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
funnel-basic: Basic Funnel Chart
3-
Library: letsplot 4.8.2 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: letsplot 4.9.0 | Python 3.14.4
4+
Quality: 86/100 | Updated: 2026-04-26
55
"""
66

7+
import os
8+
79
import pandas as pd
8-
from lets_plot import * # noqa: F403
9-
from lets_plot.export import ggsave as export_ggsave
10+
from lets_plot import (
11+
LetsPlot,
12+
aes,
13+
element_blank,
14+
element_rect,
15+
element_text,
16+
geom_polygon,
17+
geom_text,
18+
ggplot,
19+
ggsave,
20+
ggsize,
21+
labs,
22+
scale_color_identity,
23+
scale_fill_manual,
24+
scale_y_reverse,
25+
theme,
26+
theme_void,
27+
)
28+
1029

30+
LetsPlot.setup_html()
1131

12-
LetsPlot.setup_html() # noqa: F405
32+
# Theme tokens
33+
THEME = os.getenv("ANYPLOT_THEME", "light")
34+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
35+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1336

14-
# Data - Sales funnel example
37+
# Okabe-Ito palette — first stage is brand green (#009E73)
38+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]
39+
# Light orange (#E69F00) needs dark text for contrast; others use white.
40+
TEXT_ON_FILL = ["white", "white", "white", "white", INK]
41+
42+
# Data — sales funnel example from specification
1543
stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"]
1644
values = [1000, 600, 400, 200, 100]
17-
18-
# Calculate percentages and build funnel geometry
19-
max_value = values[0]
2045
n_stages = len(stages)
46+
max_value = values[0]
2147

22-
# Build polygon data for trapezoid shapes
48+
# Build trapezoid polygon vertices for each stage
49+
gap = 0.06
2350
polygons = []
24-
y_top = 0
25-
stage_height = 1.0
26-
27-
for i, (stage, value) in enumerate(zip(stages, values, strict=True)):
28-
# Calculate widths proportional to max value
29-
width = value / max_value
30-
31-
# Next stage width (minimum 15% to keep readable shape)
32-
if i < n_stages - 1:
33-
next_width = max(values[i + 1] / max_value, 0.15)
34-
else:
35-
next_width = max(width * 0.5, 0.10) # Wider base for final stage
36-
37-
y_bottom = y_top + stage_height
38-
39-
# Trapezoid vertices (clockwise from top-left)
40-
half_w_top = width / 2
41-
half_w_bottom = next_width / 2
42-
43-
polygons.append({"stage": stage, "x": -half_w_top, "y": y_top, "order": 0})
44-
polygons.append({"stage": stage, "x": half_w_top, "y": y_top, "order": 1})
45-
polygons.append({"stage": stage, "x": half_w_bottom, "y": y_bottom, "order": 2})
46-
polygons.append({"stage": stage, "x": -half_w_bottom, "y": y_bottom, "order": 3})
47-
48-
y_top = y_bottom
49-
50-
df_poly = pd.DataFrame(polygons)
51-
52-
# Label positions (center of each trapezoid)
5351
labels = []
54-
y_pos = 0.5
55-
for stage, value in zip(stages, values, strict=True):
52+
for i, (stage, value) in enumerate(zip(stages, values, strict=True)):
53+
w_top = value / max_value
54+
w_bot = (values[i + 1] / max_value) if i + 1 < n_stages else w_top
55+
y_top = i + gap / 2
56+
y_bot = (i + 1) - gap / 2
57+
polygons.extend(
58+
[
59+
{"stage": stage, "x": -w_top / 2, "y": y_top, "order": 0},
60+
{"stage": stage, "x": w_top / 2, "y": y_top, "order": 1},
61+
{"stage": stage, "x": w_bot / 2, "y": y_bot, "order": 2},
62+
{"stage": stage, "x": -w_bot / 2, "y": y_bot, "order": 3},
63+
]
64+
)
5665
pct = value / max_value * 100
57-
labels.append({"stage": stage, "x": 0, "y": y_pos, "label": f"{stage}\n{value:,} ({pct:.0f}%)"})
58-
y_pos += stage_height
66+
labels.append(
67+
{
68+
"stage": stage,
69+
"x": 0,
70+
"y": (y_top + y_bot) / 2,
71+
"label": f"{stage}\n{value:,} · {pct:.0f}%",
72+
"text_color": TEXT_ON_FILL[i],
73+
}
74+
)
5975

76+
df_poly = pd.DataFrame(polygons)
77+
df_poly["stage"] = pd.Categorical(df_poly["stage"], categories=stages, ordered=True)
6078
df_labels = pd.DataFrame(labels)
6179

62-
# Colors for each stage - Python blue as primary with variations
63-
colors = ["#306998", "#4A90D9", "#FFD43B", "#F5A623", "#D45D00"]
64-
65-
# Create plot
80+
# Plot
6681
plot = (
67-
ggplot() # noqa: F405
68-
+ geom_polygon( # noqa: F405
69-
aes(x="x", y="y", fill="stage", group="stage"), # noqa: F405
70-
data=df_poly,
71-
color="white",
72-
size=2,
73-
alpha=0.9,
74-
)
75-
+ geom_text( # noqa: F405
76-
aes(x="x", y="y", label="label"), # noqa: F405
77-
data=df_labels,
78-
size=12,
79-
color="white",
80-
fontface="bold",
81-
)
82-
+ scale_fill_manual(values=colors) # noqa: F405
83-
+ scale_y_reverse() # noqa: F405 # Top to bottom
84-
+ labs(title="funnel-basic · letsplot · pyplots.ai", x="", y="") # noqa: F405
85-
+ theme_void() # noqa: F405
86-
+ theme( # noqa: F405
87-
plot_title=element_text(size=24, hjust=0.5, face="bold"), # noqa: F405
82+
ggplot()
83+
+ geom_polygon(aes(x="x", y="y", fill="stage", group="stage"), data=df_poly, color=PAGE_BG, size=2)
84+
+ geom_text(aes(x="x", y="y", label="label", color="text_color"), data=df_labels, size=12, fontface="bold")
85+
+ scale_fill_manual(values=OKABE_ITO[:n_stages], guide="none")
86+
+ scale_color_identity()
87+
+ scale_y_reverse()
88+
+ labs(title="funnel-basic · letsplot · anyplot.ai", x="", y="")
89+
+ theme_void()
90+
+ theme(
91+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
92+
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
93+
plot_title=element_text(size=24, color=INK, face="bold", hjust=0.5),
94+
axis_text=element_blank(),
95+
axis_ticks=element_blank(),
96+
panel_grid=element_blank(),
8897
legend_position="none",
8998
)
90-
+ ggsize(1600, 900) # noqa: F405
99+
+ ggsize(1600, 900)
91100
)
92101

93-
# Save outputs to current directory
94-
export_ggsave(plot, "plot.png", path=".", scale=3)
95-
export_ggsave(plot, "plot.html", path=".")
102+
# Save
103+
ggsave(plot, f"plot-{THEME}.png", path=".", scale=3)
104+
ggsave(plot, f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)