{"spec_id":"area-cumulative-flow","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\narea-cumulative-flow: Cumulative Flow Diagram for Workflow Analytics\nLibrary: plotnine 0.15.8 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove current directory from sys.path to avoid shadowing the plotnine package\n_here = os.path.dirname(os.path.abspath(__file__))\n_cleaned = [p for p in sys.path if os.path.abspath(p) != _here]\nsys.path.clear()\nsys.path.extend(_cleaned)\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_area,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_datetime,\n    scale_y_continuous,\n    theme,\n)\nfrom plotnine.coords import coord_cartesian\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data\nnp.random.seed(42)\nn_days = 90\ndates = pd.date_range(\"2024-01-01\", periods=n_days)\nt = np.arange(n_days)\n\n# Throughput ramps as the team finds its rhythm (~90 items completed over 90 days)\nthroughput = (0.4 + 0.013 * t + np.random.randn(n_days) * 0.2).clip(0.1)\ncum_done = np.round(np.cumsum(throughput)).astype(int)\ncum_done = np.maximum.accumulate(cum_done)\n\n# WIP per stage — Development is the bottleneck (widest band)\ntesting_wip = (7 + 2 * np.sin(t / 25) + np.random.randn(n_days) * 1.0).clip(3).round().astype(int)\ndev_wip = (13 + 4 * np.sin(t / 20) + np.random.randn(n_days) * 1.5).clip(6).round().astype(int)\nanalysis_wip = (5 + np.random.randn(n_days) * 1.0).clip(2).round().astype(int)\nbacklog_wip = (20 - 0.05 * t + np.random.randn(n_days) * 2.0).clip(8).round().astype(int)\n\n# Cumulative boundary lines (monotonically non-decreasing)\ncum_testing = np.maximum.accumulate(cum_done + testing_wip)\ncum_dev = np.maximum.accumulate(cum_testing + dev_wip)\ncum_analysis = np.maximum.accumulate(cum_dev + analysis_wip)\ncum_backlog = np.maximum.accumulate(cum_analysis + backlog_wip)\n\n# Band heights: WIP in each stage (difference between adjacent boundary lines).\n# plotnine stacks geom_area with the LAST factor level at the bottom.\n# Spec: earliest stage (Backlog) on top, latest stage (Done) on bottom.\n# Factor order → Done last so it renders at the bottom of the chart.\nstage_order = [\"Backlog\", \"Analysis\", \"Development\", \"Testing\", \"Done\"]\nbands = [\n    cum_backlog - cum_analysis,  # Backlog WIP   → visual top\n    cum_analysis - cum_dev,  # Analysis WIP\n    cum_dev - cum_testing,  # Development WIP (bottleneck)\n    cum_testing - cum_done,  # Testing WIP\n    cum_done,  # Done (cumulative completed) → visual bottom\n]\n\ndf = pd.concat(\n    [pd.DataFrame({\"date\": dates, \"stage\": stage, \"wip\": wip}) for stage, wip in zip(stage_order, bands, strict=True)],\n    ignore_index=True,\n)\ndf[\"stage\"] = pd.Categorical(df[\"stage\"], categories=stage_order, ordered=True)\n\n# Colors assigned to visual layers (bottom → top): Done, Testing, Development, Analysis, Backlog\nvisual_order = [\"Done\", \"Testing\", \"Development\", \"Analysis\", \"Backlog\"]\nstage_colors = dict(zip(visual_order, IMPRINT, strict=True))\n\n# Bottleneck callout: locate the widest Development window around its WIP peak,\n# not just a single day, so the highlight box below hugs the whole widening span.\npeak_idx = int(np.argmax(dev_wip))\nwindow = 12\nwin_lo = max(0, peak_idx - window)\nwin_hi = min(n_days - 1, peak_idx + window)\nbox_pad = 3\nbox_xmin, box_xmax = dates[win_lo], dates[win_hi]\nbox_ymin = max(0.0, float(np.min(cum_testing[win_lo : win_hi + 1])) - box_pad)\nbox_ymax = float(np.max(cum_dev[win_lo : win_hi + 1])) + box_pad\npeak_wip = int(dev_wip[peak_idx])\nx_annot = dates[peak_idx]\ny_annot = int((cum_testing[peak_idx] + cum_dev[peak_idx]) / 2)\n\n# Explicit y-limit for coord_cartesian below: a touch of headroom above the\n# stack top, computed once instead of relying on scale expansion alone.\ny_top = float(cum_backlog.max()) * 1.06\n\n# Title — canonical format, well under the 67-char baseline so no fontsize scaling needed\ntitle = \"area-cumulative-flow · python · plotnine · anyplot.ai\"\n\n# Plot\nanyplot_theme = 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_grid_major_x=element_blank(),\n    panel_grid_major_y=element_line(color=INK_SOFT, size=0.3, alpha=0.20),\n    panel_grid_minor=element_blank(),\n    panel_border=element_blank(),\n    axis_title=element_text(color=INK, size=10),\n    axis_text=element_text(color=INK_SOFT, size=8),\n    # L-shaped frame: bottom x-axis line and left y-axis line only\n    axis_line_x=element_line(color=INK_SOFT),\n    axis_line_y=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=12, face=\"bold\"),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=8),\n    # Slightly larger legend title for visual hierarchy within the legend\n    legend_title=element_text(color=INK, size=9, face=\"bold\"),\n    legend_position=\"right\",\n    # Tight breathing room between plot area and legend — avoids the excess\n    # right-side whitespace flagged in the previous review\n    legend_box_spacing=0.02,\n    plot_margin=0.02,\n)\n\nplot = (\n    ggplot(df, aes(x=\"date\", y=\"wip\", fill=\"stage\"))\n    # outline_type=\"full\" closes each band's own polygon border (not just the\n    # upper edge), a deliberate refinement over the geom_area default\n    + geom_area(position=\"stack\", alpha=0.88, color=PAGE_BG, size=0.3, outline_type=\"full\")\n    + scale_fill_manual(values=stage_colors)\n    # date axis hugs the data range — no leading/trailing padding\n    + scale_x_datetime(date_labels=\"%b %d\", date_breaks=\"2 weeks\", expand=(0, 0))\n    + scale_y_continuous(expand=(0, 0))\n    # Fine-tuned view window (vs. relying on scale expansion alone) so the\n    # bottleneck highlight box below always has clean headroom above it\n    + coord_cartesian(ylim=(0, y_top), expand=False)\n    + labs(x=\"Date\", y=\"Cumulative Items\", fill=\"Stage\", title=title)\n    # Dashed callout box makes the widening Development band unmissable at a\n    # glance, instead of relying solely on the text label to carry the insight\n    + annotate(\n        \"rect\",\n        xmin=box_xmin,\n        xmax=box_xmax,\n        ymin=box_ymin,\n        ymax=box_ymax,\n        fill=None,\n        color=INK,\n        linetype=\"dashed\",\n        size=0.7,\n        alpha=0.85,\n    )\n    + annotate(\n        \"text\",\n        x=x_annot,\n        y=y_annot,\n        label=f\"Development bottleneck\\nPeak WIP: {peak_wip}\",\n        color=\"#FFFFFF\",\n        size=3.5,\n        ha=\"left\",\n        va=\"center\",\n        fontweight=\"bold\",\n    )\n    + anyplot_theme\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}