{"spec_id":"waterfall-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nwaterfall-basic: Basic Waterfall Chart\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 92/100 | Created: 2026-08-04\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom plotnine import (\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    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    theme,\n    theme_minimal,\n)\n\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 categorical palette - positive/negative reassigned via the finance\n# semantic exception (profit/gain -> green, loss/down -> red); totals use the\n# theme-adaptive neutral anchor so they read as part of the chart's structure.\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data - quarterly financial breakdown\ncategories = [\"Starting Balance\", \"Q1 Sales\", \"Operating Costs\", \"R&D Investment\", \"Tax Payment\", \"Ending Balance\"]\nvalues = [1000, 450, -280, -120, -150, 900]\n\ndf = pd.DataFrame({\"category\": categories, \"value\": values})\ndf[\"category\"] = pd.Categorical(df[\"category\"], categories=categories, ordered=True)\n\n# Calculate cumulative waterfall positions\nrunning_total = 0\nstarts = []\nends = []\nbar_types = []\n\nfor i, val in enumerate(values):\n    if i == 0:\n        starts.append(0)\n        ends.append(val)\n        bar_types.append(\"total\")\n        running_total = val\n    elif i == len(values) - 1:\n        starts.append(0)\n        ends.append(running_total)\n        bar_types.append(\"total\")\n    else:\n        if val >= 0:\n            starts.append(running_total)\n            ends.append(running_total + val)\n            bar_types.append(\"positive\")\n        else:\n            starts.append(running_total + val)\n            ends.append(running_total)\n            bar_types.append(\"negative\")\n        running_total += val\n\ndf[\"start\"] = starts\ndf[\"end\"] = ends\ndf[\"bar_type\"] = pd.Categorical(bar_types, categories=[\"positive\", \"negative\", \"total\"], ordered=True)\ndf[\"x_pos\"] = range(len(categories))\n\n# Value labels: signed deltas for changes, plain totals for start/end bars\nlabel_offset = 45\ndf[\"label\"] = [f\"{v:+,}\" if t != \"total\" else f\"{v:,}\" for v, t in zip(values, bar_types, strict=True)]\ndf[\"label_y\"] = [e + label_offset if e >= s else e - label_offset for s, e in zip(starts, ends, strict=True)]\n\n# Connector lines bridging each bar's running total to the next bar's start.\n# The bridging y-value is always the post-change running total after step i:\n# for a decrease bar that value lives in \"start\", not \"end\".\nconnectors = []\nfor i in range(len(df) - 1):\n    bridge_y = df.iloc[i][\"start\"] if bar_types[i] == \"negative\" else df.iloc[i][\"end\"]\n    connectors.append({\"x_start\": df.iloc[i][\"x_pos\"] + 0.35, \"x_end\": df.iloc[i + 1][\"x_pos\"] - 0.35, \"y\": bridge_y})\nconnector_df = pd.DataFrame(connectors) if connectors else pd.DataFrame()\n\ncolors = {\"total\": INK, \"positive\": IMPRINT_PALETTE[0], \"negative\": IMPRINT_PALETTE[4]}\n\n# Title, scaled to the mandated ~67-char baseline\ntitle = \"Quarterly Financial Summary · waterfall-basic · python · plotnine · anyplot.ai\"\ntitle_fontsize = round(12 * min(1.0, 67 / len(title)))\n\nplot = ggplot() + geom_rect(\n    df,\n    aes(xmin=\"x_pos - 0.35\", xmax=\"x_pos + 0.35\", ymin=\"start\", ymax=\"end\", fill=\"bar_type\"),\n    color=PAGE_BG,\n    size=0.6,\n)\n\nif not connector_df.empty:\n    plot = plot + geom_segment(\n        connector_df,\n        aes(x=\"x_start\", xend=\"x_end\", y=\"y\", yend=\"y\"),\n        color=INK_SOFT,\n        size=0.6,\n        alpha=0.5,\n        linetype=\"dashed\",\n    )\n\nplot = (\n    plot\n    + geom_text(df, aes(x=\"x_pos\", y=\"label_y\", label=\"label\"), size=7, color=INK)\n    + scale_fill_manual(\n        values=colors, name=\"Change Type\", labels={\"total\": \"Total\", \"positive\": \"Increase\", \"negative\": \"Decrease\"}\n    )\n    + scale_x_continuous(breaks=list(range(len(categories))), labels=categories, limits=(-0.6, len(categories) - 0.4))\n    + labs(x=\"\", y=\"Amount ($1K)\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_border=element_blank(),\n        panel_grid_major_x=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.4, alpha=0.15),\n        panel_grid_minor=element_blank(),\n        axis_line_x=element_line(color=INK_SOFT, size=0.6),\n        axis_line_y=element_line(color=INK_SOFT, size=0.6),\n        axis_ticks_major=element_blank(),\n        axis_title=element_text(size=10, color=INK),\n        axis_text_x=element_text(size=8, color=INK_SOFT, angle=45, ha=\"right\"),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=title_fontsize, color=INK, fontweight=\"bold\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.4),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_position=\"top\",\n        legend_key=element_blank(),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}