Skip to content

Commit b12ba37

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

2 files changed

Lines changed: 236 additions & 158 deletions

File tree

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,106 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
funnel-basic: Basic Funnel Chart
3-
Library: plotnine 0.15.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: plotnine 0.15.3 | Python 3.14.4
4+
Quality: 87/100 | Updated: 2026-04-26
55
"""
66

7-
# Fix import conflict: script named plotnine.py shadows the plotnine package
7+
# Script filename "plotnine.py" shadows the plotnine package on sys.path.
8+
# Drop the script's own directory before importing.
9+
import os
810
import sys
911

1012

11-
sys.path = [p for p in sys.path if not p.endswith("implementations")]
13+
sys.path = [p for p in sys.path if not p.endswith("implementations/python")]
1214

1315
import pandas as pd # noqa: E402
1416
from plotnine import ( # noqa: E402
1517
aes,
1618
element_blank,
19+
element_rect,
1720
element_text,
18-
geom_rect,
21+
geom_polygon,
1922
geom_text,
2023
ggplot,
2124
labs,
25+
scale_color_identity,
2226
scale_fill_manual,
27+
scale_y_reverse,
2328
theme,
24-
theme_minimal,
29+
theme_void,
2530
)
2631

2732

28-
# Data - Sales funnel with 5 stages showing clear progression
29-
stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"]
30-
values = [1000, 600, 400, 200, 100]
33+
# Theme tokens
34+
THEME = os.getenv("ANYPLOT_THEME", "light")
35+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
36+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
3137

32-
df = pd.DataFrame({"stage": stages, "value": values})
38+
# Okabe-Ito palette — first stage is brand green (#009E73)
39+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]
40+
# Light orange (#E69F00) needs dark text for contrast; others use white.
41+
TEXT_ON_FILL = ["white", "white", "white", "white", INK]
3342

34-
# Calculate funnel geometry - centered bars with widths proportional to values
35-
max_value = df["value"].max()
36-
df["width"] = df["value"] / max_value # Normalize widths (0-1 scale)
37-
df["y"] = range(len(df), 0, -1) # Y positions (top to bottom)
38-
df["xmin"] = -df["width"] / 2
39-
df["xmax"] = df["width"] / 2
40-
df["ymin"] = df["y"] - 0.4
41-
df["ymax"] = df["y"] + 0.4
42-
43-
# Labels with values and percentages
44-
df["label"] = df.apply(lambda row: f"{row['stage']}\n{row['value']:,} ({row['value'] / max_value * 100:.0f}%)", axis=1)
43+
# Data — sales funnel example from specification
44+
stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"]
45+
values = [1000, 600, 400, 200, 100]
46+
n_stages = len(stages)
47+
max_value = values[0]
4548

46-
# Stage as ordered categorical for legend order
47-
df["stage"] = pd.Categorical(df["stage"], categories=stages, ordered=True)
49+
# Build trapezoid polygon vertices for each stage
50+
gap = 0.06
51+
polygons = []
52+
labels = []
53+
for i, (stage, value) in enumerate(zip(stages, values, strict=True)):
54+
w_top = value / max_value
55+
# Flat bottom on the last segment — match its own width.
56+
w_bot = (values[i + 1] / max_value) if i + 1 < n_stages else w_top
57+
y_top = i + gap / 2
58+
y_bot = (i + 1) - gap / 2
59+
polygons.extend(
60+
[
61+
{"stage": stage, "x": -w_top / 2, "y": y_top, "order": 0},
62+
{"stage": stage, "x": w_top / 2, "y": y_top, "order": 1},
63+
{"stage": stage, "x": w_bot / 2, "y": y_bot, "order": 2},
64+
{"stage": stage, "x": -w_bot / 2, "y": y_bot, "order": 3},
65+
]
66+
)
67+
pct = value / max_value * 100
68+
labels.append(
69+
{
70+
"stage": stage,
71+
"x": 0,
72+
"y": (y_top + y_bot) / 2,
73+
"label": f"{stage}\n{value:,} · {pct:.0f}%",
74+
"text_color": TEXT_ON_FILL[i],
75+
}
76+
)
4877

49-
# Colors - Python Blue to lighter shades for progression
50-
colors = ["#306998", "#4A7FA8", "#6495B8", "#7EABC8", "#98C1D8"]
78+
df_poly = pd.DataFrame(polygons)
79+
df_poly["stage"] = pd.Categorical(df_poly["stage"], categories=stages, ordered=True)
80+
df_labels = pd.DataFrame(labels)
5181

52-
# Create funnel plot using rectangles
82+
# Plot
5383
plot = (
54-
ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="stage"))
55-
+ geom_rect(color="white", size=0.5)
56-
+ geom_text(aes(x=0, y="y", label="label"), color="white", size=14, fontweight="bold")
57-
+ scale_fill_manual(values=colors)
58-
+ labs(title="funnel-basic · plotnine · pyplots.ai", x="", y="")
59-
+ theme_minimal()
84+
ggplot(df_poly, aes(x="x", y="y", fill="stage", group="stage"))
85+
+ geom_polygon(color=PAGE_BG, size=2)
86+
+ geom_text(
87+
aes(x="x", y="y", label="label", color="text_color"),
88+
data=df_labels,
89+
size=14,
90+
fontweight="bold",
91+
inherit_aes=False,
92+
)
93+
+ scale_fill_manual(values=OKABE_ITO[:n_stages], guide=None)
94+
+ scale_color_identity()
95+
+ scale_y_reverse()
96+
+ labs(title="funnel-basic · plotnine · anyplot.ai", x="", y="")
97+
+ theme_void()
6098
+ theme(
6199
figure_size=(16, 9),
62-
plot_title=element_text(size=24, ha="center", weight="bold"),
100+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
101+
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
102+
plot_title=element_text(size=24, color=INK, weight="medium", ha="center"),
103+
plot_margin=0.02,
63104
axis_text=element_blank(),
64105
axis_ticks=element_blank(),
65106
panel_grid=element_blank(),
@@ -68,4 +109,4 @@
68109
)
69110

70111
# Save
71-
plot.save("plot.png", dpi=300, verbose=False)
112+
plot.save(f"plot-{THEME}.png", dpi=300, verbose=False)

0 commit comments

Comments
 (0)