{ "cells": [ { "cell_type": "markdown", "id": "cell-0", "metadata": {}, "source": [ "# Genesys CX Cloud TEI — Business Case\n", "\n", "Reproduction of Forrester's *The Total Economic Impact™ Of CX Cloud — Cost\n", "Savings And Business Benefits Enabled By Genesys And Salesforce* (December\n", "2025, commissioned by Genesys and Salesforce) — and a live personalization\n", "of it. The published composite organization is the **verbatim anchor**\n", "(never edited); the verification gate proves this notebook reproduces the\n", "published **$10.8M NPV · 266% ROI**; the client drivers then rescale the\n", "composite to your organization.\n", "\n", "One line deserves special attention: the published study models **$0 of\n", "Genesys AI Experience token consumption**, even though three of its four\n", "benefits depend on AI capabilities that Genesys bills via tokens. That line\n", "is anchored at $0 (so the reproduction matches the PDF) and exposed as a\n", "direct 🔴 input — price it from the Genesys quote and watch the case move.\n", "\n", "**This notebook is the deliverable** — served interactively with Mercury,\n", "exported via nbconvert as the report source (Mercury Notebook Pattern,\n", "Variant 4).\n", "\n", "| Layer | What it is | Confidence |\n", "|---|---|---|\n", "| Verbatim anchor | Forrester's composite tables, unedited | 🟢 published |\n", "| Client overlay | first-order linear rescale by your drivers | 🟡 estimated |\n", "| AI-token line | negotiated annual quote figure, direct input | 🔴 unknown until quoted |\n", "| Scenario | adoption × risk stress | 🟡 estimated |\n", "\n", "Confidence legend: 🟢 confirmed/published · 🟡 estimated (stated assumption) · 🔴 unknown (flagged)\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "cell-1", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:18.686288Z", "iopub.status.busy": "2026-07-09T19:15:18.686041Z", "iopub.status.idle": "2026-07-09T19:15:19.083373Z", "shell.execute_reply": "2026-07-09T19:15:19.082724Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "teicalc loaded — window 2026–2028 · published NPV $10.8M · ROI 266% · AI-token line anchored at $0\n" ] } ], "source": [ "# ── Setup ──────────────────────────────────────────────────────────\n", "import sys, pathlib\n", "_ROOT = pathlib.Path.cwd()\n", "if not (_ROOT / \"teicalc\").exists(): # notebook lives in notebooks/\n", " _ROOT = _ROOT.parent\n", "sys.path.insert(0, str(_ROOT))\n", "\n", "import pandas as pd\n", "import plotly.graph_objects as go\n", "\n", "import mercury as mr\n", "\n", "# Single source of truth — all math lives in the study package; only\n", "# presentation (and Mercury input widgets) lives here.\n", "from teicalc import (\n", " ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM, PUBLISHED,\n", " YEARS, X_LABELS,\n", " BENEFIT_DRIVERS, COST_DRIVERS, COMPOSITE, ClientDrivers,\n", " SCENARIOS, apply_scenario, compute_summary,\n", " money, html_money, overlay_rows,\n", ")\n", "from teicalc.staging import backstage\n", "\n", "pd.options.display.float_format = \"{:,.0f}\".format\n", "\n", "# ── Chart chrome (dataviz reference palette, light surface) ─────────\n", "INK, INK2, MUTED = \"#0b0b0b\", \"#52514e\", \"#898781\"\n", "SURFACE, GRID, BASELINE = \"#fcfcfb\", \"#e1e0d9\", \"#c3c2b7\"\n", "CUMULATIVE, CONTEXT = \"#52514e\", \"#c3c2b7\" # neutral line; de-emphasized context series\n", "FONT_STACK = 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n", "\n", "# Fixed row colors — color follows the entity across every figure.\n", "BENEFIT_COLOR = {\n", " \"legacy_retirement\": \"#2a78d6\", # blue\n", " \"self_service_savings\": \"#1baf7a\", # aqua\n", " \"agent_efficiency\": \"#4a3aa7\", # violet\n", " \"agent_assist_sales\": \"#eda100\", # yellow\n", "}\n", "COST_COLOR = {\n", " \"cx_cloud_licenses\": \"#2a78d6\", # blue\n", " \"implementation\": \"#eda100\", # yellow\n", " \"ongoing_management\": \"#4a3aa7\", # violet\n", " \"genesys_ai_tokens\": \"#e34948\", # red — the flagged $0 line\n", "}\n", "BEN_TOTAL, COST_TOTAL, NPV_COLOR = \"#1baf7a\", \"#e34948\", \"#2a78d6\"\n", "\n", "\n", "def tei_layout(fig, title, subtitle=None, height=460):\n", " t = f\"{title}\"\n", " if subtitle:\n", " t += f\"
{subtitle}\"\n", " fig.update_layout(\n", " title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor=\"left\"),\n", " paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n", " font=dict(family=FONT_STACK, size=12, color=INK2),\n", " legend=dict(orientation=\"h\", yanchor=\"top\", y=-0.10, x=0,\n", " font=dict(size=11, color=INK2)),\n", " xaxis=dict(type=\"category\", showgrid=False, linecolor=BASELINE,\n", " tickfont=dict(color=MUTED)),\n", " yaxis=dict(gridcolor=GRID, zerolinecolor=BASELINE, zerolinewidth=1.5,\n", " tickformat=\"$~s\", tickfont=dict(color=MUTED)),\n", " hovermode=\"x unified\", bargap=0.45, height=height,\n", " margin=dict(t=70, r=30, b=80, l=70),\n", " )\n", " return fig\n", "\n", "\n", "def bar(x, y, name, color):\n", " return go.Bar(x=x, y=y, name=name,\n", " marker=dict(color=color, line=dict(width=2, color=SURFACE)),\n", " hovertemplate=\"%{fullData.name}: %{y:$,.0f}\")\n", "\n", "\n", "def cum_line(x, y, name, color=CUMULATIVE, dash=None):\n", " return go.Scatter(x=x, y=y, name=name, mode=\"lines+markers\",\n", " line=dict(color=color, width=2, dash=dash),\n", " marker=dict(size=8, line=dict(width=2, color=SURFACE)),\n", " hovertemplate=\"%{fullData.name}: %{y:$,.0f}\")\n", "\n", "\n", "backstage(f\"teicalc loaded — window {YEARS[0]}–{YEARS[-1]} · published \"\n", " f\"NPV {money(PUBLISHED['npv'])} · ROI {PUBLISHED['roi_pct']}% · \"\n", " f\"AI-token line anchored at $0\")\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "cell-2", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.085680Z", "iopub.status.busy": "2026-07-09T19:15:19.085417Z", "iopub.status.idle": "2026-07-09T19:15:19.097643Z", "shell.execute_reply": "2026-07-09T19:15:19.096879Z" } }, "outputs": [ { "data": { "application/mercury+json": { "model_id": "c7bf126b79ee49b2bb7140ec4fb10b96", "position": "sidebar", "widget": "MarkdownWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "c7bf126b79ee49b2bb7140ec4fb10b96", "version_major": 2, "version_minor": 0 }, "text/plain": [ "MarkdownWidget(value='
{label}'\n", " for n, label in _TOC)\n", "_toc = mr.Markdown(\n", " text=(f'Jump to section'\n", " f'
    {_items}
'),\n", " position=\"sidebar\")\n" ] }, { "cell_type": "markdown", "id": "cell-3", "metadata": {}, "source": [ "\n", "## 1 · The Forrester composite (verbatim anchor 🟢)\n", "\n", "Forrester's composite organization: a **global supply company with $2.5B\n", "revenue and 10,000 employees**, running **600 CX agents** (400 concurrent\n", "licenses) who handle **80,000 weekly interactions** averaging 12 minutes.\n", "\n", "TEI methodology, carried verbatim into the engine: benefits are\n", "risk-adjusted **down** (×(1−rf)), costs **up** (×(1+rf)); the initial\n", "investment sits at time 0 undiscounted; year flows discount at end-of-year\n", "(10%, 3 years). Two source footnotes preserved in the anchor: the p.14\n", "Total Costs table's $1,304,600 implementation figure is a typo (the detail\n", "table and cash-flow analysis give $1,309,000), and the study models **$0\n", "AI-token consumption** (§4).\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "cell-4", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.100446Z", "iopub.status.busy": "2026-07-09T19:15:19.100167Z", "iopub.status.idle": "2026-07-09T19:15:19.119649Z", "shell.execute_reply": "2026-07-09T19:15:19.118932Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Value 🟢
Assumption
annual revenue$2,500,000,000
employees10,000
agents fte600
concurrent licenses400
weekly interactions80,000
interaction minutes12 min
self service rate before15%
self service rate after25%
mttr saved minutes2 min
agent hourly rate$28/hr
agent annual salary$58,240
revenue impacted$500,000,000
revenue lift1.5%
gross margin8%
discount rate10%
analysis years3 years
\n", "
" ], "text/plain": [ " Value 🟢\n", "Assumption \n", "annual revenue $2,500,000,000\n", "employees 10,000\n", "agents fte 600\n", "concurrent licenses 400\n", "weekly interactions 80,000\n", "interaction minutes 12 min\n", "self service rate before 15%\n", "self service rate after 25%\n", "mttr saved minutes 2 min\n", "agent hourly rate $28/hr\n", "agent annual salary $58,240\n", "revenue impacted $500,000,000\n", "revenue lift 1.5%\n", "gross margin 8%\n", "discount rate 10%\n", "analysis years 3 years" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Published 🟢
Metric
Benefits PV (risk-adjusted)$14,840,638
Costs PV (risk-adjusted)$4,057,170
NPV$10,783,468
ROI266%
Paybacknot headlined (engine: 3.3 months)
\n", "
" ], "text/plain": [ " Published 🟢\n", "Metric \n", "Benefits PV (risk-adjusted) $14,840,638\n", "Costs PV (risk-adjusted) $4,057,170\n", "NPV $10,783,468\n", "ROI 266%\n", "Payback not headlined (engine: 3.3 months)" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "engine reproduction Δ vs PDF: benefits -1.19 · costs +0.40 · npv -1.58\n" ] } ], "source": [ "# ── Composite assumptions & published financial summary (🟢) ─────────\n", "composite = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM,\n", " PUBLISHED[\"discount_rate\"])\n", "\n", "_fmt = {\n", " \"annual_revenue\": \"${:,}\", \"employees\": \"{:,}\", \"agents_fte\": \"{:,}\",\n", " \"concurrent_licenses\": \"{:,}\", \"weekly_interactions\": \"{:,}\",\n", " \"interaction_minutes\": \"{} min\", \"self_service_rate_before\": \"{:.0%}\",\n", " \"self_service_rate_after\": \"{:.0%}\", \"mttr_saved_minutes\": \"{} min\",\n", " \"agent_hourly_rate\": \"${}/hr\", \"agent_annual_salary\": \"${:,}\",\n", " \"revenue_impacted\": \"${:,}\", \"revenue_lift\": \"{:.1%}\",\n", " \"gross_margin\": \"{:.0%}\", \"discount_rate\": \"{:.0%}\",\n", " \"analysis_years\": \"{} years\",\n", "}\n", "assumptions_df = pd.DataFrame(\n", " [{\"Assumption\": k.replace(\"_\", \" \"), \"Value 🟢\": _fmt[k].format(v)}\n", " for k, v in ASSUMPTIONS.items()])\n", "display(assumptions_df.set_index(\"Assumption\"))\n", "\n", "published_df = pd.DataFrame([\n", " {\"Metric\": \"Benefits PV (risk-adjusted)\", \"Published 🟢\": f\"${PUBLISHED['benefits_pv']:,}\"},\n", " {\"Metric\": \"Costs PV (risk-adjusted)\", \"Published 🟢\": f\"${PUBLISHED['costs_pv']:,}\"},\n", " {\"Metric\": \"NPV\", \"Published 🟢\": f\"${PUBLISHED['npv']:,}\"},\n", " {\"Metric\": \"ROI\", \"Published 🟢\": f\"{PUBLISHED['roi_pct']}%\"},\n", " {\"Metric\": \"Payback\", \"Published 🟢\": \"not headlined (engine: 3.3 months)\"},\n", "])\n", "display(published_df.set_index(\"Metric\"))\n", "\n", "backstage(f\"engine reproduction Δ vs PDF: \"\n", " f\"benefits {composite['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n", " f\"costs {composite['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n", " f\"npv {composite['npv'] - PUBLISHED['npv']:+,.2f}\")\n" ] }, { "cell_type": "markdown", "id": "cell-5", "metadata": {}, "source": [ "\n", "## 2 · Client inputs (overlay 🟡)\n", "\n", "The overlay is a **first-order linear rescale** of Forrester's composite —\n", "it answers *\"what does the composite look like at your size?\"*, not *\"what\n", "is your TEI?\"*. Each published row scales with the driver that dominates\n", "its derivation in the PDF; project-based costs stay fixed. This composite's\n", "trajectory is flat (Y2 = Y3), so there is no growth re-base.\n", "\n", "| Published row | Scales with | Confidence |\n", "|---|---|---|\n", "| Retirement of legacy systems | agents | 🟡 seat-scoped platform costs |\n", "| Self-service savings | interactions | 🟡 |\n", "| CX agent efficiency gains | interactions | 🟡 |\n", "| Incremental sales from agent assist | revenue | 🟡 |\n", "| CX Cloud licenses | agents | 🟡 |\n", "| Implementation & deployment | fixed | 🟡 project-based |\n", "| Ongoing management | fixed | 🟡 |\n", "| **Genesys AI Experience tokens** | **direct $/yr input** | 🔴 $0 until quoted |\n", "\n", "*Change any input in the sidebar — every table, figure and KPI below\n", "recomputes. The assertions in §7 hold at any setting.*\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "cell-6", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.121610Z", "iopub.status.busy": "2026-07-09T19:15:19.121442Z", "iopub.status.idle": "2026-07-09T19:15:19.139328Z", "shell.execute_reply": "2026-07-09T19:15:19.138672Z" } }, "outputs": [ { "data": { "application/mercury+json": { "model_id": "b2751bf42368472ebf73f08317e9cde9", "position": "sidebar", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "b2751bf42368472ebf73f08317e9cde9", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/mercury+json": { "model_id": "2247afa5bfb04ad8905333e3f64caf67", "position": "sidebar", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "2247afa5bfb04ad8905333e3f64caf67", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/mercury+json": { "model_id": "55da27f443ca4021895933ad5d96f23b", "position": "sidebar", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "55da27f443ca4021895933ad5d96f23b", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/mercury+json": { "model_id": "9173bebf5a34432e8e4819f906db4add", "position": "sidebar", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "9173bebf5a34432e8e4819f906db4add", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/mercury+json": { "model_id": "76c10bd18e9c466daec54721bc92b435", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "76c10bd18e9c466daec54721bc92b435", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/mercury+json": { "model_id": "ce038c3719d240a1a635401ac4a6208e", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { "model_id": "ce038c3719d240a1a635401ac4a6208e", "version_major": 2, "version_minor": 1 }, "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ── Client drivers (Mercury sidebar — widgets only, NO other output) ─\n", "# NB: Mercury re-executes only cells BELOW a changed widget's cell, so\n", "# this cell constructs widgets ONLY — .value is read downstream.\n", "_agents_w = mr.NumberInput(label=\"CX agents (FTE) — composite 600\",\n", " value=600, min=25, max=20_000, step=25)\n", "_interactions_w = mr.NumberInput(label=\"Weekly interactions — composite 80,000\",\n", " value=80_000, min=1_000, max=2_000_000,\n", " step=5_000)\n", "_revenue_w = mr.NumberInput(label=\"Annual revenue ($) — composite 2.5B\",\n", " value=2_500_000_000, min=100_000_000,\n", " max=100_000_000_000, step=100_000_000)\n", "_tokens_w = mr.NumberInput(label=\"Genesys AI tokens ($/yr) — study modeled $0\",\n", " value=0, min=0, max=5_000_000, step=25_000)\n", "_discount_w = mr.Select(label=\"Discount rate\", value=\"10% (Forrester)\",\n", " choices=[\"8%\", \"10% (Forrester)\", \"12%\"])\n", "_scenario_w = mr.Select(label=\"Scenario\", value=\"moderate\",\n", " choices=[\"conservative\", \"moderate\", \"aggressive\"])\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "cell-7", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.141205Z", "iopub.status.busy": "2026-07-09T19:15:19.141026Z", "iopub.status.idle": "2026-07-09T19:15:19.147273Z", "shell.execute_reply": "2026-07-09T19:15:19.146674Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Client frame: 600 agents · 80,000 weekly interactions · moderate scenario → NPV $10.8M · ROI 266% · payback 3.3 months (~Apr 2026)\n", "scale factors — agents 1.00× · interactions 1.00× · revenue 1.00×\n" ] } ], "source": [ "# ── Client overlay state (re-runs on any change to the widgets above) ─\n", "AGENTS_FTE = int(_agents_w.value)\n", "WEEKLY_INTERACTIONS = int(_interactions_w.value)\n", "ANNUAL_REVENUE = float(_revenue_w.value)\n", "AI_TOKENS_ANNUAL = float(_tokens_w.value)\n", "DISCOUNT_RATE = {\"8%\": 0.08, \"10% (Forrester)\": 0.10,\n", " \"12%\": 0.12}[str(_discount_w.value)]\n", "SCENARIO = str(_scenario_w.value)\n", "\n", "DRIVERS = ClientDrivers(agents_fte=AGENTS_FTE,\n", " weekly_interactions=WEEKLY_INTERACTIONS,\n", " annual_revenue=ANNUAL_REVENUE,\n", " ai_tokens_annual=AI_TOKENS_ANNUAL,\n", " discount_rate=DISCOUNT_RATE)\n", "overlay_benefits, overlay_costs = overlay_rows(DRIVERS)\n", "client_benefits = apply_scenario(overlay_benefits, SCENARIO)\n", "client_costs = apply_scenario(overlay_costs, SCENARIO)\n", "client = compute_summary(client_benefits, client_costs, DISCOUNT_RATE)\n", "\n", "_at_default = (DRIVERS == COMPOSITE and SCENARIO == \"moderate\")\n", "\n", "_tok = f\" · AI tokens {money(AI_TOKENS_ANNUAL)}/yr\" if AI_TOKENS_ANNUAL else \"\"\n", "print(f\"Client frame: {AGENTS_FTE:,} agents · {WEEKLY_INTERACTIONS:,} weekly \"\n", " f\"interactions{_tok} · {SCENARIO} scenario → NPV {money(client['npv'])} · \"\n", " f\"ROI {client['roi_pct']:.0f}% · payback {client['payback_label']}\")\n", "backstage(f\"scale factors — agents {AGENTS_FTE / ASSUMPTIONS['agents_fte']:.2f}× · \"\n", " f\"interactions {WEEKLY_INTERACTIONS / ASSUMPTIONS['weekly_interactions']:.2f}× · \"\n", " f\"revenue {ANNUAL_REVENUE / ASSUMPTIONS['annual_revenue']:.2f}×\")\n" ] }, { "cell_type": "markdown", "id": "cell-8", "metadata": {}, "source": [ "\n", "## 3 · Benefits\n", "\n", "Four benefit streams (Forrester refs A–D), risk-adjusted down 5–15%.\n", "Agent efficiency and self-service deflection carry ~three-quarters of the\n", "total; legacy retirement ramps as the old telephony sunsets mid-year-1;\n", "the profile is otherwise flat.\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "cell-9", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.149355Z", "iopub.status.busy": "2026-07-09T19:15:19.149198Z", "iopub.status.idle": "2026-07-09T19:15:19.161479Z", "shell.execute_reply": "2026-07-09T19:15:19.160698Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
DriverRisk adj2026202720283-yr RAPV
Benefit
Retirement of legacy systems with CX Cloud adoptionagents-5%646,000883,500883,5002,413,0001,981,225
Cost savings from reallocated workers and avoided seasonal hires with increased customer self-serviceinteractions-15%1,980,1601,980,1601,980,1605,940,4804,924,365
CX agent efficiency gainsinteractions-10%2,620,8002,620,8002,620,8007,862,4006,517,542
Incremental sales from agent assist capabilitiesrevenue-5%570,000570,000570,0001,710,0001,417,506
TOTAL5,816,9606,054,4606,054,46017,925,88014,840,637
\n", "
" ], "text/plain": [ " Driver Risk adj \\\n", "Benefit \n", "Retirement of legacy systems with CX Cloud adop... agents -5% \n", "Cost savings from reallocated workers and avoid... interactions -15% \n", "CX agent efficiency gains interactions -10% \n", "Incremental sales from agent assist capabilities revenue -5% \n", "TOTAL \n", "\n", " 2026 2027 \\\n", "Benefit \n", "Retirement of legacy systems with CX Cloud adop... 646,000 883,500 \n", "Cost savings from reallocated workers and avoid... 1,980,160 1,980,160 \n", "CX agent efficiency gains 2,620,800 2,620,800 \n", "Incremental sales from agent assist capabilities 570,000 570,000 \n", "TOTAL 5,816,960 6,054,460 \n", "\n", " 2028 3-yr RA \\\n", "Benefit \n", "Retirement of legacy systems with CX Cloud adop... 883,500 2,413,000 \n", "Cost savings from reallocated workers and avoid... 1,980,160 5,940,480 \n", "CX agent efficiency gains 2,620,800 7,862,400 \n", "Incremental sales from agent assist capabilities 570,000 1,710,000 \n", "TOTAL 6,054,460 17,925,880 \n", "\n", " PV \n", "Benefit \n", "Retirement of legacy systems with CX Cloud adop... 1,981,225 \n", "Cost savings from reallocated workers and avoid... 4,924,365 \n", "CX agent efficiency gains 6,517,542 \n", "Incremental sales from agent assist capabilities 1,417,506 \n", "TOTAL 14,840,637 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ── Benefits table — client overlay, risk-adjusted ───────────────────\n", "_ben_rows = client[\"rows\"][\"benefits\"]\n", "benefits_df = pd.DataFrame([{\n", " \"Benefit\": r[\"label\"],\n", " \"Driver\": BENEFIT_DRIVERS[r[\"field_key\"]],\n", " \"Risk adj\": f\"-{r['risk_adjustment']:.0%}\",\n", " **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n", " \"3-yr RA\": r[\"three_yr_ra\"],\n", " \"PV\": r[\"pv\"],\n", "} for r in _ben_rows]).set_index(\"Benefit\")\n", "benefits_df.loc[\"TOTAL\"] = [\"\", \"\"] + [client[\"benefits_by_year\"][y] for y in YEARS] \\\n", " + [sum(r[\"three_yr_ra\"] for r in _ben_rows), client[\"benefits_pv\"]]\n", "if not _at_default: # published composite beside the overlay for reference\n", " benefits_df[\"Composite PV 🟢\"] = \\\n", " [r[\"pv\"] for r in composite[\"rows\"][\"benefits\"]] + [composite[\"benefits_pv\"]]\n", "display(benefits_df)\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "cell-10", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:19.163518Z", "iopub.status.busy": "2026-07-09T19:15:19.163257Z", "iopub.status.idle": "2026-07-09T19:15:20.430977Z", "shell.execute_reply": "2026-07-09T19:15:20.429934Z" } }, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#2a78d6", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Retirement of legacy systems with CX Cloud adoption", "type": "bar", "x": [ "2026", "2027", "2028" ], "y": [ 646000.0, 883500.0, 883500.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#1baf7a", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service", "type": "bar", "x": [ "2026", "2027", "2028" ], "y": [ 1980160.0, 1980160.0, 1980160.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#4a3aa7", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "CX agent efficiency gains", "type": "bar", "x": [ "2026", "2027", "2028" ], "y": [ 2620800.0, 2620800.0, 2620800.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#eda100", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Incremental sales from agent assist capabilities", "type": "bar", "x": [ "2026", "2027", "2028" ], "y": [ 570000.0, 570000.0, 570000.0 ] } ], "layout": { "bargap": 0.45, "barmode": "stack", "font": { "color": "#52514e", "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif", "size": 12 }, "height": 480, "hovermode": "x unified", "legend": { "font": { "color": "#52514e", "size": 11 }, "orientation": "h", "x": 0, "y": -0.1, "yanchor": "top" }, "margin": { "b": 80, "l": 70, "r": 30, "t": 70 }, "paper_bgcolor": "#fcfcfb", "plot_bgcolor": "#fcfcfb", "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "heatmap" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermap": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermap" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "sequentialminus": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "title": { "font": { "color": "#0b0b0b", "size": 16 }, "text": "Benefits by year (risk-adjusted)
stacked by benefit stream — client overlay", "x": 0.02, "xanchor": "left" }, "xaxis": { "linecolor": "#c3c2b7", "showgrid": false, "tickfont": { "color": "#898781" }, "type": "category" }, "yaxis": { "gridcolor": "#e1e0d9", "tickfont": { "color": "#898781" }, "tickformat": "$~s", "zerolinecolor": "#c3c2b7", "zerolinewidth": 1.5 } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = go.Figure()\n", "_x = [str(y) for y in YEARS]\n", "for r in client[\"rows\"][\"benefits\"]:\n", " fig.add_trace(bar(_x, [r[\"ra_by_year\"][y] for y in YEARS],\n", " r[\"label\"], BENEFIT_COLOR[r[\"field_key\"]]))\n", "if not _at_default: # composite yearly total as de-emphasized context\n", " fig.add_trace(cum_line(_x, [composite[\"benefits_by_year\"][y] for y in YEARS],\n", " \"Forrester composite total\", CONTEXT, dash=\"dot\"))\n", "fig.update_layout(barmode=\"stack\")\n", "tei_layout(fig, \"Benefits by year (risk-adjusted)\",\n", " subtitle=\"stacked by benefit stream — client overlay\", height=480)\n", "fig.show()\n" ] }, { "cell_type": "markdown", "id": "cell-11", "metadata": {}, "source": [ "\n", "## 4 · Costs — and the $0 AI line (🔴)\n", "\n", "Three published cost lines, risk-adjusted **up** 5–10%: seat licenses,\n", "a 10-week implementation (the only time-0 outlay: $1.19M nominal → $1.31M\n", "risk-adjusted), and a small ops team.\n", "\n", "The fourth line is the one the study *doesn't* price: **Genesys AI\n", "Experience token consumption**. Forrester models it at $0, yet the\n", "self-service uplift (B), agent efficiency (C), and agent assist (D)\n", "benefits all run on AI capabilities that Genesys bills via tokens. The\n", "anchor keeps the $0 verbatim so the reproduction matches the PDF; the\n", "sidebar's token input prices it for a real client from the Genesys quote.\n" ] }, { "cell_type": "code", "execution_count": 8, "id": "cell-12", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.435367Z", "iopub.status.busy": "2026-07-09T19:15:20.435032Z", "iopub.status.idle": "2026-07-09T19:15:20.449985Z", "shell.execute_reply": "2026-07-09T19:15:20.449281Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
DriverRisk adjInitial202620272028PV
Cost
CX Cloud solution costs (licenses)agents+5%0882,000882,000882,0002,193,403
Implementation and deployment costfixed+10%1,309,0000001,309,000
Ongoing management costsfixed+10%0223,080223,080223,080554,767
Genesys AI Experience token consumptionai_tokens+0%00000
TOTAL1,309,0001,105,0801,105,0801,105,0804,057,170
\n", "
" ], "text/plain": [ " Driver Risk adj Initial \\\n", "Cost \n", "CX Cloud solution costs (licenses) agents +5% 0 \n", "Implementation and deployment cost fixed +10% 1,309,000 \n", "Ongoing management costs fixed +10% 0 \n", "Genesys AI Experience token consumption ai_tokens +0% 0 \n", "TOTAL 1,309,000 \n", "\n", " 2026 2027 2028 \\\n", "Cost \n", "CX Cloud solution costs (licenses) 882,000 882,000 882,000 \n", "Implementation and deployment cost 0 0 0 \n", "Ongoing management costs 223,080 223,080 223,080 \n", "Genesys AI Experience token consumption 0 0 0 \n", "TOTAL 1,105,080 1,105,080 1,105,080 \n", "\n", " PV \n", "Cost \n", "CX Cloud solution costs (licenses) 2,193,403 \n", "Implementation and deployment cost 1,309,000 \n", "Ongoing management costs 554,767 \n", "Genesys AI Experience token consumption 0 \n", "TOTAL 4,057,170 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ── Costs table — client overlay, risk-adjusted ──────────────────────\n", "_cost_rows = client[\"rows\"][\"costs\"]\n", "costs_df = pd.DataFrame([{\n", " \"Cost\": r[\"label\"],\n", " \"Driver\": COST_DRIVERS[r[\"field_key\"]],\n", " \"Risk adj\": f\"+{r['risk_adjustment']:.0%}\",\n", " \"Initial\": r[\"initial_ra\"],\n", " **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n", " \"PV\": r[\"pv\"],\n", "} for r in _cost_rows]).set_index(\"Cost\")\n", "costs_df.loc[\"TOTAL\"] = [\"\", \"\", client[\"initial_costs\"]] \\\n", " + [client[\"costs_by_year\"][y] for y in YEARS] + [client[\"costs_pv\"]]\n", "if not _at_default:\n", " costs_df[\"Composite PV 🟢\"] = \\\n", " [r[\"pv\"] for r in composite[\"rows\"][\"costs\"]] + [composite[\"costs_pv\"]]\n", "display(costs_df)\n" ] }, { "cell_type": "code", "execution_count": 9, "id": "cell-13", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.458332Z", "iopub.status.busy": "2026-07-09T19:15:20.458028Z", "iopub.status.idle": "2026-07-09T19:15:20.482508Z", "shell.execute_reply": "2026-07-09T19:15:20.481680Z" } }, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#2a78d6", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "CX Cloud solution costs (licenses)", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ 0.0, 882000.0, 882000.0, 882000.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#eda100", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Implementation and deployment cost", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ 1309000.0, 0.0, 0.0, 0.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#4a3aa7", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Ongoing management costs", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ 0.0, 223080.00000000003, 223080.00000000003, 223080.00000000003 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#e34948", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Genesys AI Experience token consumption", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ 0.0, 0.0, 0.0, 0.0 ] } ], "layout": { "bargap": 0.45, "barmode": "stack", "font": { "color": "#52514e", "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif", "size": 12 }, "height": 460, "hovermode": "x unified", "legend": { "font": { "color": "#52514e", "size": 11 }, "orientation": "h", "x": 0, "y": -0.1, "yanchor": "top" }, "margin": { "b": 80, "l": 70, "r": 30, "t": 70 }, "paper_bgcolor": "#fcfcfb", "plot_bgcolor": "#fcfcfb", "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "heatmap" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermap": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermap" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "sequentialminus": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "title": { "font": { "color": "#0b0b0b", "size": 16 }, "text": "Costs by year (risk-adjusted)
Initial = undiscounted time-0 outlay; the red AI-token segment is $0 until priced", "x": 0.02, "xanchor": "left" }, "xaxis": { "linecolor": "#c3c2b7", "showgrid": false, "tickfont": { "color": "#898781" }, "type": "category" }, "yaxis": { "gridcolor": "#e1e0d9", "tickfont": { "color": "#898781" }, "tickformat": "$~s", "zerolinecolor": "#c3c2b7", "zerolinewidth": 1.5 } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = go.Figure()\n", "for r in client[\"rows\"][\"costs\"]:\n", " fig.add_trace(bar(X_LABELS,\n", " [r[\"initial_ra\"]] + [r[\"ra_by_year\"][y] for y in YEARS],\n", " r[\"label\"], COST_COLOR[r[\"field_key\"]]))\n", "fig.update_layout(barmode=\"stack\")\n", "tei_layout(fig, \"Costs by year (risk-adjusted)\",\n", " subtitle=\"Initial = undiscounted time-0 outlay; the red AI-token \"\n", " \"segment is $0 until priced\")\n", "fig.show()\n" ] }, { "cell_type": "markdown", "id": "cell-14", "metadata": {}, "source": [ "\n", "## 5 · Business case\n", "\n", "The left column is Forrester's published Financial Summary, verbatim. The\n", "right column is the client overlay at the sidebar's drivers. At the\n", "defaults the engine reproduces the published totals to within $2 — the\n", "gate in §7 enforces it. Forrester does not headline a payback for this\n", "study; the engine computes **3.3 months** from the cash-flow table\n", "($1.31M initial vs ~$4.7M year-1 net).\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "cell-15", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.484624Z", "iopub.status.busy": "2026-07-09T19:15:20.484408Z", "iopub.status.idle": "2026-07-09T19:15:20.493816Z", "shell.execute_reply": "2026-07-09T19:15:20.493019Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Forrester composite (published 🟢)Client overlay (🟡)
Benefits PV$14,840,638$14,840,637
Costs PV$4,057,170$4,057,170
NPV$10,783,468$10,783,466
ROI266%266%
Paybacknot headlined3.3 months (~Apr 2026)
Discount rate10%10%
\n", "
" ], "text/plain": [ " Forrester composite (published 🟢) Client overlay (🟡)\n", "Benefits PV $14,840,638 $14,840,637\n", "Costs PV $4,057,170 $4,057,170\n", "NPV $10,783,468 $10,783,466\n", "ROI 266% 266%\n", "Payback not headlined 3.3 months (~Apr 2026)\n", "Discount rate 10% 10%" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "engine composite NPV $10,783,466.42 vs published $10,783,468 (Δ -1.58)\n" ] } ], "source": [ "# ── KPIs — published composite beside the client overlay ─────────────\n", "kpis_fmt = pd.DataFrame({\n", " \"Forrester composite (published 🟢)\": {\n", " \"Benefits PV\": f\"${PUBLISHED['benefits_pv']:,}\",\n", " \"Costs PV\": f\"${PUBLISHED['costs_pv']:,}\",\n", " \"NPV\": f\"${PUBLISHED['npv']:,}\",\n", " \"ROI\": f\"{PUBLISHED['roi_pct']}%\",\n", " \"Payback\": \"not headlined\",\n", " \"Discount rate\": f\"{PUBLISHED['discount_rate']:.0%}\",\n", " },\n", " \"Client overlay (🟡)\": {\n", " \"Benefits PV\": f\"${client['benefits_pv']:,.0f}\",\n", " \"Costs PV\": f\"${client['costs_pv']:,.0f}\",\n", " \"NPV\": f\"${client['npv']:,.0f}\",\n", " \"ROI\": f\"{client['roi_pct']:.0f}%\",\n", " \"Payback\": client[\"payback_label\"],\n", " \"Discount rate\": f\"{DISCOUNT_RATE:.0%}\",\n", " },\n", "})\n", "display(kpis_fmt)\n", "backstage(f\"engine composite NPV ${composite['npv']:,.2f} vs published \"\n", " f\"${PUBLISHED['npv']:,} (Δ {composite['npv'] - PUBLISHED['npv']:+,.2f})\")\n" ] }, { "cell_type": "code", "execution_count": 11, "id": "cell-16", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.496223Z", "iopub.status.busy": "2026-07-09T19:15:20.495912Z", "iopub.status.idle": "2026-07-09T19:15:20.525205Z", "shell.execute_reply": "2026-07-09T19:15:20.524534Z" } }, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#1baf7a", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Benefits (risk-adjusted)", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ 0, 5816960.0, 6054460.0, 6054460.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#e34948", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Costs (risk-adjusted)", "type": "bar", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ -1309000.0, -1105080.0, -1105080.0, -1105080.0 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "line": { "color": "#52514e", "width": 2 }, "marker": { "line": { "color": "#fcfcfb", "width": 2 }, "size": 8 }, "mode": "lines+markers", "name": "Cumulative net", "type": "scatter", "x": [ "Initial", "2026", "2027", "2028" ], "y": [ -1309000.0, 3402880.0, 8352260.0, 13301640.0 ] } ], "layout": { "annotations": [ { "align": "left", "bgcolor": "#fcfcfb", "bordercolor": "#e1e0d9", "borderwidth": 1, "font": { "color": "#0b0b0b", "size": 12 }, "showarrow": false, "text": "NPV $10.8M · ROI 266% · payback 3.3 months (~Apr 2026)", "x": 0.02, "xref": "paper", "y": 0.98, "yref": "paper" } ], "bargap": 0.45, "barmode": "relative", "font": { "color": "#52514e", "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif", "size": 12 }, "height": 460, "hovermode": "x unified", "legend": { "font": { "color": "#52514e", "size": 11 }, "orientation": "h", "x": 0, "y": -0.1, "yanchor": "top" }, "margin": { "b": 80, "l": 70, "r": 30, "t": 70 }, "paper_bgcolor": "#fcfcfb", "plot_bgcolor": "#fcfcfb", "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "heatmap" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermap": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermap" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "sequentialminus": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "title": { "font": { "color": "#0b0b0b", "size": 16 }, "text": "Cash flow — risk-adjusted
benefits up, costs down; initial outlay undiscounted at time 0", "x": 0.02, "xanchor": "left" }, "xaxis": { "linecolor": "#c3c2b7", "showgrid": false, "tickfont": { "color": "#898781" }, "type": "category" }, "yaxis": { "gridcolor": "#e1e0d9", "tickfont": { "color": "#898781" }, "tickformat": "$~s", "zerolinecolor": "#c3c2b7", "zerolinewidth": 1.5 } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ── Cash flow — mirrors the PDF's Cash Flow Chart ────────────────────\n", "fig = go.Figure()\n", "fig.add_trace(bar(X_LABELS,\n", " [0] + [client[\"benefits_by_year\"][y] for y in YEARS],\n", " \"Benefits (risk-adjusted)\", BEN_TOTAL))\n", "fig.add_trace(bar(X_LABELS,\n", " [-client[\"initial_costs\"]] + [-client[\"costs_by_year\"][y] for y in YEARS],\n", " \"Costs (risk-adjusted)\", COST_TOTAL))\n", "fig.add_trace(cum_line(X_LABELS,\n", " [-client[\"initial_costs\"]] + [client[\"cumulative_net_by_year\"][y] for y in YEARS],\n", " \"Cumulative net\"))\n", "fig.update_layout(barmode=\"relative\")\n", "fig.add_annotation(\n", " x=0.02, y=0.98, xref=\"paper\", yref=\"paper\", showarrow=False, align=\"left\",\n", " text=(f\"NPV {html_money(client['npv'])} · ROI {client['roi_pct']:.0f}% · \"\n", " f\"payback {client['payback_label']}\"),\n", " font=dict(size=12, color=INK), bgcolor=SURFACE,\n", " bordercolor=GRID, borderwidth=1)\n", "tei_layout(fig, \"Cash flow — risk-adjusted\",\n", " subtitle=\"benefits up, costs down; initial outlay undiscounted at time 0\")\n", "fig.show()\n" ] }, { "cell_type": "code", "execution_count": 12, "id": "cell-17", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.527870Z", "iopub.status.busy": "2026-07-09T19:15:20.527655Z", "iopub.status.idle": "2026-07-09T19:15:20.551901Z", "shell.execute_reply": "2026-07-09T19:15:20.551086Z" } }, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "connector": { "line": { "color": "#e1e0d9" } }, "decreasing": { "marker": { "color": "#e34948" } }, "increasing": { "marker": { "color": "#1baf7a" } }, "measure": [ "relative", "relative", "total" ], "text": [ "$14.8M", "-$4.1M", "$10.8M" ], "textposition": "outside", "totals": { "marker": { "color": "#2a78d6" } }, "type": "waterfall", "x": [ "Benefits PV", "Costs PV", "NPV" ], "y": [ 14840636.814425241, -4057170.398196844, 0 ] } ], "layout": { "bargap": 0.45, "font": { "color": "#52514e", "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif", "size": 12 }, "height": 420, "hovermode": "x unified", "legend": { "font": { "color": "#52514e", "size": 11 }, "orientation": "h", "x": 0, "y": -0.1, "yanchor": "top" }, "margin": { "b": 80, "l": 70, "r": 30, "t": 70 }, "paper_bgcolor": "#fcfcfb", "plot_bgcolor": "#fcfcfb", "showlegend": false, "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "heatmap" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermap": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermap" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "sequentialminus": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "title": { "font": { "color": "#0b0b0b", "size": 16 }, "text": "Present value walk — client overlay
discounted at 10%, 3 years", "x": 0.02, "xanchor": "left" }, "xaxis": { "linecolor": "#c3c2b7", "showgrid": false, "tickfont": { "color": "#898781" }, "type": "category" }, "yaxis": { "gridcolor": "#e1e0d9", "tickfont": { "color": "#898781" }, "tickformat": "$~s", "zerolinecolor": "#c3c2b7", "zerolinewidth": 1.5 } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = go.Figure(go.Waterfall(\n", " x=[\"Benefits PV\", \"Costs PV\", \"NPV\"],\n", " measure=[\"relative\", \"relative\", \"total\"],\n", " y=[client[\"benefits_pv\"], -client[\"costs_pv\"], 0],\n", " text=[html_money(client[\"benefits_pv\"]), html_money(-client[\"costs_pv\"]),\n", " html_money(client[\"npv\"])],\n", " textposition=\"outside\",\n", " connector=dict(line=dict(color=GRID)),\n", " increasing=dict(marker=dict(color=BEN_TOTAL)),\n", " decreasing=dict(marker=dict(color=COST_TOTAL)),\n", " totals=dict(marker=dict(color=NPV_COLOR)),\n", "))\n", "fig.update_layout(showlegend=False)\n", "tei_layout(fig, \"Present value walk — client overlay\",\n", " subtitle=f\"discounted at {'{:.0%}'.format(DISCOUNT_RATE)}, 3 years\",\n", " height=420)\n", "fig.show()\n" ] }, { "cell_type": "markdown", "id": "cell-18", "metadata": {}, "source": [ "\n", "## 6 · Scenarios (🟡)\n", "\n", "Scenarios stress the overlay on two levers: **adoption** scales every\n", "nominal value (including the initial outlay), and **risk delta** widens or\n", "narrows the TEI risk adjustments — added to benefit risk, subtracted from\n", "cost risk, clamped at zero.\n", "\n", "One counterintuitive consequence, worth stating: the **conservative**\n", "scenario *lowers* costs PV as well as benefits — 80% adoption shrinks\n", "every cost line and the clamp zeroes all this study's cost paddings (5–10%\n", "risk factors, all under the 10% delta). The case direction is still\n", "conservative: NPV and ROI both fall.\n" ] }, { "cell_type": "code", "execution_count": 13, "id": "cell-19", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.554518Z", "iopub.status.busy": "2026-07-09T19:15:20.554277Z", "iopub.status.idle": "2026-07-09T19:15:20.564822Z", "shell.execute_reply": "2026-07-09T19:15:20.564005Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
AdoptionRisk ΔBenefits PVCosts PVNPVROI %Payback (months)
Scenario
conservative80%+10%10,543,4943,026,6317,516,8632483
moderate100%+0%14,840,6374,057,17010,783,4662663
aggressive115%-5%18,021,9624,883,28513,138,6772693
\n", "
" ], "text/plain": [ " Adoption Risk Δ Benefits PV Costs PV NPV ROI % \\\n", "Scenario \n", "conservative 80% +10% 10,543,494 3,026,631 7,516,863 248 \n", "moderate 100% +0% 14,840,637 4,057,170 10,783,466 266 \n", "aggressive 115% -5% 18,021,962 4,883,285 13,138,677 269 \n", "\n", " Payback (months) \n", "Scenario \n", "conservative 3 \n", "moderate 3 \n", "aggressive 3 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ── Scenario sweep over the client overlay ───────────────────────────\n", "scen_summaries = {\n", " s: compute_summary(apply_scenario(overlay_benefits, s),\n", " apply_scenario(overlay_costs, s), DISCOUNT_RATE)\n", " for s in SCENARIOS\n", "}\n", "scen_df = pd.DataFrame([{\n", " \"Scenario\": s,\n", " \"Adoption\": f\"{SCENARIOS[s]['adoption']:.0%}\",\n", " \"Risk Δ\": f\"{SCENARIOS[s]['risk_delta']:+.0%}\",\n", " \"Benefits PV\": r[\"benefits_pv\"],\n", " \"Costs PV\": r[\"costs_pv\"],\n", " \"NPV\": r[\"npv\"],\n", " \"ROI %\": round(r[\"roi_pct\"], 1),\n", " \"Payback (months)\": round(r[\"payback_months\"], 2)\n", " if r[\"payback_months\"] is not None else float(\"nan\"),\n", "} for s, r in scen_summaries.items()]).set_index(\"Scenario\")\n", "display(scen_df)\n" ] }, { "cell_type": "code", "execution_count": 14, "id": "cell-20", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.567700Z", "iopub.status.busy": "2026-07-09T19:15:20.567440Z", "iopub.status.idle": "2026-07-09T19:15:20.587490Z", "shell.execute_reply": "2026-07-09T19:15:20.586666Z" } }, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#1baf7a", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Benefits PV", "type": "bar", "x": [ "conservative", "moderate", "aggressive" ], "y": [ 10543493.914350111, 14840636.814425241, 18021962.2539444 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#e34948", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "Costs PV", "type": "bar", "x": [ "conservative", "moderate", "aggressive" ], "y": [ 3026631.4049586775, 4057170.398196844, 4883285.090157775 ] }, { "hovertemplate": "%{fullData.name}: %{y:$,.0f}", "marker": { "color": "#2a78d6", "line": { "color": "#fcfcfb", "width": 2 } }, "name": "NPV", "type": "bar", "x": [ "conservative", "moderate", "aggressive" ], "y": [ 7516862.509391434, 10783466.416228397, 13138677.163786625 ] } ], "layout": { "bargap": 0.45, "barmode": "group", "font": { "color": "#52514e", "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif", "size": 12 }, "height": 420, "hovermode": "x unified", "legend": { "font": { "color": "#52514e", "size": 11 }, "orientation": "h", "x": 0, "y": -0.1, "yanchor": "top" }, "margin": { "b": 80, "l": 70, "r": 30, "t": 70 }, "paper_bgcolor": "#fcfcfb", "plot_bgcolor": "#fcfcfb", "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "heatmap" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermap": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermap" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ], "sequentialminus": [ [ 0.0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1.0, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "title": { "font": { "color": "#0b0b0b", "size": 16 }, "text": "Scenario comparison — client overlay
adoption × risk-delta stress on the same drivers", "x": 0.02, "xanchor": "left" }, "xaxis": { "linecolor": "#c3c2b7", "showgrid": false, "tickfont": { "color": "#898781" }, "type": "category" }, "yaxis": { "gridcolor": "#e1e0d9", "tickfont": { "color": "#898781" }, "tickformat": "$~s", "zerolinecolor": "#c3c2b7", "zerolinewidth": 1.5 } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = go.Figure()\n", "_scen = list(scen_summaries)\n", "for _name, _key, _color in [(\"Benefits PV\", \"benefits_pv\", BEN_TOTAL),\n", " (\"Costs PV\", \"costs_pv\", COST_TOTAL),\n", " (\"NPV\", \"npv\", NPV_COLOR)]:\n", " fig.add_trace(bar(_scen, [scen_summaries[s][_key] for s in _scen],\n", " _name, _color))\n", "fig.update_layout(barmode=\"group\")\n", "tei_layout(fig, \"Scenario comparison — client overlay\",\n", " subtitle=\"adoption × risk-delta stress on the same drivers\",\n", " height=420)\n", "fig.show()\n" ] }, { "cell_type": "markdown", "id": "cell-21", "metadata": {}, "source": [ "\n", "## 7 · Verification & assertions\n", "\n", "The gate re-derives the case from the engine and asserts: the verbatim\n", "anchor is intact (AI-token line at $0 included); the engine reproduces\n", "Forrester's published totals within $5; the overlay is the identity at\n", "composite scale; and the structural identities hold at **any** widget\n", "setting. It must pass in a headless `nbconvert --execute` run — that is\n", "this study's regression check.\n" ] }, { "cell_type": "code", "execution_count": 15, "id": "cell-22", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.590288Z", "iopub.status.busy": "2026-07-09T19:15:20.590029Z", "iopub.status.idle": "2026-07-09T19:15:20.607459Z", "shell.execute_reply": "2026-07-09T19:15:20.606722Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "All assertions passed.\n", " reproduction Δ vs PDF: benefits -1.19 · costs +0.40 · npv -1.58\n" ] } ], "source": [ "def _approx(got, want, tol=0.5):\n", " assert abs(got - want) <= tol, f\"got {got:,.2f}, want {want:,.2f}\"\n", "\n", "\n", "# ── Anchor integrity — the verbatim record is intact (unconditional) ─\n", "_approx(BENEFITS_VERBATIM[0][\"year_values\"][\"1\"], 680_000)\n", "_approx(COSTS_VERBATIM[1][\"initial\"], 1_190_000)\n", "_tok = next(r for r in COSTS_VERBATIM if r[\"field_key\"] == \"genesys_ai_tokens\")\n", "assert all(v == 0 for v in _tok[\"year_values\"].values()) # the $0 AI line, verbatim\n", "assert ASSUMPTIONS[\"agents_fte\"] == 600\n", "assert ASSUMPTIONS[\"weekly_interactions\"] == 80_000\n", "assert (COMPOSITE.agents_fte, COMPOSITE.ai_tokens_annual) == (600, 0.0)\n", "\n", "# ── Published reproduction — engine defaults, explicit args ──────────\n", "_c = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)\n", "_approx(_c[\"benefits_pv\"], PUBLISHED[\"benefits_pv\"], tol=5) # Δ −1.19 (PDF rounding)\n", "_approx(_c[\"costs_pv\"], PUBLISHED[\"costs_pv\"], tol=5) # Δ +0.40\n", "_approx(_c[\"npv\"], PUBLISHED[\"npv\"], tol=5) # Δ −1.58\n", "assert round(_c[\"roi_pct\"]) == PUBLISHED[\"roi_pct\"] # 265.79 → 266\n", "_approx(_c[\"payback_months\"], 3.33, tol=0.01) # not headlined in the PDF\n", "_approx(_c[\"initial_costs\"], 1_309_000)\n", "_approx(_c[\"benefits_by_year\"][2026], 5_816_960, tol=1)\n", "_approx(_c[\"costs_by_year\"][2028], 1_105_080, tol=1)\n", "\n", "# ── Overlay identity + scaling behaviour (explicit args) ─────────────\n", "_ob, _oc = overlay_rows(COMPOSITE)\n", "_id = compute_summary(_ob, _oc, 0.10)\n", "_approx(_id[\"benefits_pv\"], _c[\"benefits_pv\"], tol=0.01) # identity at composite\n", "_hb, _hc = overlay_rows(ClientDrivers(agents_fte=300))\n", "_approx(next(r for r in _hb if r[\"field_key\"] == \"legacy_retirement\")\n", " [\"year_values\"][\"1\"], 340_000) # agents-driven halves\n", "_approx(next(r for r in _hb if r[\"field_key\"] == \"self_service_savings\")\n", " [\"year_values\"][\"1\"], 2_329_600) # interactions-driven unmoved\n", "_tb, _tc = overlay_rows(ClientDrivers(ai_tokens_annual=250_000))\n", "_t = compute_summary(_tb, _tc, 0.10)\n", "_approx(_t[\"costs_pv\"] - _c[\"costs_pv\"], 621_713, tol=1) # tokens priced → PV moves\n", "\n", "# ── Scenario pin (explicit args) ─────────────────────────────────────\n", "_s = compute_summary(apply_scenario(BENEFITS_VERBATIM, \"conservative\"),\n", " apply_scenario(COSTS_VERBATIM, \"conservative\"), 0.10)\n", "_approx(_s[\"npv\"], 7_516_862.51, tol=1)\n", "\n", "# ── Structural ties — hold at ANY widget state (unconditional) ───────\n", "_approx(client[\"npv\"], client[\"benefits_pv\"] - client[\"costs_pv\"], tol=0.01)\n", "_approx(client[\"roi_pct\"], client[\"npv\"] / client[\"costs_pv\"] * 100, tol=0.01)\n", "for _y in YEARS:\n", " _approx(client[\"net_by_year\"][_y],\n", " client[\"benefits_by_year\"][_y] - client[\"costs_by_year\"][_y], tol=0.01)\n", "_approx(client[\"cumulative_net_by_year\"][YEARS[-1]],\n", " sum(client[\"net_by_year\"].values()) - client[\"initial_costs\"], tol=0.01)\n", "_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"benefits\"]), client[\"benefits_pv\"], tol=0.01)\n", "_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"costs\"]), client[\"costs_pv\"], tol=0.01)\n", "\n", "# ── Live state — only when the sidebar sits at the composite defaults ─\n", "if _at_default:\n", " _approx(client[\"benefits_pv\"], 14_840_636.81, tol=1) # engine-exact\n", " _approx(client[\"npv\"], PUBLISHED[\"npv\"], tol=5)\n", " _approx(client[\"payback_months\"], 3.33, tol=0.01)\n", "\n", "backstage(\"All assertions passed.\")\n", "backstage(f\" reproduction Δ vs PDF: benefits \"\n", " f\"{_c['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n", " f\"costs {_c['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n", " f\"npv {_c['npv'] - PUBLISHED['npv']:+,.2f}\")\n" ] }, { "cell_type": "markdown", "id": "cell-23", "metadata": {}, "source": [ "\n", "## 8 · Data appendix — for the machines\n", "\n", "Everything below renders **backstage only** (JupyterLab / nbconvert\n", "exports): markdown tables plus one JSON block of model state. Carried in\n", "the exports, this is the payload a downstream LLM — or, on the roadmap,\n", "Athena as the study repository — consumes directly. On the Mercury stage\n", "it stays hidden.\n" ] }, { "cell_type": "code", "execution_count": 16, "id": "cell-24", "metadata": { "execution": { "iopub.execute_input": "2026-07-09T19:15:20.610247Z", "iopub.status.busy": "2026-07-09T19:15:20.609930Z", "iopub.status.idle": "2026-07-09T19:15:20.632690Z", "shell.execute_reply": "2026-07-09T19:15:20.631995Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "#### Composite organization (verbatim assumptions 🟢)\n", "\n", "| Assumption | Value 🟢 |\n", "|:-------------------------|:---------------|\n", "| annual revenue | $2,500,000,000 |\n", "| employees | 10,000 |\n", "| agents fte | 600 |\n", "| concurrent licenses | 400 |\n", "| weekly interactions | 80,000 |\n", "| interaction minutes | 12 min |\n", "| self service rate before | 15% |\n", "| self service rate after | 25% |\n", "| mttr saved minutes | 2 min |\n", "| agent hourly rate | $28/hr |\n", "| agent annual salary | $58,240 |\n", "| revenue impacted | $500,000,000 |\n", "| revenue lift | 1.5% |\n", "| gross margin | 8% |\n", "| discount rate | 10% |\n", "| analysis years | 3 years |\n", "\n", "#### Benefits — client overlay (risk-adjusted $)\n", "\n", "| Benefit | Driver | Risk adj | 2026 | 2027 | 2028 | 3-yr RA | PV |\n", "|:------------------------------------------------------------------------------------------------------|:-------------|:-----------|----------:|----------:|----------:|-----------:|-----------:|\n", "| Retirement of legacy systems with CX Cloud adoption | agents | -5% | 646,000 | 883,500 | 883,500 | 2,413,000 | 1,981,225 |\n", "| Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service | interactions | -15% | 1,980,160 | 1,980,160 | 1,980,160 | 5,940,480 | 4,924,365 |\n", "| CX agent efficiency gains | interactions | -10% | 2,620,800 | 2,620,800 | 2,620,800 | 7,862,400 | 6,517,542 |\n", "| Incremental sales from agent assist capabilities | revenue | -5% | 570,000 | 570,000 | 570,000 | 1,710,000 | 1,417,506 |\n", "| TOTAL | | | 5,816,960 | 6,054,460 | 6,054,460 | 17,925,880 | 14,840,637 |\n", "\n", "#### Costs — client overlay (risk-adjusted $)\n", "\n", "| Cost | Driver | Risk adj | Initial | 2026 | 2027 | 2028 | PV |\n", "|:----------------------------------------|:----------|:-----------|----------:|----------:|----------:|----------:|----------:|\n", "| CX Cloud solution costs (licenses) | agents | +5% | 0 | 882,000 | 882,000 | 882,000 | 2,193,403 |\n", "| Implementation and deployment cost | fixed | +10% | 1,309,000 | 0 | 0 | 0 | 1,309,000 |\n", "| Ongoing management costs | fixed | +10% | 0 | 223,080 | 223,080 | 223,080 | 554,767 |\n", "| Genesys AI Experience token consumption | ai_tokens | +0% | 0 | 0 | 0 | 0 | 0 |\n", "| TOTAL | | | 1,309,000 | 1,105,080 | 1,105,080 | 1,105,080 | 4,057,170 |\n", "\n", "#### KPIs — published composite vs client overlay\n", "\n", "| | Forrester composite (published 🟢) | Client overlay (🟡) |\n", "|:--------------|:-------------------------------------|:-----------------------|\n", "| Benefits PV | $14,840,638 | $14,840,637 |\n", "| Costs PV | $4,057,170 | $4,057,170 |\n", "| NPV | $10,783,468 | $10,783,466 |\n", "| ROI | 266% | 266% |\n", "| Payback | not headlined | 3.3 months (~Apr 2026) |\n", "| Discount rate | 10% | 10% |\n", "\n", "#### Scenarios (client overlay)\n", "\n", "| Scenario | Adoption | Risk Δ | Benefits PV | Costs PV | NPV | ROI % | Payback (months) |\n", "|:-------------|:-----------|:---------|--------------:|-----------:|-----------:|--------:|-------------------:|\n", "| conservative | 80% | +10% | 10,543,494 | 3,026,631 | 7,516,863 | 248 | 3 |\n", "| moderate | 100% | +0% | 14,840,637 | 4,057,170 | 10,783,466 | 266 | 3 |\n", "| aggressive | 115% | -5% | 18,021,962 | 4,883,285 | 13,138,677 | 269 | 3 |\n", "\n", "#### Model state (JSON)\n", "\n", "```json\n", "{\n", " \"study\": \"202512_TEI_Genesys_CX_Cloud\",\n", " \"source\": \"Forrester TEI of CX Cloud, by Genesys and Salesforce (Dec 2025, commissioned by Genesys and Salesforce)\",\n", " \"published\": {\n", " \"benefits_pv\": 14840638,\n", " \"costs_pv\": 4057170,\n", " \"npv\": 10783468,\n", " \"roi_pct\": 266,\n", " \"discount_rate\": 0.1,\n", " \"analysis_years\": 3\n", " },\n", " \"reproduction\": {\n", " \"benefits_pv\": 14840636.81,\n", " \"costs_pv\": 4057170.4,\n", " \"npv\": 10783466.42,\n", " \"roi_pct\": 265.79,\n", " \"payback_months\": 3.33,\n", " \"payback_note\": \"Forrester does not headline a payback for this study\"\n", " },\n", " \"client\": {\n", " \"drivers\": {\n", " \"agents_fte\": 600,\n", " \"weekly_interactions\": 80000,\n", " \"annual_revenue\": 2500000000.0,\n", " \"ai_tokens_annual\": 0.0,\n", " \"discount_rate\": 0.1,\n", " \"scenario\": \"moderate\"\n", " },\n", " \"benefits_by_year\": {\n", " \"2026\": 5816960,\n", " \"2027\": 6054460,\n", " \"2028\": 6054460\n", " },\n", " \"costs_by_year\": {\n", " \"2026\": 1105080,\n", " \"2027\": 1105080,\n", " \"2028\": 1105080\n", " },\n", " \"net_by_year\": {\n", " \"2026\": 4711880,\n", " \"2027\": 4949380,\n", " \"2028\": 4949380\n", " },\n", " \"cumulative_net_by_year\": {\n", " \"2026\": 3402880,\n", " \"2027\": 8352260,\n", " \"2028\": 13301640\n", " },\n", " \"initial_costs\": 1309000,\n", " \"kpis\": {\n", " \"benefits_pv\": 14840637,\n", " \"costs_pv\": 4057170,\n", " \"npv\": 10783466,\n", " \"roi_pct\": 265.79,\n", " \"payback_months\": 3.33,\n", " \"payback_label\": \"3.3 months (~Apr 2026)\"\n", " },\n", " \"scenarios\": {\n", " \"conservative\": {\n", " \"benefits_pv\": 10543494,\n", " \"costs_pv\": 3026631,\n", " \"npv\": 7516863,\n", " \"roi_pct\": 248.36\n", " },\n", " \"moderate\": {\n", " \"benefits_pv\": 14840637,\n", " \"costs_pv\": 4057170,\n", " \"npv\": 10783466,\n", " \"roi_pct\": 265.79\n", " },\n", " \"aggressive\": {\n", " \"benefits_pv\": 18021962,\n", " \"costs_pv\": 4883285,\n", " \"npv\": 13138677,\n", " \"roi_pct\": 269.05\n", " }\n", " }\n", " },\n", " \"driver_map\": {\n", " \"benefits\": {\n", " \"legacy_retirement\": \"agents\",\n", " \"self_service_savings\": \"interactions\",\n", " \"agent_efficiency\": \"interactions\",\n", " \"agent_assist_sales\": \"revenue\"\n", " },\n", " \"costs\": {\n", " \"cx_cloud_licenses\": \"agents\",\n", " \"implementation\": \"fixed\",\n", " \"ongoing_management\": \"fixed\",\n", " \"genesys_ai_tokens\": \"ai_tokens\"\n", " }\n", " },\n", " \"ai_token_note\": \"Published study models $0 Genesys AI Experience token consumption; benefits B/C/D depend on token-billed AI capabilities. Price via drivers.ai_tokens_annual.\"\n", "}\n", "```\n" ] } ], "source": [ "# ── Data appendix — LLM-readable dump of every model output ──────────\n", "# Renders backstage only (JupyterLab / nbconvert exports) — hidden on\n", "# the Mercury stage, where the narrative and figures carry the story.\n", "import json as _json\n", "\n", "\n", "def _section(title, df, **kw):\n", " backstage(f\"\\n#### {title}\\n\")\n", " backstage(df.to_markdown(floatfmt=\",.0f\", **kw))\n", "\n", "\n", "_section(\"Composite organization (verbatim assumptions 🟢)\",\n", " assumptions_df, index=False)\n", "_section(\"Benefits — client overlay (risk-adjusted $)\", benefits_df)\n", "_section(\"Costs — client overlay (risk-adjusted $)\", costs_df)\n", "_section(\"KPIs — published composite vs client overlay\", kpis_fmt)\n", "_section(\"Scenarios (client overlay)\", scen_df)\n", "\n", "backstage(\"\\n#### Model state (JSON)\\n\")\n", "backstage(\"```json\")\n", "backstage(_json.dumps({\n", " \"study\": \"202512_TEI_Genesys_CX_Cloud\",\n", " \"source\": (\"Forrester TEI of CX Cloud, by Genesys and Salesforce \"\n", " \"(Dec 2025, commissioned by Genesys and Salesforce)\"),\n", " \"published\": PUBLISHED,\n", " \"reproduction\": {\n", " \"benefits_pv\": round(composite[\"benefits_pv\"], 2),\n", " \"costs_pv\": round(composite[\"costs_pv\"], 2),\n", " \"npv\": round(composite[\"npv\"], 2),\n", " \"roi_pct\": round(composite[\"roi_pct\"], 2),\n", " \"payback_months\": round(composite[\"payback_months\"], 2),\n", " \"payback_note\": \"Forrester does not headline a payback for this study\",\n", " },\n", " \"client\": {\n", " \"drivers\": {\n", " \"agents_fte\": AGENTS_FTE,\n", " \"weekly_interactions\": WEEKLY_INTERACTIONS,\n", " \"annual_revenue\": ANNUAL_REVENUE,\n", " \"ai_tokens_annual\": AI_TOKENS_ANNUAL,\n", " \"discount_rate\": DISCOUNT_RATE,\n", " \"scenario\": SCENARIO,\n", " },\n", " \"benefits_by_year\": {str(y): round(client[\"benefits_by_year\"][y]) for y in YEARS},\n", " \"costs_by_year\": {str(y): round(client[\"costs_by_year\"][y]) for y in YEARS},\n", " \"net_by_year\": {str(y): round(client[\"net_by_year\"][y]) for y in YEARS},\n", " \"cumulative_net_by_year\": {str(y): round(client[\"cumulative_net_by_year\"][y]) for y in YEARS},\n", " \"initial_costs\": round(client[\"initial_costs\"]),\n", " \"kpis\": {\n", " \"benefits_pv\": round(client[\"benefits_pv\"]),\n", " \"costs_pv\": round(client[\"costs_pv\"]),\n", " \"npv\": round(client[\"npv\"]),\n", " \"roi_pct\": round(client[\"roi_pct\"], 2),\n", " \"payback_months\": round(client[\"payback_months\"], 2)\n", " if client[\"payback_months\"] is not None else None,\n", " \"payback_label\": client[\"payback_label\"],\n", " },\n", " \"scenarios\": {\n", " s: {\"benefits_pv\": round(r[\"benefits_pv\"]),\n", " \"costs_pv\": round(r[\"costs_pv\"]),\n", " \"npv\": round(r[\"npv\"]),\n", " \"roi_pct\": round(r[\"roi_pct\"], 2)}\n", " for s, r in scen_summaries.items()\n", " },\n", " },\n", " \"driver_map\": {\"benefits\": BENEFIT_DRIVERS, \"costs\": COST_DRIVERS},\n", " \"ai_token_note\": (\"Published study models $0 Genesys AI Experience token \"\n", " \"consumption; benefits B/C/D depend on token-billed AI \"\n", " \"capabilities. Price via drivers.ai_tokens_annual.\"),\n", "}, indent=2))\n", "backstage(\"```\")\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { "14c2d1e5a0644752a2731aca143939b2": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "2247afa5bfb04ad8905333e3f64caf67": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.number.NumberInputWidget", "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "disabled": false, "hidden": false, "label": "Weekly interactions — composite 80,000", "layout": "IPY_MODEL_e2772f6f5be849508a98ba9fd5de0dd1", "layout_path": null, "max": 2000000.0, "min": 1000.0, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "step": 5000.0, "tabbable": null, "tooltip": null, "url_key": "", "value": 80000.0 } }, "2b2aa32e5ae340dfab0c7f0de2791359": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "55da27f443ca4021895933ad5d96f23b": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.number.NumberInputWidget", "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "disabled": false, "hidden": false, "label": "Annual revenue ($) — composite 2.5B", "layout": "IPY_MODEL_14c2d1e5a0644752a2731aca143939b2", "layout_path": null, "max": 100000000000.0, "min": 100000000.0, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "step": 100000000.0, "tabbable": null, "tooltip": null, "url_key": "", "value": 2500000000.0 } }, "6367ab66c77b4dcba88690962f8f466b": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "76c10bd18e9c466daec54721bc92b435": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.select.SelectWidget", "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "choices": [ "8%", "10% (Forrester)", "12%" ], "disabled": false, "hidden": false, "label": "Discount rate", "layout": "IPY_MODEL_fdb25a02906540a49bcd2fb7791aba21", "layout_path": null, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "tabbable": null, "tooltip": null, "url_key": "", "value": "10% (Forrester)" } }, "9173bebf5a34432e8e4819f906db4add": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.number.NumberInputWidget", "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "disabled": false, "hidden": false, "label": "Genesys AI tokens ($/yr) — study modeled $0", "layout": "IPY_MODEL_2b2aa32e5ae340dfab0c7f0de2791359", "layout_path": null, "max": 5000000.0, "min": 0.0, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "step": 25000.0, "tabbable": null, "tooltip": null, "url_key": "", "value": 0.0 } }, "979e73c2734f41dca452ea6800cad0b8": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "2.0.0", "_model_name": "HTMLStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "StyleView", "background": null, "description_width": "", "font_size": null, "text_color": null } }, "ac40d1aeb52245df84aad900dc195c77": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "b01d658cd1294c5382cf6c64284d4781": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "b2751bf42368472ebf73f08317e9cde9": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.number.NumberInputWidget", "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "disabled": false, "hidden": false, "label": "CX agents (FTE) — composite 600", "layout": "IPY_MODEL_6367ab66c77b4dcba88690962f8f466b", "layout_path": null, "max": 20000.0, "min": 25.0, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "step": 25.0, "tabbable": null, "tooltip": null, "url_key": "", "value": 600.0 } }, "c7bf126b79ee49b2bb7140ec4fb10b96": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "2.0.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "2.0.0", "_view_name": "HTMLView", "cell_id": "", "description": "", "description_allow_html": false, "layout": "IPY_MODEL_ac40d1aeb52245df84aad900dc195c77", "layout_path": null, "placeholder": "​", "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "style": "IPY_MODEL_979e73c2734f41dca452ea6800cad0b8", "tabbable": null, "tooltip": null, "value": "" } }, "ce038c3719d240a1a635401ac4a6208e": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", "state": { "_anywidget_id": "mercury.select.SelectWidget", "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", "_dom_classes": [], "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", "_model_module": "anywidget", "_model_module_version": "~0.11.*", "_model_name": "AnyModel", "_view_count": null, "_view_module": "anywidget", "_view_module_version": "~0.11.*", "_view_name": "AnyView", "cell_id": "", "choices": [ "conservative", "moderate", "aggressive" ], "disabled": false, "hidden": false, "label": "Scenario", "layout": "IPY_MODEL_b01d658cd1294c5382cf6c64284d4781", "layout_path": null, "position": "sidebar", "render_slot_id": null, "source_cell_id": null, "tabbable": null, "tooltip": null, "url_key": "", "value": "moderate" } }, "e2772f6f5be849508a98ba9fd5de0dd1": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "fdb25a02906540a49bcd2fb7791aba21": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "2.0.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border_bottom": null, "border_left": null, "border_right": null, "border_top": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } } }, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 5 }