{"spec_id":"waterfall-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nwaterfall-basic: Basic Waterfall Chart\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette, with a semantic exception for the profit/loss convention:\n# green for gains, matte red for losses (both drawn from the Imprint pool),\n# blue for the start/end total bars per the spec's \"blue or gray\" example.\nBRAND = \"#009E73\"  # Position 1 - green (increase)\nNEGATIVE = \"#AE3030\"  # Position 5 - matte red, semantic anchor for loss (decrease)\nTOTAL = \"#4467A3\"  # Position 3 - blue (start/end totals)\n\n# Data - Quarterly financial breakdown from revenue to net income\ncategories = [\n    \"Starting Balance\",\n    \"Product Sales\",\n    \"Service Revenue\",\n    \"Operating Costs\",\n    \"Marketing\",\n    \"Taxes\",\n    \"Net Profit\",\n]\nvalues = [50000, 35000, 18000, -22000, -8000, -12000, 0]\n\n# Calculate waterfall positions\nrunning_total = 0\nbar_starts = []\nbar_ends = []\nbar_colors = []\n\nfor i, (_cat, val) in enumerate(zip(categories, values, strict=True)):\n    if i == 0:  # Starting balance - total bar\n        bar_starts.append(0)\n        bar_ends.append(val)\n        bar_colors.append(\"total\")\n        running_total = val\n    elif i == len(categories) - 1:  # Final total\n        bar_starts.append(0)\n        bar_ends.append(running_total)\n        bar_colors.append(\"total\")\n    else:  # Intermediate changes\n        if val >= 0:\n            bar_starts.append(running_total)\n            bar_ends.append(running_total + val)\n            bar_colors.append(\"positive\")\n        else:\n            bar_starts.append(running_total + val)\n            bar_ends.append(running_total)\n            bar_colors.append(\"negative\")\n        running_total += val\n\n# Update final value for label\nvalues[-1] = running_total\n\n# Create DataFrame with pre-computed rectangle coordinates\nbar_width = 0.35\nx_positions = list(range(len(categories)))\n\ndf = pd.DataFrame(\n    {\n        \"category\": categories,\n        \"value\": values,\n        \"ymin\": bar_starts,\n        \"ymax\": bar_ends,\n        \"color_type\": bar_colors,\n        \"x_pos\": x_positions,\n        \"xmin\": [x - bar_width for x in x_positions],\n        \"xmax\": [x + bar_width for x in x_positions],\n    }\n)\n\n# Calculate label position (center of each bar)\ndf[\"label_y\"] = (df[\"ymin\"] + df[\"ymax\"]) / 2\n\n# Format values for labels\ndf[\"label\"] = df.apply(\n    lambda row: f\"${row['value']:,.0f}\" if row[\"color_type\"] == \"total\" else f\"{row['value']:+,.0f}\", axis=1\n)\ndf[\"tooltip_change\"] = df.apply(\n    lambda row: \"Starting/ending total\" if row[\"color_type\"] == \"total\" else row[\"label\"], axis=1\n)\n\n# Calculate connector line data (connects bars) - connect at the edge that\n# matches the running total after this bar's change: ymin for decreases\n# (post-decrease level), ymax for increases and totals (post-increase level)\nconnectors = []\nfor i in range(len(categories) - 1):\n    y_val = df[\"ymin\"].iloc[i] if df[\"color_type\"].iloc[i] == \"negative\" else df[\"ymax\"].iloc[i]\n    connectors.append({\"x_start\": i + bar_width, \"x_end\": i + 1 - bar_width, \"y\": y_val})\n\nconnector_df = pd.DataFrame(connectors)\n\n# Build waterfall chart\nplot = (\n    ggplot()\n    # Draw bars using geom_rect with pre-computed coordinates; tooltip surfaces\n    # the running-total range on hover, a lets-plot-specific interactive touch\n    + geom_rect(\n        data=df,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"color_type\"),\n        color=INK_SOFT,\n        size=0.8,\n        tooltips=layer_tooltips().line(\"@category\").line(\"Change|@tooltip_change\").line(\"Running total|$@ymax\"),\n    )\n    # Connector lines between bars\n    + geom_segment(\n        data=connector_df,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y\", yend=\"y\"),\n        color=INK_SOFT,\n        size=0.6,\n        linetype=\"dashed\",\n    )\n    # Value labels on bars\n    + geom_text(data=df, mapping=aes(x=\"x_pos\", y=\"label_y\", label=\"label\"), color=INK, size=4, fontface=\"bold\")\n    # Colors: Imprint palette with a profit/loss semantic exception\n    + scale_fill_manual(\n        values={\"positive\": BRAND, \"negative\": NEGATIVE, \"total\": TOTAL},\n        name=\"Change Type\",\n        labels={\"positive\": \"Increase\", \"negative\": \"Decrease\", \"total\": \"Total\"},\n    )\n    # X axis with category labels\n    + scale_x_continuous(breaks=x_positions, labels=categories)\n    # Y axis\n    + scale_y_continuous(format=\"${,.0f}\")\n    # Labels\n    + labs(title=\"waterfall-basic · python · letsplot · anyplot.ai\", x=\"\", y=\"Amount ($)\")\n    # Theme\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid_major_y=element_line(color=INK_SOFT, size=0.4),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=16, color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text_x=element_text(size=10, color=INK_SOFT, angle=30),\n        axis_text_y=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n    )\n    + ggsize(800, 450)\n)\n\n# Save as PNG (scale 4x for 3200x1800)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\n\n# Save as HTML for interactivity\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}