studies/202512_GenesysCX -> studies/202512_TEI_Genesys_CX_Cloud, rebuilt as pattern Variant 4 (TEI composite reproduction): - teicalc/ self-contained engine: Forrester's tables as the never-edited verbatim anchor (incl. the p.14 typo note and the $0 AI-token line), generic model/scenarios/staging carried over from the Amazon Connect study, ClientDrivers overlay (agents / weekly interactions / revenue, flat composite so no growth re-base) with ai_tokens_annual as a direct input for the token line the published study models at $0 - one deliverable notebook (business_case.ipynb): widget-pair sidebar drivers incl. the AI-token price, published-vs-overlay KPI columns, cash-flow/waterfall/scenario charts, verification gate, backstage JSON data appendix - gate + tests reproduce the published totals within $2: NPV $10.8M / ROI 266% (engine $10,783,466 / 265.79%; payback 3.3 months, not headlined in the PDF); 29 study tests, headless nbconvert green, stage simulation leak-free, exports carry the appendix - old Athena workflow (00_provision..04_export, config.py, seed_data.py, PALLADIUM_GENESYSCX_* keys, ATHENA_EXPECTED reconciliation) deleted; git history preserves it With the last legacy study migrated, the retirement lands too: - app/ (Streamlit UI) and core/notebook_helpers deleted; nothing else imported them - streamlit stripped from pyproject extras, requirements.txt, Makefile; .env.example reduced to the Athena keys; 00_setup.ipynb and core/bootstrap.py repointed at the pattern studies - root README reworked: self-contained studies + slim core/ Athena toolkit (tei_client, calculations, export, cli) All suites green: Genesys 29, Amazon Connect 27, CTM 55, template 7, root 58. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7134 lines
268 KiB
Plaintext
7134 lines
268 KiB
Plaintext
{
|
||
"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\"<b>{title}</b>\"\n",
|
||
" if subtitle:\n",
|
||
" t += f\"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>\"\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}<extra></extra>\")\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}<extra></extra>\")\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='<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, …"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── Jump-to-section ToC (Mercury sidebar — widgets only, no output) ──\n",
|
||
"# Fragment links don't scroll inside Mercury's app shell (SPA base URL),\n",
|
||
"# so each entry scrolls via JS: anchor id first, heading-text fallback.\n",
|
||
"_TOC = [\n",
|
||
" (1, \"Composite organization\"),\n",
|
||
" (2, \"Client inputs\"),\n",
|
||
" (3, \"Benefits\"),\n",
|
||
" (4, \"Costs & the $0 AI line\"),\n",
|
||
" (5, \"Business case\"),\n",
|
||
" (6, \"Scenarios\"),\n",
|
||
" (7, \"Verification & assertions\"),\n",
|
||
" (8, \"Data appendix\"),\n",
|
||
"]\n",
|
||
"# NB: no raw \"<\" allowed inside the handler — python-markdown escapes the\n",
|
||
"# whole tag if the attribute text looks like malformed HTML.\n",
|
||
"_JS = (\"var el=document.getElementById('section-{n}');\"\n",
|
||
" \"if(!el){{document.querySelectorAll('h1,h2').forEach(function(h){{\"\n",
|
||
" \"if(!el&&h.textContent.trim().indexOf('{n} ')===0){{el=h;}}}});}}\"\n",
|
||
" \"if(el)el.scrollIntoView({{behavior:'smooth',block:'start'}});\")\n",
|
||
"_items = \"\".join(\n",
|
||
" f'<li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline\"'\n",
|
||
" f' onclick=\"{_JS.format(n=n)}\">{label}</a></li>'\n",
|
||
" for n, label in _TOC)\n",
|
||
"_toc = mr.Markdown(\n",
|
||
" text=(f'<b>Jump to section</b>'\n",
|
||
" f'<ol style=\"padding-left:1.2em;margin:6px 0\">{_items}</ol>'),\n",
|
||
" position=\"sidebar\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "cell-3",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-1\"></a>\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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Value 🟢</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Assumption</th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>annual revenue</th>\n",
|
||
" <td>$2,500,000,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>employees</th>\n",
|
||
" <td>10,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>agents fte</th>\n",
|
||
" <td>600</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>concurrent licenses</th>\n",
|
||
" <td>400</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>weekly interactions</th>\n",
|
||
" <td>80,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>interaction minutes</th>\n",
|
||
" <td>12 min</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>self service rate before</th>\n",
|
||
" <td>15%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>self service rate after</th>\n",
|
||
" <td>25%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>mttr saved minutes</th>\n",
|
||
" <td>2 min</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>agent hourly rate</th>\n",
|
||
" <td>$28/hr</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>agent annual salary</th>\n",
|
||
" <td>$58,240</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>revenue impacted</th>\n",
|
||
" <td>$500,000,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>revenue lift</th>\n",
|
||
" <td>1.5%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>gross margin</th>\n",
|
||
" <td>8%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>discount rate</th>\n",
|
||
" <td>10%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>analysis years</th>\n",
|
||
" <td>3 years</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Published 🟢</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Metric</th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>Benefits PV (risk-adjusted)</th>\n",
|
||
" <td>$14,840,638</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Costs PV (risk-adjusted)</th>\n",
|
||
" <td>$4,057,170</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV</th>\n",
|
||
" <td>$10,783,468</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ROI</th>\n",
|
||
" <td>266%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Payback</th>\n",
|
||
" <td>not headlined (engine: 3.3 months)</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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": [
|
||
"<a id=\"section-2\"></a>\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": [
|
||
"<mercury.number.NumberInputWidget object at 0x7888c41e12b0>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<mercury.number.NumberInputWidget object at 0x7888c41d9310>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<mercury.number.NumberInputWidget object at 0x7888c41d8f50>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<mercury.number.NumberInputWidget object at 0x78886eafd940>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<mercury.select.SelectWidget object at 0x7888c41e16a0>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<mercury.select.SelectWidget object at 0x7888c41d9950>"
|
||
]
|
||
},
|
||
"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": [
|
||
"<a id=\"section-3\"></a>\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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Driver</th>\n",
|
||
" <th>Risk adj</th>\n",
|
||
" <th>2026</th>\n",
|
||
" <th>2027</th>\n",
|
||
" <th>2028</th>\n",
|
||
" <th>3-yr RA</th>\n",
|
||
" <th>PV</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Benefit</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>Retirement of legacy systems with CX Cloud adoption</th>\n",
|
||
" <td>agents</td>\n",
|
||
" <td>-5%</td>\n",
|
||
" <td>646,000</td>\n",
|
||
" <td>883,500</td>\n",
|
||
" <td>883,500</td>\n",
|
||
" <td>2,413,000</td>\n",
|
||
" <td>1,981,225</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service</th>\n",
|
||
" <td>interactions</td>\n",
|
||
" <td>-15%</td>\n",
|
||
" <td>1,980,160</td>\n",
|
||
" <td>1,980,160</td>\n",
|
||
" <td>1,980,160</td>\n",
|
||
" <td>5,940,480</td>\n",
|
||
" <td>4,924,365</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>CX agent efficiency gains</th>\n",
|
||
" <td>interactions</td>\n",
|
||
" <td>-10%</td>\n",
|
||
" <td>2,620,800</td>\n",
|
||
" <td>2,620,800</td>\n",
|
||
" <td>2,620,800</td>\n",
|
||
" <td>7,862,400</td>\n",
|
||
" <td>6,517,542</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Incremental sales from agent assist capabilities</th>\n",
|
||
" <td>revenue</td>\n",
|
||
" <td>-5%</td>\n",
|
||
" <td>570,000</td>\n",
|
||
" <td>570,000</td>\n",
|
||
" <td>570,000</td>\n",
|
||
" <td>1,710,000</td>\n",
|
||
" <td>1,417,506</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>TOTAL</th>\n",
|
||
" <td></td>\n",
|
||
" <td></td>\n",
|
||
" <td>5,816,960</td>\n",
|
||
" <td>6,054,460</td>\n",
|
||
" <td>6,054,460</td>\n",
|
||
" <td>17,925,880</td>\n",
|
||
" <td>14,840,637</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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": "<b>Benefits by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>stacked by benefit stream — client overlay</span>",
|
||
"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": [
|
||
"<a id=\"section-4\"></a>\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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Driver</th>\n",
|
||
" <th>Risk adj</th>\n",
|
||
" <th>Initial</th>\n",
|
||
" <th>2026</th>\n",
|
||
" <th>2027</th>\n",
|
||
" <th>2028</th>\n",
|
||
" <th>PV</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Cost</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>CX Cloud solution costs (licenses)</th>\n",
|
||
" <td>agents</td>\n",
|
||
" <td>+5%</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>882,000</td>\n",
|
||
" <td>882,000</td>\n",
|
||
" <td>882,000</td>\n",
|
||
" <td>2,193,403</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Implementation and deployment cost</th>\n",
|
||
" <td>fixed</td>\n",
|
||
" <td>+10%</td>\n",
|
||
" <td>1,309,000</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>1,309,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Ongoing management costs</th>\n",
|
||
" <td>fixed</td>\n",
|
||
" <td>+10%</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>223,080</td>\n",
|
||
" <td>223,080</td>\n",
|
||
" <td>223,080</td>\n",
|
||
" <td>554,767</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Genesys AI Experience token consumption</th>\n",
|
||
" <td>ai_tokens</td>\n",
|
||
" <td>+0%</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>TOTAL</th>\n",
|
||
" <td></td>\n",
|
||
" <td></td>\n",
|
||
" <td>1,309,000</td>\n",
|
||
" <td>1,105,080</td>\n",
|
||
" <td>1,105,080</td>\n",
|
||
" <td>1,105,080</td>\n",
|
||
" <td>4,057,170</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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": "<b>Costs by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>Initial = undiscounted time-0 outlay; the red AI-token segment is $0 until priced</span>",
|
||
"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": [
|
||
"<a id=\"section-5\"></a>\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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Forrester composite (published 🟢)</th>\n",
|
||
" <th>Client overlay (🟡)</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>Benefits PV</th>\n",
|
||
" <td>$14,840,638</td>\n",
|
||
" <td>$14,840,637</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Costs PV</th>\n",
|
||
" <td>$4,057,170</td>\n",
|
||
" <td>$4,057,170</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV</th>\n",
|
||
" <td>$10,783,468</td>\n",
|
||
" <td>$10,783,466</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ROI</th>\n",
|
||
" <td>266%</td>\n",
|
||
" <td>266%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Payback</th>\n",
|
||
" <td>not headlined</td>\n",
|
||
" <td>3.3 months (~Apr 2026)</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Discount rate</th>\n",
|
||
" <td>10%</td>\n",
|
||
" <td>10%</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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": "<b>Cash flow — risk-adjusted</b><br><span style='font-size:12px;color:#898781'>benefits up, costs down; initial outlay undiscounted at time 0</span>",
|
||
"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": "<b>Present value walk — client overlay</b><br><span style='font-size:12px;color:#898781'>discounted at 10%, 3 years</span>",
|
||
"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": [
|
||
"<a id=\"section-6\"></a>\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": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Adoption</th>\n",
|
||
" <th>Risk Δ</th>\n",
|
||
" <th>Benefits PV</th>\n",
|
||
" <th>Costs PV</th>\n",
|
||
" <th>NPV</th>\n",
|
||
" <th>ROI %</th>\n",
|
||
" <th>Payback (months)</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Scenario</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>conservative</th>\n",
|
||
" <td>80%</td>\n",
|
||
" <td>+10%</td>\n",
|
||
" <td>10,543,494</td>\n",
|
||
" <td>3,026,631</td>\n",
|
||
" <td>7,516,863</td>\n",
|
||
" <td>248</td>\n",
|
||
" <td>3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>moderate</th>\n",
|
||
" <td>100%</td>\n",
|
||
" <td>+0%</td>\n",
|
||
" <td>14,840,637</td>\n",
|
||
" <td>4,057,170</td>\n",
|
||
" <td>10,783,466</td>\n",
|
||
" <td>266</td>\n",
|
||
" <td>3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>aggressive</th>\n",
|
||
" <td>115%</td>\n",
|
||
" <td>-5%</td>\n",
|
||
" <td>18,021,962</td>\n",
|
||
" <td>4,883,285</td>\n",
|
||
" <td>13,138,677</td>\n",
|
||
" <td>269</td>\n",
|
||
" <td>3</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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}<extra></extra>",
|
||
"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": "<b>Scenario comparison — client overlay</b><br><span style='font-size:12px;color:#898781'>adoption × risk-delta stress on the same drivers</span>",
|
||
"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": [
|
||
"<a id=\"section-7\"></a>\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": [
|
||
"<a id=\"section-8\"></a>\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": "<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: normal; line-height: 1.65; color: #0f172a; word-break: break-word;\"><p style=\"margin: 0 0 1em;\"><b>Jump to section</b><ol style=\"padding-left:1.2em;margin:6px 0; margin: 0 0 1em; padding-left: 1.4em;\"><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-1');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('1 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Composite organization</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-2');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('2 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Client inputs</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-3');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('3 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Benefits</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-4');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('4 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Costs & the $0 AI line</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-5');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('5 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Business case</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-6');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('6 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Scenarios</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-7');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('7 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verification & assertions</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-8');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('8 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
|
||
}
|
||
},
|
||
"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
|
||
}
|