|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | waterfall-basic: Basic Waterfall Chart |
3 | | -Library: plotnine 0.15.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-14 |
| 3 | +Library: plotnine 0.15.4 | Python 3.13.13 |
| 4 | +Quality: 84/100 | Created: 2026-05-06 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import pandas as pd |
8 | 10 | from plotnine import ( |
9 | 11 | aes, |
10 | 12 | element_blank, |
| 13 | + element_line, |
| 14 | + element_rect, |
11 | 15 | element_text, |
12 | 16 | geom_rect, |
13 | 17 | geom_segment, |
|
21 | 25 | ) |
22 | 26 |
|
23 | 27 |
|
| 28 | +# Theme tokens |
| 29 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 30 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 31 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 32 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 33 | + |
24 | 34 | # Data - quarterly financial breakdown |
25 | | -categories = ["Revenue", "Product Costs", "Marketing", "Operations", "R&D", "Admin", "Taxes", "Net Profit"] |
26 | | -values = [500, -150, -60, -45, -35, -30, -40, 140] |
| 35 | +categories = ["Starting Balance", "Q1 Sales", "Operating Costs", "R&D Investment", "Tax Payment", "Ending Balance"] |
| 36 | +values = [1000, 450, -280, -120, -150, 900] |
27 | 37 |
|
28 | 38 | # Calculate waterfall positions |
29 | 39 | df = pd.DataFrame({"category": categories, "value": values}) |
|
35 | 45 | ends = [] |
36 | 46 | bar_types = [] |
37 | 47 |
|
38 | | -for i, (_cat, val) in enumerate(zip(categories, values, strict=True)): |
| 48 | +for i, val in enumerate(values): |
39 | 49 | if i == 0: |
40 | | - # First bar: starts at 0, ends at value |
41 | 50 | starts.append(0) |
42 | 51 | ends.append(val) |
43 | 52 | bar_types.append("total") |
44 | 53 | running_total = val |
45 | | - elif i == len(categories) - 1: |
46 | | - # Last bar: total bar starting from 0 |
| 54 | + elif i == len(values) - 1: |
47 | 55 | starts.append(0) |
48 | 56 | ends.append(running_total) |
49 | 57 | bar_types.append("total") |
50 | 58 | else: |
51 | | - # Middle bars: changes |
52 | 59 | if val >= 0: |
53 | 60 | starts.append(running_total) |
54 | 61 | ends.append(running_total + val) |
|
64 | 71 | df["bar_type"] = bar_types |
65 | 72 | df["x_pos"] = range(len(categories)) |
66 | 73 |
|
67 | | -# Create connector line data (from end of one bar to start of next) |
| 74 | +# Create connector lines |
68 | 75 | connectors = [] |
69 | 76 | for i in range(len(df) - 1): |
70 | | - if i < len(df) - 2: # Don't connect to the final total bar |
| 77 | + if i < len(df) - 2: |
71 | 78 | connectors.append( |
72 | 79 | {"x_start": df.iloc[i]["x_pos"] + 0.4, "x_end": df.iloc[i + 1]["x_pos"] - 0.4, "y": df.iloc[i]["end"]} |
73 | 80 | ) |
74 | | -connector_df = pd.DataFrame(connectors) |
| 81 | +connector_df = pd.DataFrame(connectors) if connectors else pd.DataFrame() |
| 82 | + |
| 83 | +# Colors: green for positive, red for negative, gray for totals |
| 84 | +colors = {"total": INK_SOFT, "positive": "#2ecc71", "negative": "#e74c3c"} |
75 | 85 |
|
76 | | -# Add labels for values |
77 | | -df["label_y"] = df.apply(lambda row: row["end"] + 10 if row["end"] >= row["start"] else row["start"] + 10, axis=1) |
78 | | -df["label_text"] = df.apply( |
79 | | - lambda row: f"+{int(row['value'])}" if row["value"] > 0 and row["bar_type"] != "total" else str(int(row["value"])), |
80 | | - axis=1, |
| 86 | +# Create plot |
| 87 | +plot = ggplot() + geom_rect( |
| 88 | + df, aes(xmin="x_pos - 0.35", xmax="x_pos + 0.35", ymin="start", ymax="end", fill="bar_type") |
81 | 89 | ) |
82 | 90 |
|
83 | | -# Colors: blue for totals, green for positive, red for negative |
84 | | -colors = {"total": "#306998", "positive": "#2ECC71", "negative": "#E74C3C"} |
| 91 | +if not connector_df.empty: |
| 92 | + plot = plot + geom_segment( |
| 93 | + connector_df, aes(x="x_start", xend="x_end", y="y", yend="y"), color=INK_SOFT, size=0.5, alpha=0.5 |
| 94 | + ) |
85 | 95 |
|
86 | | -# Create plot |
87 | 96 | plot = ( |
88 | | - ggplot() |
89 | | - + geom_rect(df, aes(xmin="x_pos - 0.4", xmax="x_pos + 0.4", ymin="start", ymax="end", fill="bar_type")) |
90 | | - + geom_segment( |
91 | | - connector_df, aes(x="x_start", xend="x_end", y="y", yend="y"), color="#666666", size=0.5, linetype="dashed" |
92 | | - ) |
93 | | - + geom_text(df, aes(x="x_pos", y="label_y", label="label_text"), size=12, color="#333333") |
| 97 | + plot |
| 98 | + + geom_text(df, aes(x="x_pos", y="end", label="value"), size=10, color=INK) |
94 | 99 | + scale_fill_manual( |
95 | | - values=colors, name="Type", labels={"total": "Total", "positive": "Increase", "negative": "Decrease"} |
| 100 | + values=colors, name="Category", labels={"total": "Total", "positive": "Increase", "negative": "Decrease"} |
96 | 101 | ) |
97 | | - + labs(x="", y="Amount ($K)", title="Quarterly Financials · waterfall-basic · plotnine · pyplots.ai") |
| 102 | + + scale_x_continuous(breaks=list(range(len(categories))), labels=categories, limits=(-0.6, len(categories) - 0.4)) |
| 103 | + + labs(x="", y="Amount ($1K)", title="Quarterly Financial Summary · waterfall-basic · plotnine · anyplot.ai") |
98 | 104 | + theme_minimal() |
99 | 105 | + theme( |
100 | 106 | figure_size=(16, 9), |
101 | | - text=element_text(size=14), |
102 | | - axis_title=element_text(size=20), |
103 | | - axis_text_x=element_text(size=16, rotation=30, ha="right"), |
104 | | - axis_text_y=element_text(size=16), |
105 | | - plot_title=element_text(size=24), |
106 | | - legend_text=element_text(size=16), |
107 | | - legend_position="top", |
| 107 | + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
| 108 | + panel_background=element_rect(fill=PAGE_BG), |
| 109 | + panel_grid_major=element_line(color=INK_SOFT, size=0.3, alpha=0.1, linewidth=0.8), |
108 | 110 | panel_grid_minor=element_blank(), |
| 111 | + panel_border=element_rect(color=INK_SOFT, fill=None, size=0.5), |
| 112 | + axis_title=element_text(size=20, color=INK), |
| 113 | + axis_text_x=element_text(size=16, color=INK_SOFT, angle=45, ha="right"), |
| 114 | + axis_text_y=element_text(size=16, color=INK_SOFT), |
| 115 | + axis_line=element_line(color=INK_SOFT, size=0.5), |
| 116 | + plot_title=element_text(size=24, color=INK, face="medium"), |
| 117 | + legend_background=element_rect(fill=PAGE_BG, color=INK_SOFT, size=0.5), |
| 118 | + legend_text=element_text(size=16, color=INK_SOFT), |
| 119 | + legend_title=element_text(size=16, color=INK), |
| 120 | + legend_position="top", |
109 | 121 | ) |
110 | 122 | ) |
111 | 123 |
|
112 | | -# Add x-axis labels |
113 | | -plot = plot + scale_x_continuous(breaks=list(range(len(categories))), labels=categories) |
114 | | - |
115 | | -plot.save("plot.png", dpi=300) |
| 124 | +plot.save(f"plot-{THEME}.png", dpi=300, width=16, height=9) |
0 commit comments