|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | waterfall-basic: Basic Waterfall Chart |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-24 |
| 3 | +Library: altair 6.1.0 | Python 3.13.13 |
| 4 | +Quality: 92/100 | Updated: 2026-05-06 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import altair as alt |
8 | 10 | import pandas as pd |
9 | 11 |
|
10 | 12 |
|
| 13 | +# Theme tokens (see prompts/default-style-guide.md) |
| 14 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 15 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 16 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 17 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 18 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 19 | + |
| 20 | +# Okabe-Ito palette for waterfall types |
| 21 | +POSITIVE_COLOR = "#009E73" # Position 1: brand green for positive |
| 22 | +NEGATIVE_COLOR = "#D55E00" # Position 2: vermillion/red for negative |
| 23 | +TOTAL_COLOR = "#0072B2" # Position 3: blue for totals |
| 24 | + |
11 | 25 | # Data: Quarterly financial breakdown from revenue to net income |
12 | 26 | categories = ["Revenue", "Cost of Goods", "Gross Profit", "Operating Expenses", "Other Income", "Taxes", "Net Income"] |
13 | | -# Values: First and last are totals (None), middle values are changes |
14 | 27 | values = [500, -200, None, -150, 25, -45, None] |
15 | 28 |
|
16 | 29 | # Calculate running totals and bar positions |
17 | 30 | n = len(categories) |
18 | 31 | running_total = [0] * n |
19 | 32 | bar_bottom = [0] * n |
20 | 33 | bar_top = [0] * n |
21 | | -bar_types = [] # 'total', 'positive', 'negative' |
| 34 | +bar_types = [] |
22 | 35 |
|
23 | 36 | running_total[0] = values[0] |
24 | 37 | bar_bottom[0] = 0 |
|
28 | 41 | current = values[0] |
29 | 42 | for i in range(1, n): |
30 | 43 | if values[i] is None: |
31 | | - # Subtotal bar (Gross Profit or Net Income) |
32 | 44 | running_total[i] = current |
33 | 45 | bar_bottom[i] = 0 |
34 | 46 | bar_top[i] = current |
35 | 47 | bar_types.append("total") |
36 | 48 | else: |
37 | | - # Change bar |
38 | 49 | running_total[i] = current + values[i] |
39 | 50 | if values[i] >= 0: |
40 | 51 | bar_bottom[i] = current |
|
46 | 57 | bar_types.append("negative") |
47 | 58 | current = running_total[i] |
48 | 59 |
|
49 | | -# Create display values for labels (show change for non-totals, running total for totals) |
| 60 | +# Create display values for labels |
50 | 61 | display_values = [] |
51 | 62 | for i, val in enumerate(values): |
52 | 63 | if val is None: |
|
70 | 81 | } |
71 | 82 | ) |
72 | 83 |
|
73 | | -# Color scale: Python Blue for totals, green for positive, red for negative |
74 | | -color_scale = alt.Scale(domain=["total", "positive", "negative"], range=["#306998", "#4CAF50", "#E53935"]) |
| 84 | +# Color scale using Okabe-Ito palette |
| 85 | +color_scale = alt.Scale(domain=["total", "positive", "negative"], range=[TOTAL_COLOR, POSITIVE_COLOR, NEGATIVE_COLOR]) |
75 | 86 |
|
76 | 87 | # Sort by order field |
77 | 88 | sort_order = alt.EncodingSortField(field="order", order="ascending") |
78 | 89 |
|
79 | 90 | # Create bar chart using bar marks with y and y2 |
80 | 91 | bars = ( |
81 | 92 | alt.Chart(df) |
82 | | - .mark_bar(size=65, stroke="white", strokeWidth=2) |
| 93 | + .mark_bar(size=65, stroke=INK_SOFT, strokeWidth=2) |
83 | 94 | .encode( |
84 | 95 | x=alt.X( |
85 | 96 | "category:N", |
|
96 | 107 | # Value labels on bars |
97 | 108 | labels = ( |
98 | 109 | alt.Chart(df) |
99 | | - .mark_text(fontSize=18, fontWeight="bold", color="white") |
| 110 | + .mark_text(fontSize=18, fontWeight="bold", color=INK) |
100 | 111 | .encode(x=alt.X("category:N", sort=sort_order), y=alt.Y("label_y:Q"), text="display_value:N") |
101 | 112 | ) |
102 | 113 |
|
|
112 | 123 | # Connector lines using rule mark |
113 | 124 | connectors = ( |
114 | 125 | alt.Chart(df_connectors) |
115 | | - .mark_rule(color="#666666", strokeDash=[6, 4], strokeWidth=2) |
| 126 | + .mark_rule(color=INK_SOFT, strokeDash=[6, 4], strokeWidth=2) |
116 | 127 | .encode(x=alt.X("x:N", sort=sort_order), x2=alt.X2("x2:N"), y=alt.Y("y:Q")) |
117 | 128 | ) |
118 | 129 |
|
119 | 130 | # Combine all layers |
120 | 131 | chart = ( |
121 | 132 | alt.layer(connectors, bars, labels) |
122 | | - .properties(width=1600, height=900, title=alt.Title("waterfall-basic · altair · pyplots.ai", fontSize=28)) |
123 | | - .configure_axis(grid=True, gridOpacity=0.3, gridDash=[4, 4]) |
124 | | - .configure_view(strokeWidth=0) |
| 133 | + .properties( |
| 134 | + width=1600, |
| 135 | + height=900, |
| 136 | + background=PAGE_BG, |
| 137 | + title=alt.Title("waterfall-basic · altair · anyplot.ai", fontSize=28, color=INK), |
| 138 | + ) |
| 139 | + .configure_view(fill=PAGE_BG, stroke=INK_SOFT, continuousWidth=1600, continuousHeight=900) |
| 140 | + .configure_axis( |
| 141 | + domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK |
| 142 | + ) |
| 143 | + .configure_title(color=INK) |
125 | 144 | ) |
126 | 145 |
|
127 | | -# Save as PNG and HTML |
128 | | -chart.save("plot.png", scale_factor=3.0) |
129 | | -chart.save("plot.html") |
| 146 | +# Save as PNG and HTML with theme suffix |
| 147 | +chart.save(f"plot-{THEME}.png", scale_factor=3.0) |
| 148 | +chart.save(f"plot-{THEME}.html") |
0 commit comments