|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
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 |
8 | 10 | import sys |
9 | 11 |
|
10 | 12 |
|
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")] |
12 | 14 |
|
13 | 15 | import pandas as pd # noqa: E402 |
14 | 16 | from plotnine import ( # noqa: E402 |
15 | 17 | aes, |
16 | 18 | element_blank, |
| 19 | + element_rect, |
17 | 20 | element_text, |
18 | | - geom_rect, |
| 21 | + geom_polygon, |
19 | 22 | geom_text, |
20 | 23 | ggplot, |
21 | 24 | labs, |
| 25 | + scale_color_identity, |
22 | 26 | scale_fill_manual, |
| 27 | + scale_y_reverse, |
23 | 28 | theme, |
24 | | - theme_minimal, |
| 29 | + theme_void, |
25 | 30 | ) |
26 | 31 |
|
27 | 32 |
|
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" |
31 | 37 |
|
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] |
33 | 42 |
|
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] |
45 | 48 |
|
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 | + ) |
48 | 77 |
|
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) |
51 | 81 |
|
52 | | -# Create funnel plot using rectangles |
| 82 | +# Plot |
53 | 83 | 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() |
60 | 98 | + theme( |
61 | 99 | 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, |
63 | 104 | axis_text=element_blank(), |
64 | 105 | axis_ticks=element_blank(), |
65 | 106 | panel_grid=element_blank(), |
|
68 | 109 | ) |
69 | 110 |
|
70 | 111 | # Save |
71 | | -plot.save("plot.png", dpi=300, verbose=False) |
| 112 | +plot.save(f"plot-{THEME}.png", dpi=300, verbose=False) |
0 commit comments