|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | 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 | + |
10 | 29 |
|
| 30 | +LetsPlot.setup_html() |
11 | 31 |
|
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" |
13 | 36 |
|
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 |
15 | 43 | stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"] |
16 | 44 | values = [1000, 600, 400, 200, 100] |
17 | | - |
18 | | -# Calculate percentages and build funnel geometry |
19 | | -max_value = values[0] |
20 | 45 | n_stages = len(stages) |
| 46 | +max_value = values[0] |
21 | 47 |
|
22 | | -# Build polygon data for trapezoid shapes |
| 48 | +# Build trapezoid polygon vertices for each stage |
| 49 | +gap = 0.06 |
23 | 50 | 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) |
53 | 51 | 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 | + ) |
56 | 65 | 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 | + ) |
59 | 75 |
|
| 76 | +df_poly = pd.DataFrame(polygons) |
| 77 | +df_poly["stage"] = pd.Categorical(df_poly["stage"], categories=stages, ordered=True) |
60 | 78 | df_labels = pd.DataFrame(labels) |
61 | 79 |
|
62 | | -# Colors for each stage - Python blue as primary with variations |
63 | | -colors = ["#306998", "#4A90D9", "#FFD43B", "#F5A623", "#D45D00"] |
64 | | - |
65 | | -# Create plot |
| 80 | +# Plot |
66 | 81 | 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(), |
88 | 97 | legend_position="none", |
89 | 98 | ) |
90 | | - + ggsize(1600, 900) # noqa: F405 |
| 99 | + + ggsize(1600, 900) |
91 | 100 | ) |
92 | 101 |
|
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