7585 lines
337 KiB
Plaintext
7585 lines
337 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "a79bef3a",
|
||
"metadata": {},
|
||
"source": [
|
||
"# CTM × Genesys CCaaS — Migration + WFM (No-AI) Case\n",
|
||
"\n",
|
||
"**Thesis:** strip every AI capability out of the business case and price the platform move\n",
|
||
"alone: migrate all four regions to Genesys Cloud CX, move NA off its existing WFM-like tool\n",
|
||
"onto Genesys WFM, and implement WFM for APAC (**ANZ + ASIA**). Migration and WFM are\n",
|
||
"**included in the base implementation price** — contracted as the signed NTT SOW: **$2.03M\n",
|
||
"professional services** billed over four milestones (2026–27), plus the verbatim $167K\n",
|
||
"training and **$411K/yr managed services** from MCX go-live (30 Sep 2026). The only\n",
|
||
"benefits kept are the deck's verbatim **WFM** lines for the regions\n",
|
||
"in WFM scope; every other benefit (Agent Copilot, Email, STA, Predictive Routing, Supervisor\n",
|
||
"Copilot) is AI-driven and out.\n",
|
||
"\n",
|
||
"| Layer | Source | Treatment |\n",
|
||
"|---|---|---|\n",
|
||
"| WFM benefits | *Appendix 4 — CCaaS Platform Benefit Calculations (Consolidated).pptx*, slides 12–15 | **Verbatim**, scoped to NA/ANZ/ASIA, phased on Genesys's own deployment schedule |\n",
|
||
"| CCaaS licences | Deck TCO (slides 5–6) + signed contract | Deck pitched $4.3M/yr; **contracted $3.2M/yr** drives the case · 6-month ramp credit (🟢 order form) |\n",
|
||
"| NTT PS + training | Signed SOW + deck training line | **$2.03M over four billing milestones (2026–27)** + $167K training — **includes migration + WFM** (deck pitched $2.4M, year 1) |\n",
|
||
"| Managed services | Signed SOW | **$411K/yr from MCX go-live (30 Sep 2026)** — ongoing run-rate cost the deck's TCO never carried |\n",
|
||
"| Existing platform run-off | Term contracts to 31 Dec 2027, per-region | Double-billing until termination |\n",
|
||
"| AI tokens / AI implementation | — | **Removed** — see `ctm_business_case_corrected.ipynb` |\n",
|
||
"\n",
|
||
"Companion to the Corrected Business Case notebook — same engine (`tokencalc`), same\n",
|
||
"baseline-relative frame, same 2026–2028 window, so the two cases are directly comparable.\n",
|
||
"Confidence legend: 🟢 confirmed (published/contractual) · 🟡 estimated (working assumption) · 🔴 unknown.\n",
|
||
"\n",
|
||
"*This notebook is the deliverable: serve it interactively with `mercury --working-dir notebooks/`, tune the 🟡 inputs live for the client, then export an LLM-readable report source with `python scripts/export_report.py`.*"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"id": "38d0b7d9",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.299824Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.299589Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.712827Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.712043Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"tokencalc loaded — 9 sites, 2,088 named users (contracted: 2,088)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── Setup ──────────────────────────────────────────────────────────\n",
|
||
"import sys, pathlib\n",
|
||
"_ROOT = pathlib.Path.cwd()\n",
|
||
"if not (_ROOT / \"tokencalc\").exists(): # notebook lives in notebooks/\n",
|
||
" _ROOT = _ROOT.parent\n",
|
||
"sys.path.insert(0, str(_ROOT))\n",
|
||
"\n",
|
||
"import datetime as dt\n",
|
||
"\n",
|
||
"import pandas as pd\n",
|
||
"import plotly.graph_objects as go\n",
|
||
"\n",
|
||
"import mercury as mr\n",
|
||
"\n",
|
||
"from tokencalc import *\n",
|
||
"# Single source of truth — all math lives in the library; only\n",
|
||
"# presentation (and Mercury input widgets) lives here.\n",
|
||
"from tokencalc.appendix4 import (\n",
|
||
" YEARS, YEAR_INDEX, REGIONS,\n",
|
||
" VERBATIM_BENEFITS, TCO_VERBATIM, IMPL_MONTH, REALIZE_MONTH,\n",
|
||
" DEFAULT_RAMP_MONTHS,\n",
|
||
" build_rollouts, case_flows, case_kpis, crossfoot_tolerance,\n",
|
||
" current_costs_by_year, current_state_inputs, licence_costs_by_year,\n",
|
||
" MANAGED_SERVICES_ANNUAL, MCX_GO_LIVE, PS_CONTRACTED_TOTAL, PS_MILESTONES,\n",
|
||
" managed_services_by_year, money, html_money, month_label, ps_costs_by_year,\n",
|
||
" ps_milestones_dataframe, region_agents, region_site_names, tco,\n",
|
||
")\n",
|
||
"from tokencalc.migration_wfm import (\n",
|
||
" DEFAULT_WFM_REGIONS, runrate_breakeven_label, runrate_saving_annual,\n",
|
||
" wfm_annual_runrate, wfm_benefits_by_year,\n",
|
||
")\n",
|
||
"from tokencalc.business_case import npv\n",
|
||
"from tokencalc.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 region colors — color follows the entity across every figure.\n",
|
||
"REGION_COLOR = {\"NA\": \"#2a78d6\", \"ANZ\": \"#1baf7a\", \"EMEA\": \"#4a3aa7\", \"ASIA\": \"#eda100\"}\n",
|
||
"COST_COLOR = {\n",
|
||
" \"CCaaS platform licences (contracted, ramp-adjusted)\": \"#2a78d6\",\n",
|
||
" \"NTT PS + training (SOW billing milestones)\": \"#1baf7a\",\n",
|
||
" \"NTT managed services (from MCX go-live)\": \"#0b6b4a\",\n",
|
||
" \"Existing platform (term-contract run-off)\": \"#eda100\",\n",
|
||
"}\n",
|
||
"DIVERGING = [[0.0, \"#e34948\"], [0.5, \"#f0efe9\"], [1.0, \"#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",
|
||
"X = [str(y) for y in YEARS]\n",
|
||
"\n",
|
||
"backstage(f\"tokencalc loaded — {len(CTM_DEFAULT_SITES)} sites, \"\n",
|
||
" f\"{sum(s.named_users for s in CTM_DEFAULT_SITES):,} named users \"\n",
|
||
" f\"(contracted: {CONTRACTED_NAMED_USERS:,})\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "104afa86",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.715016Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.714770Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.725088Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.724690Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "cf504293e15c4cd99f1a492d73d8e263",
|
||
"position": "sidebar",
|
||
"widget": "MarkdownWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "cf504293e15c4cd99f1a492d73d8e263",
|
||
"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, \"Current state & contracts\"),\n",
|
||
" (2, \"Scenario scope & licences\"),\n",
|
||
" (3, \"WFM benefits\"),\n",
|
||
" (4, \"Cost stack\"),\n",
|
||
" (5, \"Business case\"),\n",
|
||
" (6, \"Sensitivity\"),\n",
|
||
" (7, \"Verification & assertions\"),\n",
|
||
" (8, \"Risks & notes\"),\n",
|
||
" (9, \"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\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "cecdb4b9",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-1\"></a>\n",
|
||
"\n",
|
||
"## 1 · Current state & contract inputs — data collection\n",
|
||
"\n",
|
||
"Verbatim anchors (Appendix 4, slides 5–6): current solution costs **$7.3M/yr globally**\n",
|
||
"(ANZ, APAC, EMEA, NA — $22M over 3 years); CCaaS is **$4.3M/yr** with **$2.4M professional\n",
|
||
"services + $167K training in year 1**.\n",
|
||
"\n",
|
||
"The same three contract facts as the corrected notebook, collected as editable inputs:\n",
|
||
"\n",
|
||
"1. **Per-region current cost** — seeded as $7.3M × (region agents ÷ total agents). 🟡 Allocation\n",
|
||
" estimate; replace with real regional contract values as they arrive. If NA's existing\n",
|
||
" WFM-like tool bills separately from the platform, fold it into NA's number here — migrating\n",
|
||
" off it is part of this scenario.\n",
|
||
"2. **Contract termination dates** — the current platforms are **term contracts**; CTM pays until\n",
|
||
" termination (default **31 Dec 2027**) regardless of when Genesys goes live → double-billing.\n",
|
||
"3. **Genesys ramp programme** — `RAMP_MONTHS = 6` (🟢 order form): licences are free for\n",
|
||
" the first 6 months, so the licence run-rate bills from July 2026."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "f8863db7",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.727202Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.727057Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.757766Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.757050Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "ddacc3befdc54c278abccd4a5b6b98b5",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "ddacc3befdc54c278abccd4a5b6b98b5",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7908fc43fe00>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "6d231d033d7b4f41b852046f069ebc3f",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "6d231d033d7b4f41b852046f069ebc3f",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x79089e5afc50>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "b2ec5338e0ba4055b77a57edd08b464c",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "b2ec5338e0ba4055b77a57edd08b464c",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x7908fc43fcb0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "28eb2b2d2a2a41adaad830ac204d7b39",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "28eb2b2d2a2a41adaad830ac204d7b39",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7908fc201d10>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "4d3b2b0c854b4bcab393ef4f89aee1b4",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "4d3b2b0c854b4bcab393ef4f89aee1b4",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x7908fc201e50>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "9a1300f4e5374fa6bb1c9717866f8ce1",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "9a1300f4e5374fa6bb1c9717866f8ce1",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x79089e6028b0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "8e89c81717ff408d9e5cb1a949ab337c",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "8e89c81717ff408d9e5cb1a949ab337c",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x7908fc202210>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "cdf8fe1626a54e7eb048e9e8d27cb2a4",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "cdf8fe1626a54e7eb048e9e8d27cb2a4",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x79089e603820>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "68b411c315ff4357815d7ff9801f445a",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "68b411c315ff4357815d7ff9801f445a",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x79089e603950>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── Contract inputs (model: tokencalc.appendix4) ─────────────────────\n",
|
||
"# ⚙ Interactive when served with Mercury; headless runs (nbconvert /\n",
|
||
"# regression gate) use the widget defaults below.\n",
|
||
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
|
||
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
|
||
"_ramp_w = mr.NumberInput(\n",
|
||
" label=\"Genesys ramp — licence-free months\", value=DEFAULT_RAMP_MONTHS,\n",
|
||
" min=0, max=24, step=1, position=\"inline\")\n",
|
||
"\n",
|
||
"sites = list(CTM_DEFAULT_SITES)\n",
|
||
"REGION_SITES = region_site_names(sites)\n",
|
||
"agents_by_region = region_agents(sites)\n",
|
||
"\n",
|
||
"# ── Per-region current-state inputs (EDIT as real data arrives) ──────\n",
|
||
"# Seeded as $7.3M × agent share; annual_cost and contract_termination\n",
|
||
"# are the editable inputs.\n",
|
||
"_seed = current_state_inputs(sites)\n",
|
||
"assert abs(_seed[\"annual_cost\"].sum() - TCO_VERBATIM[\"current_annual\"]) < 1\n",
|
||
"\n",
|
||
"_cost_w, _term_w = {}, {}\n",
|
||
"for _r in REGIONS:\n",
|
||
" _cost_w[_r] = mr.NumberInput(\n",
|
||
" label=f\"{_r} — current platform cost ($/yr)\",\n",
|
||
" value=round(float(_seed.loc[_r, \"annual_cost\"])),\n",
|
||
" min=0, max=8_000_000, step=10_000, position=\"inline\")\n",
|
||
" _term_w[_r] = mr.DateInput(\n",
|
||
" label=f\"{_r} — contract termination\",\n",
|
||
" value=_seed.loc[_r, \"contract_termination\"].isoformat(),\n",
|
||
" position=\"inline\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "0bf61187",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.759694Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.759486Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.771664Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.771155Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"current-state total $7.3M/yr (seed: $7.3M; deck 3-yr $22.0M — deck rounding)\n",
|
||
"existing-platform run-off by year: {2026: '$7.3M', 2027: '$7.3M', 2028: '$0K'}\n"
|
||
]
|
||
},
|
||
{
|
||
"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>agents</th>\n",
|
||
" <th>share</th>\n",
|
||
" <th>annual_cost</th>\n",
|
||
" <th>contract_termination</th>\n",
|
||
" <th>confidence</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>region</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>NA</th>\n",
|
||
" <td>890</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>3,348,969</td>\n",
|
||
" <td>2027-12-31</td>\n",
|
||
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ANZ</th>\n",
|
||
" <td>180</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>677,320</td>\n",
|
||
" <td>2027-12-31</td>\n",
|
||
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>EMEA</th>\n",
|
||
" <td>320</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>1,204,124</td>\n",
|
||
" <td>2027-12-31</td>\n",
|
||
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ASIA</th>\n",
|
||
" <td>550</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>2,069,588</td>\n",
|
||
" <td>2027-12-31</td>\n",
|
||
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" agents share annual_cost contract_termination \\\n",
|
||
"region \n",
|
||
"NA 890 0 3,348,969 2027-12-31 \n",
|
||
"ANZ 180 0 677,320 2027-12-31 \n",
|
||
"EMEA 320 0 1,204,124 2027-12-31 \n",
|
||
"ASIA 550 0 2,069,588 2027-12-31 \n",
|
||
"\n",
|
||
" confidence \n",
|
||
"region \n",
|
||
"NA 🟡 agent-share allocation of the verbatim total \n",
|
||
"ANZ 🟡 agent-share allocation of the verbatim total \n",
|
||
"EMEA 🟡 agent-share allocation of the verbatim total \n",
|
||
"ASIA 🟡 agent-share allocation of the verbatim total "
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── Contract values → model (re-runs on any change to the widgets above) ─\n",
|
||
"RAMP_MONTHS = int(_ramp_w.value)\n",
|
||
"\n",
|
||
"current_state = _seed.copy()\n",
|
||
"for _r in REGIONS:\n",
|
||
" current_state.loc[_r, \"annual_cost\"] = float(_cost_w[_r].value)\n",
|
||
" current_state.loc[_r, \"contract_termination\"] = dt.date.fromisoformat(\n",
|
||
" _term_w[_r].value)\n",
|
||
"\n",
|
||
"current_by_year = current_costs_by_year(current_state)\n",
|
||
"\n",
|
||
"backstage(f\"current-state total {money(current_state['annual_cost'].sum())}/yr \"\n",
|
||
" f\"(seed: {money(TCO_VERBATIM['current_annual'])}; deck 3-yr \"\n",
|
||
" f\"{money(TCO_VERBATIM['current_3yr'])} — deck rounding)\")\n",
|
||
"backstage(f\"existing-platform run-off by year: \"\n",
|
||
" f\"{ {y: money(v) for y, v in current_by_year.items()} }\")\n",
|
||
"display(current_state)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "2caa14c2",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-2\"></a>\n",
|
||
"\n",
|
||
"## 2 · Scenario scope — what the base price includes\n",
|
||
"\n",
|
||
"- **Genesys Cloud CX migration** for all four regions.\n",
|
||
"- **NA — WFM migration**: users move off the existing WFM-like tool onto Genesys WFM. The deck\n",
|
||
" claims **$0** WFM benefit for NA (\"has similar feature\") — kept verbatim.\n",
|
||
"- **APAC (ANZ + ASIA) — WFM implementation**: net-new WFM capability.\n",
|
||
"- **EMEA — no WFM** in this scenario; the deck's $687K 3-yr EMEA WFM line is dropped\n",
|
||
" (upside option — toggle it in the sidebar to see the effect).\n",
|
||
"- **Base implementation price covers all of the above** — contracted as the signed NTT SOW:\n",
|
||
" **$2.03M professional services** billed over four milestones (2026–27; section 4 shows the\n",
|
||
" schedule) plus the verbatim $167K training. No extra cost lines for migration or WFM.\n",
|
||
"- 🟢 **Managed services — contracted**: **$410,918/yr** commences billing at MCX go-live\n",
|
||
" (30 Sep 2026) — an ongoing run-rate cost the deck's TCO never carried; it stays in the\n",
|
||
" post-window steady-state run-rate.\n",
|
||
"- 🟢 **Licence run-rate — contracted**: the signed rate is **$3.2M/yr**, below the deck's\n",
|
||
" pitched $4.3M/yr. The case runs on the contracted rate; the KPI table in section 5 keeps\n",
|
||
" a deck-rate column beside it so the walk from the Genesys report stays visible.\n",
|
||
" 🟡 Still assumed to include the WFM/WEM SKUs (incl. new APAC seats) — if WEM is an\n",
|
||
" add-on, raise the sidebar rate."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "8bafeb91",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.773242Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.773111Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.784646Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.784118Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "cc5b28489de346d3b59e3ace05fb2ebb",
|
||
"position": "sidebar",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "cc5b28489de346d3b59e3ace05fb2ebb",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x79089e6469f0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "2be8dde4d1644691add8dae88a36ac81",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "2be8dde4d1644691add8dae88a36ac81",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x79089e62aa50>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "cddbd4fd3deb4ed1a29dad5cf082939b",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "cddbd4fd3deb4ed1a29dad5cf082939b",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x7908fc202850>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "1d4ddc81f2c249cf9be44e32e8213b85",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "1d4ddc81f2c249cf9be44e32e8213b85",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x7908fc202710>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "5c47680e253c4f45bbbbfcc76612499b",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "5c47680e253c4f45bbbbfcc76612499b",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x79085e4842b0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── CONFIG — scenario scope (Mercury sidebar — widgets only, no output) ─\n",
|
||
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
|
||
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
|
||
"_licence_w = mr.NumberInput(\n",
|
||
" label=\"Genesys licence run-rate ($/yr)\",\n",
|
||
" value=round(tco(\"ccaas_annual\")), # contracted; deck pitched $4.3M\n",
|
||
" min=0, max=10_000_000, step=100_000)\n",
|
||
"_wfm_w = {\n",
|
||
" \"NA\": mr.CheckBox(label=\"WFM — NA (migrate existing users)\", value=True),\n",
|
||
" \"ANZ\": mr.CheckBox(label=\"WFM — ANZ (implement)\", value=True),\n",
|
||
" \"ASIA\": mr.CheckBox(label=\"WFM — ASIA (implement)\", value=True),\n",
|
||
" \"EMEA\": mr.CheckBox(label=\"WFM — EMEA (upside option)\", value=False),\n",
|
||
"}"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"id": "6d971716",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.786019Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.785869Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.802381Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.801425Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"WFM scope: NA, ANZ, ASIA · licence run-rate $3.2M/yr contracted (deck pitched $4.3M/yr) · ramp 6 months\n",
|
||
"ramp-adjusted licences by year: {2026: '$1.6M', 2027: '$3.2M', 2028: '$3.2M'}\n",
|
||
"NTT PS + training (SOW milestones, incl. migration + WFM): $2.2M across 2026-27 · managed services $411K/yr from MCX go-live\n"
|
||
]
|
||
},
|
||
{
|
||
"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>WFM treatment</th>\n",
|
||
" <th>in scope</th>\n",
|
||
" <th>verbatim annual</th>\n",
|
||
" <th>verbatim 3-yr</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>region</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>NA</th>\n",
|
||
" <td>migrate existing users</td>\n",
|
||
" <td>✓</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ANZ</th>\n",
|
||
" <td>implement (APAC)</td>\n",
|
||
" <td>✓</td>\n",
|
||
" <td>1300000</td>\n",
|
||
" <td>1400000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>EMEA</th>\n",
|
||
" <td>implement (upside option)</td>\n",
|
||
" <td>—</td>\n",
|
||
" <td>824000</td>\n",
|
||
" <td>687000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ASIA</th>\n",
|
||
" <td>implement (APAC)</td>\n",
|
||
" <td>✓</td>\n",
|
||
" <td>1600000</td>\n",
|
||
" <td>914000</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" WFM treatment in scope verbatim annual verbatim 3-yr\n",
|
||
"region \n",
|
||
"NA migrate existing users ✓ 0 0\n",
|
||
"ANZ implement (APAC) ✓ 1300000 1400000\n",
|
||
"EMEA implement (upside option) — 824000 687000\n",
|
||
"ASIA implement (APAC) ✓ 1600000 914000"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── Scope derivation + licence schedule ──────────────────────────────\n",
|
||
"LICENCE_ANNUAL = float(_licence_w.value)\n",
|
||
"WFM_REGIONS = [r for r in REGIONS if bool(_wfm_w[r].value)]\n",
|
||
"\n",
|
||
"licence_by_year = licence_costs_by_year(RAMP_MONTHS, LICENCE_ANNUAL)\n",
|
||
"ps_by_year = ps_costs_by_year(contracted=True) # SOW milestones + verbatim training\n",
|
||
"managed_by_year = managed_services_by_year() # 🟢 bills from MCX go-live (30 Sep 2026)\n",
|
||
"\n",
|
||
"_treat = {\"NA\": \"migrate existing users\", \"ANZ\": \"implement (APAC)\",\n",
|
||
" \"ASIA\": \"implement (APAC)\", \"EMEA\": \"implement (upside option)\"}\n",
|
||
"wfm_scope_table = pd.DataFrame([\n",
|
||
" {\"region\": r, \"WFM treatment\": _treat[r],\n",
|
||
" \"in scope\": \"✓\" if r in WFM_REGIONS else \"—\",\n",
|
||
" \"verbatim annual\": VERBATIM_BENEFITS[(r, \"WFM\")][0],\n",
|
||
" \"verbatim 3-yr\": VERBATIM_BENEFITS[(r, \"WFM\")][1]}\n",
|
||
" for r in REGIONS]).set_index(\"region\")\n",
|
||
"\n",
|
||
"print(f\"WFM scope: {', '.join(WFM_REGIONS) or '(none)'} · \"\n",
|
||
" f\"licence run-rate {money(LICENCE_ANNUAL)}/yr contracted \"\n",
|
||
" f\"(deck pitched {money(TCO_VERBATIM['ccaas_annual'])}/yr) · ramp {RAMP_MONTHS} months\")\n",
|
||
"backstage(f\"ramp-adjusted licences by year: \"\n",
|
||
" f\"{ {y: money(v) for y, v in licence_by_year.items()} }\")\n",
|
||
"print(f\"NTT PS + training (SOW milestones, incl. migration + WFM): \"\n",
|
||
" f\"{money(sum(ps_by_year.values()))} across 2026-27 · managed services \"\n",
|
||
" f\"{money(MANAGED_SERVICES_ANNUAL)}/yr from MCX go-live\")\n",
|
||
"display(wfm_scope_table)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "95b71218",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-3\"></a>\n",
|
||
"\n",
|
||
"## 3 · WFM benefits — verbatim, scoped, schedule-phased\n",
|
||
"\n",
|
||
"Only the deck's WFM lines survive the no-AI cut, and only for the regions in WFM scope.\n",
|
||
"They phase on the same Genesys deployment schedule as the corrected notebook (benefits\n",
|
||
"realize **implementation + 3 months**, inclusive): ANZ realizes Dec 2027, ASIA Jun 2028 —\n",
|
||
"so most of the WFM value lands at the very edge of the 3-year window.\n",
|
||
"\n",
|
||
"*Deck quirk, kept verbatim: ASIA's WFM **annual** value ($1.6M) exceeds its **3-yr** value\n",
|
||
"($914K) — the deck's own late-realization arithmetic. The 3-yr figure is what phases here;\n",
|
||
"the annual figure feeds the post-window run-rate in section 5.*"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"id": "12673034",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.804434Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.804197Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:47.852870Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:47.852057Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"WFM benefits by year: {2026: '$0K', 2027: '$108K', 2028: '$2.2M'} → 3-yr $2.3M\n"
|
||
]
|
||
},
|
||
{
|
||
"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>year</th>\n",
|
||
" <th>2026</th>\n",
|
||
" <th>2027</th>\n",
|
||
" <th>2028</th>\n",
|
||
" <th>TOTAL</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>region</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>NA</th>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ANZ</th>\n",
|
||
" <td>0</td>\n",
|
||
" <td>107,692</td>\n",
|
||
" <td>1,292,308</td>\n",
|
||
" <td>1,400,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ASIA</th>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>914,000</td>\n",
|
||
" <td>914,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>TOTAL</th>\n",
|
||
" <td>0</td>\n",
|
||
" <td>107,692</td>\n",
|
||
" <td>2,206,308</td>\n",
|
||
" <td>2,314,000</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
"year 2026 2027 2028 TOTAL\n",
|
||
"region \n",
|
||
"NA 0 0 0 0\n",
|
||
"ANZ 0 107,692 1,292,308 1,400,000\n",
|
||
"ASIA 0 0 914,000 914,000\n",
|
||
"TOTAL 0 107,692 2,206,308 2,314,000"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── WFM slice of the verbatim benefits (tokencalc.migration_wfm) ─────\n",
|
||
"_, _, BENEFIT_ROLLOUT = build_rollouts(sites, ramp_months=RAMP_MONTHS)\n",
|
||
"wfm_long = wfm_benefits_by_year(BENEFIT_ROLLOUT, WFM_REGIONS)\n",
|
||
"ben_by_year = {y: float(wfm_long.loc[wfm_long.year == y, \"benefit\"].sum())\n",
|
||
" for y in YEARS}\n",
|
||
"\n",
|
||
"backstage(f\"WFM benefits by year: { {y: money(v) for y, v in ben_by_year.items()} } \"\n",
|
||
" f\"→ 3-yr {money(sum(ben_by_year.values()))}\")\n",
|
||
"if wfm_long.empty:\n",
|
||
" print(\"No WFM regions in scope — pure licence-swap case.\")\n",
|
||
"else:\n",
|
||
" display(wfm_long.pivot_table(index=\"region\", columns=\"year\", values=\"benefit\",\n",
|
||
" aggfunc=\"sum\", margins=True, margins_name=\"TOTAL\")\n",
|
||
" .reindex([*[r for r in REGIONS if r in WFM_REGIONS], \"TOTAL\"]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"id": "3cbefa13",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:47.854742Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:47.854559Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:48.983809Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:48.980233Z"
|
||
}
|
||
},
|
||
"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": "NA WFM",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
0.0,
|
||
0.0,
|
||
0.0
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#1baf7a",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "ANZ WFM",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
0.0,
|
||
107692.30769230769,
|
||
1292307.6923076923
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#eda100",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "ASIA WFM",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
0.0,
|
||
0.0,
|
||
914000.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 WFM benefits",
|
||
"type": "scatter",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": {
|
||
"bdata": "AAAAAAAAAADsxE7sxEr6QP////+Hp0FB",
|
||
"dtype": "f8"
|
||
}
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$0K</b>",
|
||
"x": 0,
|
||
"y": 0.0,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$108K</b>",
|
||
"x": 1,
|
||
"y": 107692.30769230769,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$2.2M</b>",
|
||
"x": 2,
|
||
"y": 2206307.692307692,
|
||
"yshift": 12
|
||
}
|
||
],
|
||
"bargap": 0.45,
|
||
"barmode": "stack",
|
||
"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>WFM benefits — verbatim, scoped, schedule-phased</b><br><span style='font-size:12px;color:#898781'>NA claims $0 (migration — deck: 'has similar feature') · ANZ realizes Dec 2027, ASIA Jun 2028 — value lands at the edge of the window</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 [r for r in REGIONS if r in WFM_REGIONS]:\n",
|
||
" _v = [float(wfm_long.query(\"region == @_r and year == @y\")[\"benefit\"].sum())\n",
|
||
" for y in YEARS]\n",
|
||
" fig.add_trace(bar(X, _v, f\"{_r} WFM\", REGION_COLOR[_r]))\n",
|
||
"cum_ben = pd.Series([ben_by_year[y] for y in YEARS]).cumsum()\n",
|
||
"fig.add_trace(cum_line(X, cum_ben, \"Cumulative WFM benefits\"))\n",
|
||
"for i, y in enumerate(YEARS):\n",
|
||
" fig.add_annotation(x=i, y=ben_by_year[y], text=f\"<b>{money(ben_by_year[y])}</b>\",\n",
|
||
" showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
|
||
"fig.update_layout(barmode=\"stack\")\n",
|
||
"tei_layout(fig, \"WFM benefits — verbatim, scoped, schedule-phased\",\n",
|
||
" \"NA claims $0 (migration — deck: 'has similar feature') · ANZ realizes \"\n",
|
||
" \"Dec 2027, ASIA Jun 2028 — value lands at the edge of the window\", height=420)\n",
|
||
"fig.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "059bd0f9",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-4\"></a>\n",
|
||
"\n",
|
||
"## 4 · Cost stack — licences, NTT SOW, double-billing\n",
|
||
"\n",
|
||
"Four lines. The AI token-consumption and AI implementation lines are gone —\n",
|
||
"that is the point of this scenario. What remains is the deck's own cost case plus the\n",
|
||
"contract facts it ignored: the **ramp credit** (licences bill from month 7 — 🟢 6-month\n",
|
||
"ramp per the order form), the **term-contract run-off** (double-billing until\n",
|
||
"31 Dec 2027), and the signed NTT SOW — **PS billed over four milestones** (50/50 across\n",
|
||
"2026–27) and **managed services** from MCX go-live.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"id": "ccde902f",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:48.986149Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:48.985913Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.012029Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.008783Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Migration programme cost: {2026: '$10.2M', 2027: '$11.9M', 2028: '$3.6M'} → 3-yr $25.7M\n",
|
||
"Do nothing: $7.3M/yr → 3-yr $21.9M\n"
|
||
]
|
||
},
|
||
{
|
||
"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>2026</th>\n",
|
||
" <th>2027</th>\n",
|
||
" <th>2028</th>\n",
|
||
" <th>3-yr</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>CCaaS platform licences (contracted, ramp-adjusted)</th>\n",
|
||
" <td>1,600,000</td>\n",
|
||
" <td>3,200,000</td>\n",
|
||
" <td>3,200,000</td>\n",
|
||
" <td>8,000,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NTT PS + training (SOW billing milestones)</th>\n",
|
||
" <td>1,179,723</td>\n",
|
||
" <td>1,012,723</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>2,192,446</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NTT managed services (from MCX go-live)</th>\n",
|
||
" <td>102,730</td>\n",
|
||
" <td>410,918</td>\n",
|
||
" <td>410,918</td>\n",
|
||
" <td>924,566</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Existing platform (term-contract run-off)</th>\n",
|
||
" <td>7,300,001</td>\n",
|
||
" <td>7,300,001</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>14,600,002</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>TOTAL</th>\n",
|
||
" <td>10,182,454</td>\n",
|
||
" <td>11,923,643</td>\n",
|
||
" <td>3,610,918</td>\n",
|
||
" <td>25,717,015</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" 2026 2027 \\\n",
|
||
"CCaaS platform licences (contracted, ramp-adjus... 1,600,000 3,200,000 \n",
|
||
"NTT PS + training (SOW billing milestones) 1,179,723 1,012,723 \n",
|
||
"NTT managed services (from MCX go-live) 102,730 410,918 \n",
|
||
"Existing platform (term-contract run-off) 7,300,001 7,300,001 \n",
|
||
"TOTAL 10,182,454 11,923,643 \n",
|
||
"\n",
|
||
" 2028 3-yr \n",
|
||
"CCaaS platform licences (contracted, ramp-adjus... 3,200,000 8,000,000 \n",
|
||
"NTT PS + training (SOW billing milestones) 0 2,192,446 \n",
|
||
"NTT managed services (from MCX go-live) 410,918 924,566 \n",
|
||
"Existing platform (term-contract run-off) 0 14,600,002 \n",
|
||
"TOTAL 3,610,918 25,717,015 "
|
||
]
|
||
},
|
||
"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>date</th>\n",
|
||
" <th>share</th>\n",
|
||
" <th>amount</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>SOW billing milestone</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>SOW Effective Date</th>\n",
|
||
" <td>2026-03-15</td>\n",
|
||
" <td>30%</td>\n",
|
||
" <td>$607,633.94</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Start of client UAT (first region)</th>\n",
|
||
" <td>2026-09-30</td>\n",
|
||
" <td>20%</td>\n",
|
||
" <td>$405,089.30</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Start of client UAT (last region)</th>\n",
|
||
" <td>2027-06-30</td>\n",
|
||
" <td>30%</td>\n",
|
||
" <td>$607,633.94</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Completion of last go-live migration</th>\n",
|
||
" <td>2027-09-30</td>\n",
|
||
" <td>20%</td>\n",
|
||
" <td>$405,089.30</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" date share amount\n",
|
||
"SOW billing milestone \n",
|
||
"SOW Effective Date 2026-03-15 30% $607,633.94\n",
|
||
"Start of client UAT (first region) 2026-09-30 20% $405,089.30\n",
|
||
"Start of client UAT (last region) 2027-06-30 30% $607,633.94\n",
|
||
"Completion of last go-live migration 2027-09-30 20% $405,089.30"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"migration_costs = pd.DataFrame({\n",
|
||
" \"CCaaS platform licences (contracted, ramp-adjusted)\": licence_by_year,\n",
|
||
" \"NTT PS + training (SOW billing milestones)\": ps_by_year,\n",
|
||
" \"NTT managed services (from MCX go-live)\": managed_by_year,\n",
|
||
" \"Existing platform (term-contract run-off)\": current_by_year,\n",
|
||
"}).T[YEARS]\n",
|
||
"migration_costs[\"3-yr\"] = migration_costs.sum(axis=1)\n",
|
||
"total_by_year = {y: float(migration_costs[y].sum()) for y in YEARS}\n",
|
||
"\n",
|
||
"BASELINE_ANNUAL = TCO_VERBATIM[\"current_annual\"]\n",
|
||
"donothing_by_year = {y: BASELINE_ANNUAL for y in YEARS}\n",
|
||
"\n",
|
||
"backstage(f\"Migration programme cost: { {y: money(v) for y, v in total_by_year.items()} } \"\n",
|
||
" f\"→ 3-yr {money(sum(total_by_year.values()))}\")\n",
|
||
"backstage(f\"Do nothing: {money(BASELINE_ANNUAL)}/yr → 3-yr {money(3 * BASELINE_ANNUAL)}\")\n",
|
||
"display(pd.concat([migration_costs,\n",
|
||
" pd.DataFrame(migration_costs.sum()).T.set_axis([\"TOTAL\"])]))\n",
|
||
"\n",
|
||
"_ms = ps_milestones_dataframe()\n",
|
||
"_ms[\"amount\"] = _ms[\"amount\"].map(\"${:,.2f}\".format)\n",
|
||
"_ms[\"share\"] = _ms[\"share\"].map(\"{:.0%}\".format)\n",
|
||
"display(_ms.set_index(\"name\").rename_axis(\"SOW billing milestone\"))\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "c058e795",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.014884Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.014409Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.061734Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.057052Z"
|
||
}
|
||
},
|
||
"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": "CCaaS platform licences (contracted, ramp-adjusted)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
1600000.0,
|
||
3200000.0,
|
||
3200000.0
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#1baf7a",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "NTT PS + training (SOW billing milestones)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
1179723.24,
|
||
1012723.24,
|
||
0.0
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#0b6b4a",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "NTT managed services (from MCX go-live)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
102729.60000000002,
|
||
410918.4000000001,
|
||
410918.4000000001
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#eda100",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "Existing platform (term-contract run-off)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
7300001.0,
|
||
7300001.0,
|
||
0.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 — migration programme",
|
||
"type": "scatter",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": {
|
||
"bdata": "rkfhuuZrY0F7FK4H/xR1QeF6FG6RhnhB",
|
||
"dtype": "f8"
|
||
}
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"line": {
|
||
"color": "#c3c2b7",
|
||
"dash": "dash",
|
||
"width": 2
|
||
},
|
||
"marker": {
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
},
|
||
"size": 8
|
||
},
|
||
"mode": "lines+markers",
|
||
"name": "Cumulative — do nothing",
|
||
"type": "scatter",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": {
|
||
"bdata": "oGNvAEDH3gDgKk4B",
|
||
"dtype": "i4"
|
||
}
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$10.2M</b>",
|
||
"x": 0,
|
||
"y": 10182453.84,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$11.9M</b>",
|
||
"x": 1,
|
||
"y": 11923642.64,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$3.6M</b>",
|
||
"x": 2,
|
||
"y": 3610918.4,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#52514e",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "3-yr <b>$25.7M</b> — $3.8M above doing nothing",
|
||
"x": 2,
|
||
"xshift": -70,
|
||
"y": 25717014.88,
|
||
"yshift": 18
|
||
}
|
||
],
|
||
"bargap": 0.45,
|
||
"barmode": "stack",
|
||
"font": {
|
||
"color": "#52514e",
|
||
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
||
"size": 12
|
||
},
|
||
"height": 500,
|
||
"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>Migration programme cost vs doing nothing</b><br><span style='font-size:12px;color:#898781'>Double-billing hits 2026-27 while existing term contracts run off · licences ramp-free for 6 months (order form), then bill at the run-rate · SOW-billed PS + managed services</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 line in migration_costs.index:\n",
|
||
" fig.add_trace(bar(X, [migration_costs.loc[line, y] for y in YEARS],\n",
|
||
" line, COST_COLOR[line]))\n",
|
||
"cum_mig = pd.Series([total_by_year[y] for y in YEARS]).cumsum()\n",
|
||
"cum_nothing = pd.Series([donothing_by_year[y] for y in YEARS]).cumsum()\n",
|
||
"fig.add_trace(cum_line(X, cum_mig, \"Cumulative — migration programme\"))\n",
|
||
"fig.add_trace(cum_line(X, cum_nothing, \"Cumulative — do nothing\", color=CONTEXT, dash=\"dash\"))\n",
|
||
"for i, y in enumerate(YEARS):\n",
|
||
" fig.add_annotation(x=i, y=total_by_year[y], text=f\"<b>{money(total_by_year[y])}</b>\",\n",
|
||
" showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
|
||
"_delta = float(cum_mig.iloc[-1] - cum_nothing.iloc[-1])\n",
|
||
"fig.add_annotation(x=len(YEARS) - 1, y=float(cum_mig.iloc[-1]),\n",
|
||
" text=f\"3-yr <b>{html_money(float(cum_mig.iloc[-1]))}</b> — \"\n",
|
||
" f\"{html_money(_delta)} above doing nothing\",\n",
|
||
" showarrow=False, yshift=18, xshift=-70, font=dict(size=12, color=INK2))\n",
|
||
"fig.update_layout(barmode=\"stack\")\n",
|
||
"tei_layout(fig, \"Migration programme cost vs doing nothing\",\n",
|
||
" \"Double-billing hits 2026-27 while existing term contracts run off · \"\n",
|
||
" \"licences ramp-free for 6 months (order form), then bill at the run-rate · \"\n",
|
||
" \"SOW-billed PS + managed services\", height=500)\n",
|
||
"fig.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f24b1255",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-5\"></a>\n",
|
||
"\n",
|
||
"## 5 · Business case vs do nothing\n",
|
||
"\n",
|
||
"**Frame:** identical to the corrected notebook — baseline-relative, against *do nothing*\n",
|
||
"(keep paying $7.3M/yr). Incremental cost = programme cost − baseline; net = scoped WFM\n",
|
||
"benefits − incremental cost. The 2026–27 double-billing penalty and the 2028 cost-avoidance\n",
|
||
"credit both fall out of this one frame.\n",
|
||
"\n",
|
||
"With the signed contracts in — **$3.2M/yr licences, 6-month ramp, SOW-billed PS, managed\n",
|
||
"services** — the platform move alone still does not pay back inside the deck's own\n",
|
||
"2026–2028 window (net ≈ −$1.5M). The **run-rate breakeven** KPI extrapolates past the\n",
|
||
"window: steady state = (baseline − licence run-rate − managed services) + scoped WFM\n",
|
||
"annual values, which carries the case to breakeven in early 2029. The deck frame (pitched\n",
|
||
"$4.3M/yr, deck's year-1 PS lump, no managed services) is worse on both counts — the KPI\n",
|
||
"table shows the columns side by side, so the walk from the Genesys report to the\n",
|
||
"contracted reality stays explicit.\n",
|
||
"\n",
|
||
"*Not modelled: early-termination fees, and migration costs beyond the SOW.*\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"id": "d8293152",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.063837Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.063671Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.069028Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.068435Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "720f723fcedb42efa4441ea0f5e2aae2",
|
||
"position": "sidebar",
|
||
"widget": "SelectWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "720f723fcedb42efa4441ea0f5e2aae2",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.select.SelectWidget object at 0x79085d1841a0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── NPV rate (Mercury sidebar — widgets only, no output) ────────────\n",
|
||
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
|
||
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
|
||
"_npv_rate_w = mr.Select(label=\"NPV discount rate\", value=\"13.5% (deck)\",\n",
|
||
" choices=[\"13.5% (deck)\", \"8.0% (CTM treasury)\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"id": "c89973af",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.070865Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.070716Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.076308Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.075521Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"net by year: {2026: '-$2.9M', 2027: '-$4.5M', 2028: '$5.9M'} → 3-yr -$1.5M\n",
|
||
"steady-state run-rate saving $6.6M/yr ($4.1M licences − $411K managed services + $2.9M WFM annuals) → breakeven 39 months (~Mar 2029, extrapolated)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Baseline-relative frame (tokencalc.appendix4.case_flows / case_kpis):\n",
|
||
"# baseline = do nothing = keep paying $7.3M/yr.\n",
|
||
"DISCOUNT_RATE = (TCO_VERBATIM[\"npv_discount_rate\"]\n",
|
||
" if _npv_rate_w.value.startswith(\"13.5\") else 0.08)\n",
|
||
"inc, net_by = case_flows(total_by_year, ben_by_year)\n",
|
||
"kpi = case_kpis(inc, net_by, DISCOUNT_RATE)\n",
|
||
"\n",
|
||
"# Steady-state run-rate once contracts end and the ramp is over\n",
|
||
"# (tokencalc.migration_wfm) — fills the end-2028 deficit.\n",
|
||
"RUNRATE_SAVING = runrate_saving_annual(LICENCE_ANNUAL, WFM_REGIONS,\n",
|
||
" managed_annual=MANAGED_SERVICES_ANNUAL)\n",
|
||
"BREAKEVEN = runrate_breakeven_label(net_by, RUNRATE_SAVING)\n",
|
||
"\n",
|
||
"\n",
|
||
"def fmt_roi(k):\n",
|
||
" return f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net cost saving\"\n",
|
||
"\n",
|
||
"\n",
|
||
"backstage(f\"net by year: { {y: money(v) for y, v in net_by.items()} } \"\n",
|
||
" f\"→ 3-yr {money(kpi['net_3yr'])}\")\n",
|
||
"backstage(f\"steady-state run-rate saving {money(RUNRATE_SAVING)}/yr \"\n",
|
||
" f\"({money(BASELINE_ANNUAL - LICENCE_ANNUAL)} licences − \"\n",
|
||
" f\"{money(MANAGED_SERVICES_ANNUAL)} managed services + \"\n",
|
||
" f\"{money(wfm_annual_runrate(WFM_REGIONS))} WFM annuals) → breakeven {BREAKEVEN}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "9d135a19",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.077976Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.077813Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.102760Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.102141Z"
|
||
}
|
||
},
|
||
"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": "WFM benefits (verbatim, scoped)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
0.0,
|
||
107692.30769230769,
|
||
2206307.692307692
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#e34948",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "Incremental cost vs $7.3M/yr baseline",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
-2882453.84,
|
||
-4623642.640000001,
|
||
3689081.6
|
||
]
|
||
},
|
||
{
|
||
"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": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": {
|
||
"bdata": "uB6F68r9RcHYFgcLATlcwRiuR+Em7zbB",
|
||
"dtype": "f8"
|
||
}
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$2.9M</b>",
|
||
"x": 0,
|
||
"y": -2882453.84,
|
||
"yshift": -14
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$7.4M</b>",
|
||
"x": 1,
|
||
"y": -7398404.1723076925,
|
||
"yshift": -14
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$1.5M</b>",
|
||
"x": 2,
|
||
"y": -1503014.8800000008,
|
||
"yshift": -14
|
||
},
|
||
{
|
||
"align": "left",
|
||
"bgcolor": "#fcfcfb",
|
||
"bordercolor": "#e1e0d9",
|
||
"borderwidth": 1,
|
||
"font": {
|
||
"color": "#52514e",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "3-yr net <b>-$1.5M</b> · ROI <b>-39%</b><br>NPV@13.5% <b>-$2.0M</b> · payback <b>beyond 2028</b><br>run-rate <b>$6.6M/yr</b> post-window → breakeven <b>39 months (~Mar 2029, extrapolated)</b>",
|
||
"x": 0.01,
|
||
"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": 500,
|
||
"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>Migration + WFM — benefits vs incremental cost</b><br><span style='font-size:12px;color:#898781'>Baseline = keep paying $7.3M/yr · the platform swap alone does not pay back inside the deck's own 3-year window</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",
|
||
"fig.add_trace(bar(X, [ben_by_year[y] for y in YEARS],\n",
|
||
" \"WFM benefits (verbatim, scoped)\", \"#1baf7a\"))\n",
|
||
"fig.add_trace(bar(X, [-inc[y] for y in YEARS],\n",
|
||
" \"Incremental cost vs $7.3M/yr baseline\", \"#e34948\"))\n",
|
||
"cum_net = pd.Series([net_by[y] for y in YEARS]).cumsum()\n",
|
||
"fig.add_trace(cum_line(X, cum_net, \"Cumulative net\"))\n",
|
||
"for i, c in enumerate(cum_net):\n",
|
||
" fig.add_annotation(x=i, y=float(c), text=f\"<b>{money(float(c))}</b>\", showarrow=False,\n",
|
||
" yshift=14 if c >= 0 else -14, font=dict(size=12, color=INK))\n",
|
||
"fig.update_layout(barmode=\"relative\")\n",
|
||
"fig.add_annotation(\n",
|
||
" xref=\"paper\", yref=\"paper\", x=0.01, y=0.98, align=\"left\", showarrow=False,\n",
|
||
" font=dict(size=12, color=INK2), bgcolor=SURFACE, bordercolor=GRID, borderwidth=1,\n",
|
||
" text=(f\"3-yr net <b>{html_money(kpi['net_3yr'])}</b> · \"\n",
|
||
" f\"ROI <b>{fmt_roi(kpi)}</b><br>\"\n",
|
||
" f\"NPV@{DISCOUNT_RATE:.1%} <b>{html_money(kpi['npv'])}</b> · \"\n",
|
||
" f\"payback <b>{kpi['payback']}</b><br>\"\n",
|
||
" f\"run-rate <b>{html_money(RUNRATE_SAVING)}/yr</b> post-window → \"\n",
|
||
" f\"breakeven <b>{BREAKEVEN}</b>\"))\n",
|
||
"tei_layout(fig, \"Migration + WFM — benefits vs incremental cost\",\n",
|
||
" \"Baseline = keep paying $7.3M/yr · the platform swap alone does not pay \"\n",
|
||
" \"back inside the deck's own 3-year window\", height=500)\n",
|
||
"fig.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"id": "7ca576f1",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.105099Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.104937Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.115327Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.114734Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"The signed contracts move the 3-yr case by $2.2M vs the deck frame — the licence correction helps; SOW managed services cost against it. For the with-AI case in the same frame, open the Corrected Business Case notebook.\n"
|
||
]
|
||
},
|
||
{
|
||
"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>Deck frame ($4.3M/yr, deck PS)</th>\n",
|
||
" <th>Contracted ($3.2M/yr + SOW)</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>3-yr WFM benefits (verbatim, scoped)</th>\n",
|
||
" <td>$2.3M</td>\n",
|
||
" <td>$2.3M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3-yr incremental cost</th>\n",
|
||
" <td>$6.0M</td>\n",
|
||
" <td>$3.8M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3-yr net</th>\n",
|
||
" <td>-$3.7M</td>\n",
|
||
" <td>-$1.5M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ROI (net ÷ incremental cost)</th>\n",
|
||
" <td>-62%</td>\n",
|
||
" <td>-39%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV @ 13.5% (deck rate)</th>\n",
|
||
" <td>-$3.8M</td>\n",
|
||
" <td>-$2.0M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV @ 8.0% (CTM treasury)</th>\n",
|
||
" <td>-$3.8M</td>\n",
|
||
" <td>-$1.9M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Payback (2026–28 window)</th>\n",
|
||
" <td>beyond 2028</td>\n",
|
||
" <td>beyond 2028</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Steady-state run-rate saving</th>\n",
|
||
" <td>$5.9M/yr</td>\n",
|
||
" <td>$6.6M/yr</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Breakeven</th>\n",
|
||
" <td>44 months (~Aug 2029, extrapolated)</td>\n",
|
||
" <td>39 months (~Mar 2029, extrapolated)</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Deck frame ($4.3M/yr, deck PS) \\\n",
|
||
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
|
||
"3-yr incremental cost $6.0M \n",
|
||
"3-yr net -$3.7M \n",
|
||
"ROI (net ÷ incremental cost) -62% \n",
|
||
"NPV @ 13.5% (deck rate) -$3.8M \n",
|
||
"NPV @ 8.0% (CTM treasury) -$3.8M \n",
|
||
"Payback (2026–28 window) beyond 2028 \n",
|
||
"Steady-state run-rate saving $5.9M/yr \n",
|
||
"Breakeven 44 months (~Aug 2029, extrapolated) \n",
|
||
"\n",
|
||
" Contracted ($3.2M/yr + SOW) \n",
|
||
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
|
||
"3-yr incremental cost $3.8M \n",
|
||
"3-yr net -$1.5M \n",
|
||
"ROI (net ÷ incremental cost) -39% \n",
|
||
"NPV @ 13.5% (deck rate) -$2.0M \n",
|
||
"NPV @ 8.0% (CTM treasury) -$1.9M \n",
|
||
"Payback (2026–28 window) beyond 2028 \n",
|
||
"Steady-state run-rate saving $6.6M/yr \n",
|
||
"Breakeven 39 months (~Mar 2029, extrapolated) "
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"def kpi_display(k, net, runrate, breakeven):\n",
|
||
" return {\n",
|
||
" \"3-yr WFM benefits (verbatim, scoped)\": money(k[\"benefits_3yr\"]),\n",
|
||
" \"3-yr incremental cost\": money(k[\"incremental_cost_3yr\"]),\n",
|
||
" \"3-yr net\": money(k[\"net_3yr\"]),\n",
|
||
" \"ROI (net ÷ incremental cost)\": fmt_roi(k),\n",
|
||
" f\"NPV @ {k['discount_rate']:.1%} (deck rate)\": money(k[\"npv\"]),\n",
|
||
" \"NPV @ 8.0% (CTM treasury)\": money(npv([net[y] for y in YEARS], 0.08)),\n",
|
||
" \"Payback (2026–28 window)\": k[\"payback\"],\n",
|
||
" \"Steady-state run-rate saving\": f\"{money(runrate)}/yr\",\n",
|
||
" \"Breakeven\": breakeven,\n",
|
||
" }\n",
|
||
"\n",
|
||
"\n",
|
||
"# Deck-anchored comparison: the deck's own frame — pitched licence rate,\n",
|
||
"# verbatim year-1 PS lump, no managed services (the SOW lines are contracted\n",
|
||
"# facts the deck never carried).\n",
|
||
"_lic_deck = licence_costs_by_year(RAMP_MONTHS, TCO_VERBATIM[\"ccaas_annual\"])\n",
|
||
"_ps_deck = ps_costs_by_year()\n",
|
||
"total_deck = {y: current_by_year[y] + _lic_deck[y] + _ps_deck[y] for y in YEARS}\n",
|
||
"inc_deck, net_deck = case_flows(total_deck, ben_by_year)\n",
|
||
"kpi_deck = case_kpis(inc_deck, net_deck, DISCOUNT_RATE)\n",
|
||
"RUNRATE_DECK = runrate_saving_annual(TCO_VERBATIM[\"ccaas_annual\"], WFM_REGIONS)\n",
|
||
"BREAKEVEN_DECK = runrate_breakeven_label(net_deck, RUNRATE_DECK)\n",
|
||
"\n",
|
||
"kpis_fmt = pd.DataFrame({\n",
|
||
" f\"Deck frame ({money(TCO_VERBATIM['ccaas_annual'])}/yr, deck PS)\":\n",
|
||
" kpi_display(kpi_deck, net_deck, RUNRATE_DECK, BREAKEVEN_DECK),\n",
|
||
" f\"Contracted ({money(LICENCE_ANNUAL)}/yr + SOW)\":\n",
|
||
" kpi_display(kpi, net_by, RUNRATE_SAVING, BREAKEVEN),\n",
|
||
"})\n",
|
||
"print(f\"The signed contracts move the 3-yr case by \"\n",
|
||
" f\"{money(kpi['net_3yr'] - kpi_deck['net_3yr'])} vs the deck frame — the licence \"\n",
|
||
" f\"correction helps; SOW managed services cost against it. \"\n",
|
||
" f\"For the with-AI case in the same frame, open the Corrected Business Case notebook.\")\n",
|
||
"display(kpis_fmt)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "a7329ce6",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-6\"></a>\n",
|
||
"\n",
|
||
"## 6 · Sensitivity — ramp × licence run-rate\n",
|
||
"\n",
|
||
"The two commercial levers Genesys controls directly, swept against the 3-yr net:\n",
|
||
"the **ramp programme length** (licence-free months) and the **licence run-rate**\n",
|
||
"(contracted $3.2M/yr vs the deck's pitched $4.3M/yr; 🟡 WEM-SKU inclusion either way).\n",
|
||
"Contract **termination dates**\n",
|
||
"are the other big lever — edit the per-region dates in section 1 and the whole\n",
|
||
"notebook recomputes."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"id": "4ab17e24",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.118883Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.118721Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.156840Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.156126Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/vnd.plotly.v1+json": {
|
||
"config": {
|
||
"plotlyServerURL": "https://plot.ly"
|
||
},
|
||
"data": [
|
||
{
|
||
"colorscale": [
|
||
[
|
||
0.0,
|
||
"#e34948"
|
||
],
|
||
[
|
||
0.5,
|
||
"#f0efe9"
|
||
],
|
||
[
|
||
1.0,
|
||
"#2a78d6"
|
||
]
|
||
],
|
||
"hovertemplate": "%{y} × %{x}: %{z:$,.0f}<extra></extra>",
|
||
"showscale": false,
|
||
"type": "heatmap",
|
||
"x": [
|
||
"$3.2M/yr (contracted)",
|
||
"$3.8M/yr",
|
||
"$4.3M/yr (deck)"
|
||
],
|
||
"xgap": 2,
|
||
"y": [
|
||
"0-mo ramp",
|
||
"6-mo ramp",
|
||
"12-mo ramp",
|
||
"18-mo ramp",
|
||
"24-mo ramp"
|
||
],
|
||
"ygap": 2,
|
||
"z": [
|
||
[
|
||
-3103014.880000001,
|
||
-4903014.880000001,
|
||
-6403014.880000001
|
||
],
|
||
[
|
||
-1503014.8800000008,
|
||
-3003014.880000001,
|
||
-4253014.880000001
|
||
],
|
||
[
|
||
96985.11999999918,
|
||
-1103014.8800000008,
|
||
-2103014.880000001
|
||
],
|
||
[
|
||
1696985.1199999987,
|
||
796985.1199999987,
|
||
46985.119999998715
|
||
],
|
||
[
|
||
3296985.119999999,
|
||
2696985.119999999,
|
||
2196985.119999999
|
||
]
|
||
],
|
||
"zmid": 0
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$3.1M</b>",
|
||
"x": 0,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$4.9M</b>",
|
||
"x": 1,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$6.4M</b>",
|
||
"x": 2,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$1.5M</b>",
|
||
"x": 0,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$3.0M</b>",
|
||
"x": 1,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$4.3M</b>",
|
||
"x": 2,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$97K</b>",
|
||
"x": 0,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$1.1M</b>",
|
||
"x": 1,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$2.1M</b>",
|
||
"x": 2,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$1.7M</b>",
|
||
"x": 0,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$797K</b>",
|
||
"x": 1,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$47K</b>",
|
||
"x": 2,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$3.3M</b>",
|
||
"x": 0,
|
||
"y": 4
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$2.7M</b>",
|
||
"x": 1,
|
||
"y": 4
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$2.2M</b>",
|
||
"x": 2,
|
||
"y": 4
|
||
}
|
||
],
|
||
"bargap": 0.45,
|
||
"font": {
|
||
"color": "#52514e",
|
||
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
||
"size": 12
|
||
},
|
||
"height": 400,
|
||
"hovermode": "closest",
|
||
"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>3-yr net across ramp length × licence run-rate</b><br><span style='font-size:12px;color:#898781'>Diverging scale centered at $0 · SOW PS + managed services included · WFM scope and contract dates held at current inputs</span>",
|
||
"x": 0.02,
|
||
"xanchor": "left"
|
||
},
|
||
"xaxis": {
|
||
"linecolor": "#c3c2b7",
|
||
"showgrid": false,
|
||
"tickfont": {
|
||
"color": "#898781"
|
||
},
|
||
"type": "category"
|
||
},
|
||
"yaxis": {
|
||
"gridcolor": "#e1e0d9",
|
||
"showgrid": false,
|
||
"tickfont": {
|
||
"color": "#898781"
|
||
},
|
||
"zerolinecolor": "#c3c2b7",
|
||
"zerolinewidth": 1.5
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Contracted corner (6-mo ramp × $3.2M/yr): -$1.5M · deck-rate corner (6-mo × $4.3M/yr): -$4.3M — the licence correction recovers $2.8M but the 3-yr window stays negative; breakeven rides the post-2028 run-rate.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# ── 3-yr net across ramp months × licence run-rate ───────────────────\n",
|
||
"RAMP_GRID = [0, 6, 12, 18, 24]\n",
|
||
"LIC_GRID = [3_200_000, 3_800_000, 4_300_000]\n",
|
||
"_LIC_TAG = {3_200_000: \" (contracted)\", 4_300_000: \" (deck)\"}\n",
|
||
"\n",
|
||
"\n",
|
||
"def net_3yr(ramp_months, licence_annual):\n",
|
||
" lby = licence_costs_by_year(ramp_months, licence_annual)\n",
|
||
" t = {y: current_by_year[y] + lby[y] + ps_by_year[y] + managed_by_year[y]\n",
|
||
" for y in YEARS}\n",
|
||
" _, n = case_flows(t, ben_by_year)\n",
|
||
" return sum(n.values())\n",
|
||
"\n",
|
||
"\n",
|
||
"z = [[net_3yr(rm, lc) for lc in LIC_GRID] for rm in RAMP_GRID]\n",
|
||
"_zabs = max(abs(v) for row in z for v in row)\n",
|
||
"fig = go.Figure(go.Heatmap(\n",
|
||
" z=z, x=[f\"{money(lc)}/yr{_LIC_TAG.get(lc, '')}\" for lc in LIC_GRID],\n",
|
||
" y=[f\"{rm}-mo ramp\" for rm in RAMP_GRID],\n",
|
||
" zmid=0, colorscale=DIVERGING, showscale=False, xgap=2, ygap=2,\n",
|
||
" hovertemplate=\"%{y} × %{x}: %{z:$,.0f}<extra></extra>\"))\n",
|
||
"for i, rm in enumerate(RAMP_GRID):\n",
|
||
" for j, lc in enumerate(LIC_GRID):\n",
|
||
" v = z[i][j]\n",
|
||
" fig.add_annotation(x=j, y=i, text=f\"<b>{money(v)}</b>\", showarrow=False,\n",
|
||
" font=dict(size=12,\n",
|
||
" color=\"#ffffff\" if abs(v) > 0.65 * _zabs else INK))\n",
|
||
"tei_layout(fig, \"3-yr net across ramp length × licence run-rate\",\n",
|
||
" \"Diverging scale centered at $0 · SOW PS + managed services included · \"\n",
|
||
" \"WFM scope and contract dates held at current inputs\", height=400)\n",
|
||
"fig.update_layout(xaxis=dict(showgrid=False), yaxis=dict(showgrid=False, tickformat=None),\n",
|
||
" hovermode=\"closest\")\n",
|
||
"fig.show()\n",
|
||
"print(f\"Contracted corner ({RAMP_MONTHS}-mo ramp × {money(LICENCE_ANNUAL)}/yr): \"\n",
|
||
" f\"{money(net_3yr(RAMP_MONTHS, LICENCE_ANNUAL))} · deck-rate corner \"\n",
|
||
" f\"({RAMP_MONTHS}-mo × {money(TCO_VERBATIM['ccaas_annual'])}/yr): \"\n",
|
||
" f\"{money(net_3yr(RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} — the licence \"\n",
|
||
" f\"correction recovers \"\n",
|
||
" f\"{money(net_3yr(RAMP_MONTHS, LICENCE_ANNUAL) - net_3yr(RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} \"\n",
|
||
" f\"but the 3-yr window stays negative; breakeven rides the post-2028 run-rate.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "13d4932f",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-7\"></a>\n",
|
||
"\n",
|
||
"## 7 · Verification & assertions\n",
|
||
"\n",
|
||
"The next cell re-derives key numbers independently and **raises on any failure**, so\n",
|
||
"`jupyter nbconvert --execute` acts as a regression gate for this notebook. Engine pins\n",
|
||
"use explicit default arguments so the gate tests `tokencalc`, not the current widget\n",
|
||
"state; live-state checks are guarded so the cell stays valid after you edit the inputs."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 16,
|
||
"id": "939417e4",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.160025Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.159836Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.177363Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.176452Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"All assertions passed.\n",
|
||
" WFM benefits 3-yr $2.3M · programme cost 3-yr $25.7M · net 3-yr -$1.5M · breakeven 39 months (~Mar 2029, extrapolated)\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",
|
||
"# Section 1 — contract mechanics (engine pins, explicit defaults)\n",
|
||
"_seed_check = current_state_inputs(sites)\n",
|
||
"_approx(_seed_check[\"annual_cost\"].sum(), 7_300_000)\n",
|
||
"assert DEFAULT_RAMP_MONTHS == 6 # 🟢 order form\n",
|
||
"_l_deck = licence_costs_by_year() # deck rate at the real ramp\n",
|
||
"_approx(_l_deck[2026], 2_150_000)\n",
|
||
"_approx(_l_deck[2027], 4_300_000)\n",
|
||
"_approx(licence_costs_by_year(12)[2026], 0) # mechanics: 12-mo ramp zeroes 2026\n",
|
||
"_contracts_at_default = (\n",
|
||
" (current_state[\"annual_cost\"] - _seed_check[\"annual_cost\"]).abs().max() < 1\n",
|
||
" and all(t == dt.date(2027, 12, 31) for t in current_state[\"contract_termination\"])\n",
|
||
")\n",
|
||
"if _contracts_at_default:\n",
|
||
" # widget seeds are whole dollars → per-region rounding of ≤ $0.50\n",
|
||
" _approx(current_by_year[2026], 7_300_000, tol=len(REGIONS))\n",
|
||
" _approx(current_by_year[2028], 0)\n",
|
||
"\n",
|
||
"# NTT SOW — billing milestones + managed services (🟢 contractual)\n",
|
||
"_approx(PS_CONTRACTED_TOTAL, 2_025_446.48, tol=0.01)\n",
|
||
"_ps_c = ps_costs_by_year(contracted=True)\n",
|
||
"_approx(_ps_c[2026], 607_633.94 + 405_089.30 + 167_000, tol=0.01) # 30% + 20% + training\n",
|
||
"_approx(_ps_c[2027], 607_633.94 + 405_089.30, tol=0.01) # 30% + 20%\n",
|
||
"_ms_c = managed_services_by_year()\n",
|
||
"_approx(_ms_c[2026], MANAGED_SERVICES_ANNUAL * 3 / 12, tol=0.01) # Oct–Dec after go-live\n",
|
||
"_approx(_ms_c[2027], MANAGED_SERVICES_ANNUAL, tol=0.01)\n",
|
||
"\n",
|
||
"# Sections 2-3 — WFM scope & phasing at engine defaults\n",
|
||
"_, _, _rollout_d = build_rollouts(sites)\n",
|
||
"_wfm_d = wfm_benefits_by_year(_rollout_d) # NA + ANZ + ASIA\n",
|
||
"assert set(_wfm_d[\"region\"]) == set(DEFAULT_WFM_REGIONS)\n",
|
||
"_approx(_wfm_d[\"benefit\"].sum(), 2_314_000) # $0 NA + $1.4M ANZ + $914K ASIA\n",
|
||
"_by_d = _wfm_d.groupby(\"year\")[\"benefit\"].sum()\n",
|
||
"_approx(_by_d[2026], 0)\n",
|
||
"_approx(_by_d[2027], 1_400_000 / 13, tol=1) # ANZ: 1 of 13 live months in 2027\n",
|
||
"_approx(wfm_annual_runrate(), 2_900_000)\n",
|
||
"_approx(runrate_saving_annual(), 5_900_000) # deck frame: no managed services\n",
|
||
"\n",
|
||
"# Contracted licence overlay (tokencalc.appendix4.TCO_CONTRACTED)\n",
|
||
"_approx(tco(\"ccaas_annual\"), 3_200_000) # signed correction\n",
|
||
"_approx(TCO_VERBATIM[\"ccaas_annual\"], 4_300_000) # deck record stays verbatim\n",
|
||
"_lc = licence_costs_by_year(annual=tco(\"ccaas_annual\"))\n",
|
||
"_approx(_lc[2026], 1_600_000) # 6-mo ramp: half-year bills in 2026\n",
|
||
"_approx(_lc[2027], 3_200_000)\n",
|
||
"_approx(runrate_saving_annual(tco(\"ccaas_annual\"),\n",
|
||
" managed_annual=MANAGED_SERVICES_ANNUAL),\n",
|
||
" 6_589_081.60, tol=0.01)\n",
|
||
"\n",
|
||
"# Section 5 — default-flow pins (pure engine defaults, no widget state)\n",
|
||
"_cur_d = current_costs_by_year(_seed_check)\n",
|
||
"_ben_d = {y: float(_by_d.get(y, 0.0)) for y in YEARS}\n",
|
||
"_tot_d = {y: _cur_d[y] + _l_deck[y] + ps_costs_by_year()[y] for y in YEARS}\n",
|
||
"_inc_d, _net_d = case_flows(_tot_d, _ben_d)\n",
|
||
"_approx(sum(_net_d.values()), -3_703_000, tol=1) # deck frame\n",
|
||
"assert runrate_breakeven_label(_net_d, runrate_saving_annual()) == \\\n",
|
||
" \"44 months (~Aug 2029, extrapolated)\"\n",
|
||
"_tot_c = {y: _cur_d[y] + _lc[y] + _ps_c[y] + _ms_c[y] for y in YEARS}\n",
|
||
"_inc_c, _net_c = case_flows(_tot_c, _ben_d)\n",
|
||
"_approx(sum(_net_c.values()), -1_503_013, tol=1) # contracted frame — still negative\n",
|
||
"assert runrate_breakeven_label(\n",
|
||
" _net_c, runrate_saving_annual(tco(\"ccaas_annual\"),\n",
|
||
" managed_annual=MANAGED_SERVICES_ANNUAL)) == \\\n",
|
||
" \"39 months (~Mar 2029, extrapolated)\"\n",
|
||
"\n",
|
||
"# Live state — stack and flows tie out at whatever the widgets say\n",
|
||
"_approx(sum(total_by_year.values()),\n",
|
||
" sum(licence_by_year.values()) + sum(ps_by_year.values())\n",
|
||
" + sum(managed_by_year.values()) + sum(current_by_year.values()))\n",
|
||
"for y in YEARS:\n",
|
||
" _approx(net_by[y], ben_by_year[y] - (total_by_year[y] - BASELINE_ANNUAL))\n",
|
||
"\n",
|
||
"backstage(\"All assertions passed.\")\n",
|
||
"backstage(f\" WFM benefits 3-yr {money(sum(ben_by_year.values()))} · \"\n",
|
||
" f\"programme cost 3-yr {money(sum(total_by_year.values()))} · \"\n",
|
||
" f\"net 3-yr {money(kpi['net_3yr'])} · breakeven {BREAKEVEN}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "67ac8561",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-8\"></a>\n",
|
||
"\n",
|
||
"## 8 · Risks, notes & next steps\n",
|
||
"\n",
|
||
"**Findings to lead with**\n",
|
||
"- **The migration alone does not stand up inside the window, even at the signed rate**:\n",
|
||
" with the full contracts in ($3.2M/yr licences, 6-month ramp, SOW milestones, managed\n",
|
||
" services) the 3-yr net is ≈ −$1.5M, breaking even ~Mar 2029 on run-rate alone; the deck\n",
|
||
" frame ($4.3M/yr, deck's PS lump) runs ≈ −$3.7M / ~Aug 2029. Keep both KPI columns in\n",
|
||
" front of CTM — the walk from the deck's own numbers is the credibility story, and the\n",
|
||
" post-termination run-rate is what carries the case. (With-AI case: the Corrected\n",
|
||
" Business Case notebook.)\n",
|
||
"- **2026 is a pure cost year** here too — PS outlay plus double-billing, zero benefits.\n",
|
||
"- The ramp credit and the term-contract run-off cut in opposite directions; both are\n",
|
||
" contract facts the deck's cost case omitted.\n",
|
||
"\n",
|
||
"**Known gaps / assumptions**\n",
|
||
"- 🟡 Licence run-rate assumes WFM/WEM SKUs (incl. new APAC seats) are inside $4.3M/yr —\n",
|
||
" if WEM is an add-on, raise the sidebar run-rate; section 6 shows the sensitivity.\n",
|
||
"- 🟡 Regional current-cost split is an agent-share allocation pending real contract data;\n",
|
||
" if NA's existing WFM tool bills separately, fold it into NA's section-1 input.\n",
|
||
"- Deck quirk kept verbatim: ASIA WFM annual ($1.6M) exceeds its 3-yr value ($914K); the\n",
|
||
" annual value feeds the run-rate extrapolation, the 3-yr value phases in-window.\n",
|
||
"- EMEA WFM ($687K 3-yr) is an upside option — sidebar toggle.\n",
|
||
"- 🟡 Training ($167K) is kept as the deck's verbatim year-1 line — the SOW milestones\n",
|
||
" don't itemize it. Managed services are modelled with no end date inside the window.\n",
|
||
"- Not modelled: early-termination fees, co-term options, migration costs beyond the SOW,\n",
|
||
" and any benefit realization lag beyond Genesys's own schedule.\n",
|
||
"\n",
|
||
"**Next steps**\n",
|
||
"- Feed real per-region contract values and termination dates into section 1.\n",
|
||
"- Confirm in the order form that WFM/WEM SKUs and the APAC seats are inside the run-rate.\n",
|
||
"- Pressure-test the base-price claim: does the $2.03M SOW really include the NA WFM\n",
|
||
" migration and two net-new WFM implementations?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b8ee762f",
|
||
"metadata": {},
|
||
"source": [
|
||
"<a id=\"section-9\"></a>\n",
|
||
"\n",
|
||
"## 9 · Machine-readable data appendix\n",
|
||
"\n",
|
||
"Every number behind the figures above, emitted as markdown tables plus a JSON block, so an\n",
|
||
"LLM can draft the client report or presentation directly from the HTML/markdown export\n",
|
||
"(`python scripts/export_report.py`). Plotly figures export as JavaScript an LLM cannot\n",
|
||
"read — this section carries the data. The dump renders **backstage** (JupyterLab and the exports) and stays hidden in the Mercury app. The tables reflect the **current** input state, so a\n",
|
||
"client-customized session exports a client-customized appendix."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"id": "32ebed57",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T15:15:49.180044Z",
|
||
"iopub.status.busy": "2026-07-08T15:15:49.179772Z",
|
||
"iopub.status.idle": "2026-07-08T15:15:49.204642Z",
|
||
"shell.execute_reply": "2026-07-08T15:15:49.203924Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"#### Current-state contracts by region (inputs)\n",
|
||
"\n",
|
||
"| region | agents | annual_cost | contract_termination | confidence |\n",
|
||
"|:---------|---------:|--------------:|:-----------------------|:------------------------------------------------|\n",
|
||
"| NA | 890 | 3,348,969 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
|
||
"| ANZ | 180 | 677,320 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
|
||
"| EMEA | 320 | 1,204,124 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
|
||
"| ASIA | 550 | 2,069,588 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
|
||
"\n",
|
||
"#### WFM scope — treatment and verbatim values ($)\n",
|
||
"\n",
|
||
"| region | WFM treatment | in scope | verbatim annual | verbatim 3-yr |\n",
|
||
"|:---------|:--------------------------|:-----------|------------------:|----------------:|\n",
|
||
"| NA | migrate existing users | ✓ | 0 | 0 |\n",
|
||
"| ANZ | implement (APAC) | ✓ | 1300000 | 1400000 |\n",
|
||
"| EMEA | implement (upside option) | — | 824000 | 687000 |\n",
|
||
"| ASIA | implement (APAC) | ✓ | 1600000 | 914000 |\n",
|
||
"\n",
|
||
"#### WFM benefits by region × year (schedule-phased, $)\n",
|
||
"\n",
|
||
"| region | 2026 | 2027 | 2028 |\n",
|
||
"|:---------|-------:|--------:|----------:|\n",
|
||
"| ANZ | 0 | 107,692 | 1,292,308 |\n",
|
||
"| ASIA | 0 | 0 | 914,000 |\n",
|
||
"| NA | 0 | 0 | 0 |\n",
|
||
"\n",
|
||
"#### Migration programme cost stack ($)\n",
|
||
"\n",
|
||
"| | 2026 | 2027 | 2028 | 3-yr |\n",
|
||
"|:----------------------------------------------------|----------:|----------:|----------:|-----------:|\n",
|
||
"| CCaaS platform licences (contracted, ramp-adjusted) | 1,600,000 | 3,200,000 | 3,200,000 | 8,000,000 |\n",
|
||
"| NTT PS + training (SOW billing milestones) | 1,179,723 | 1,012,723 | 0 | 2,192,446 |\n",
|
||
"| NTT managed services (from MCX go-live) | 102,730 | 410,918 | 410,918 | 924,566 |\n",
|
||
"| Existing platform (term-contract run-off) | 7,300,001 | 7,300,001 | 0 | 14,600,002 |\n",
|
||
"\n",
|
||
"#### Business-case flows vs do-nothing baseline ($)\n",
|
||
"\n",
|
||
"| | 2026 | 2027 | 2028 |\n",
|
||
"|:-----------------|-----------:|-----------:|-----------:|\n",
|
||
"| programme cost | 10,182,454 | 11,923,643 | 3,610,918 |\n",
|
||
"| incremental cost | 2,882,454 | 4,623,643 | -3,689,082 |\n",
|
||
"| net | -2,882,454 | -4,515,950 | 5,895,389 |\n",
|
||
"\n",
|
||
"#### KPIs — migration + WFM (no AI)\n",
|
||
"\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"| | Deck frame ($4.3M/yr, deck PS) | Contracted ($3.2M/yr + SOW) |\n",
|
||
"|:-------------------------------------|:------------------------------------|:------------------------------------|\n",
|
||
"| 3-yr WFM benefits (verbatim, scoped) | $2.3M | $2.3M |\n",
|
||
"| 3-yr incremental cost | $6.0M | $3.8M |\n",
|
||
"| 3-yr net | -$3.7M | -$1.5M |\n",
|
||
"| ROI (net ÷ incremental cost) | -62% | -39% |\n",
|
||
"| NPV @ 13.5% (deck rate) | -$3.8M | -$2.0M |\n",
|
||
"| NPV @ 8.0% (CTM treasury) | -$3.8M | -$1.9M |\n",
|
||
"| Payback (2026–28 window) | beyond 2028 | beyond 2028 |\n",
|
||
"| Steady-state run-rate saving | $5.9M/yr | $6.6M/yr |\n",
|
||
"| Breakeven | 44 months (~Aug 2029, extrapolated) | 39 months (~Mar 2029, extrapolated) |\n",
|
||
"\n",
|
||
"#### Model state (JSON)\n",
|
||
"\n",
|
||
"```json\n",
|
||
"{\n",
|
||
" \"scenario\": \"Genesys Cloud CX migration + WFM (NA migrate, APAC implement) \\u2014 no AI\",\n",
|
||
" \"wfm_benefits_by_year\": {\n",
|
||
" \"2026\": 0,\n",
|
||
" \"2027\": 107692,\n",
|
||
" \"2028\": 2206308\n",
|
||
" },\n",
|
||
" \"cost_by_year\": {\n",
|
||
" \"2026\": 10182454,\n",
|
||
" \"2027\": 11923643,\n",
|
||
" \"2028\": 3610918\n",
|
||
" },\n",
|
||
" \"net_by_year\": {\n",
|
||
" \"2026\": -2882454,\n",
|
||
" \"2027\": -4515950,\n",
|
||
" \"2028\": 5895389\n",
|
||
" },\n",
|
||
" \"kpis\": {\n",
|
||
" \"benefits_3yr\": 2314000,\n",
|
||
" \"incremental_cost_3yr\": 3817015,\n",
|
||
" \"net_3yr\": -1503015,\n",
|
||
" \"roi\": -0.3938,\n",
|
||
" \"npv\": -2013126,\n",
|
||
" \"discount_rate\": 0.135,\n",
|
||
" \"payback\": \"beyond 2028\"\n",
|
||
" },\n",
|
||
" \"kpis_at_deck_rate\": {\n",
|
||
" \"benefits_3yr\": 2314000,\n",
|
||
" \"incremental_cost_3yr\": 6017002,\n",
|
||
" \"net_3yr\": -3703002,\n",
|
||
" \"roi\": -0.6154,\n",
|
||
" \"npv\": -3849521,\n",
|
||
" \"discount_rate\": 0.135,\n",
|
||
" \"payback\": \"beyond 2028\"\n",
|
||
" },\n",
|
||
" \"runrate_saving_annual\": 6589082,\n",
|
||
" \"breakeven\": \"39 months (~Mar 2029, extrapolated)\",\n",
|
||
" \"assumptions\": {\n",
|
||
" \"ramp_months\": 6,\n",
|
||
" \"licence_annual\": 3200000,\n",
|
||
" \"deck_ccaas_annual\": 4300000,\n",
|
||
" \"ps_contracted_total\": 2025446.48,\n",
|
||
" \"ps_milestones\": [\n",
|
||
" {\n",
|
||
" \"name\": \"SOW Effective Date\",\n",
|
||
" \"date\": \"2026-03-15\",\n",
|
||
" \"amount\": 607633.94\n",
|
||
" },\n",
|
||
" {\n",
|
||
" \"name\": \"Start of client UAT (first region)\",\n",
|
||
" \"date\": \"2026-09-30\",\n",
|
||
" \"amount\": 405089.3\n",
|
||
" },\n",
|
||
" {\n",
|
||
" \"name\": \"Start of client UAT (last region)\",\n",
|
||
" \"date\": \"2027-06-30\",\n",
|
||
" \"amount\": 607633.94\n",
|
||
" },\n",
|
||
" {\n",
|
||
" \"name\": \"Completion of last go-live migration\",\n",
|
||
" \"date\": \"2027-09-30\",\n",
|
||
" \"amount\": 405089.3\n",
|
||
" }\n",
|
||
" ],\n",
|
||
" \"managed_services_annual\": 410918.4,\n",
|
||
" \"mcx_go_live\": \"2026-09-30\",\n",
|
||
" \"licence_includes_wfm_skus\": true,\n",
|
||
" \"wfm_regions\": [\n",
|
||
" \"NA\",\n",
|
||
" \"ANZ\",\n",
|
||
" \"ASIA\"\n",
|
||
" ],\n",
|
||
" \"migration_and_wfm_in_base_price\": true,\n",
|
||
" \"discount_rate\": 0.135,\n",
|
||
" \"baseline_annual\": 7300000,\n",
|
||
" \"current_state_by_region\": {\n",
|
||
" \"NA\": {\n",
|
||
" \"annual_cost\": 3348969,\n",
|
||
" \"contract_termination\": \"2027-12-31\"\n",
|
||
" },\n",
|
||
" \"ANZ\": {\n",
|
||
" \"annual_cost\": 677320,\n",
|
||
" \"contract_termination\": \"2027-12-31\"\n",
|
||
" },\n",
|
||
" \"EMEA\": {\n",
|
||
" \"annual_cost\": 1204124,\n",
|
||
" \"contract_termination\": \"2027-12-31\"\n",
|
||
" },\n",
|
||
" \"ASIA\": {\n",
|
||
" \"annual_cost\": 2069588,\n",
|
||
" \"contract_termination\": \"2027-12-31\"\n",
|
||
" }\n",
|
||
" }\n",
|
||
" }\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(\"Current-state contracts by region (inputs)\",\n",
|
||
" current_state.reset_index().drop(columns=[\"share\"]), index=False)\n",
|
||
"_section(\"WFM scope — treatment and verbatim values ($)\", wfm_scope_table)\n",
|
||
"if not wfm_long.empty:\n",
|
||
" _section(\"WFM benefits by region × year (schedule-phased, $)\",\n",
|
||
" wfm_long.pivot_table(index=\"region\", columns=\"year\",\n",
|
||
" values=\"benefit\", aggfunc=\"sum\"))\n",
|
||
"_section(\"Migration programme cost stack ($)\", migration_costs)\n",
|
||
"_section(\"Business-case flows vs do-nothing baseline ($)\",\n",
|
||
" pd.DataFrame({\"programme cost\": total_by_year,\n",
|
||
" \"incremental cost\": inc, \"net\": net_by}).T[YEARS])\n",
|
||
"_section(\"KPIs — migration + WFM (no AI)\", kpis_fmt)\n",
|
||
"\n",
|
||
"\n",
|
||
"def _jval(v):\n",
|
||
" if isinstance(v, float):\n",
|
||
" return round(v, 4) if abs(v) < 10 else round(v)\n",
|
||
" return v\n",
|
||
"\n",
|
||
"\n",
|
||
"backstage(\"\\n#### Model state (JSON)\\n\")\n",
|
||
"backstage(\"```json\")\n",
|
||
"backstage(_json.dumps({\n",
|
||
" \"scenario\": \"Genesys Cloud CX migration + WFM (NA migrate, APAC implement) — no AI\",\n",
|
||
" \"wfm_benefits_by_year\": {str(y): round(ben_by_year[y]) for y in YEARS},\n",
|
||
" \"cost_by_year\": {str(y): round(total_by_year[y]) for y in YEARS},\n",
|
||
" \"net_by_year\": {str(y): round(net_by[y]) for y in YEARS},\n",
|
||
" \"kpis\": {k: _jval(v) for k, v in kpi.items()},\n",
|
||
" \"kpis_at_deck_rate\": {k: _jval(v) for k, v in kpi_deck.items()},\n",
|
||
" \"runrate_saving_annual\": round(RUNRATE_SAVING),\n",
|
||
" \"breakeven\": BREAKEVEN,\n",
|
||
" \"assumptions\": {\n",
|
||
" \"ramp_months\": RAMP_MONTHS,\n",
|
||
" \"licence_annual\": round(LICENCE_ANNUAL),\n",
|
||
" \"deck_ccaas_annual\": round(TCO_VERBATIM[\"ccaas_annual\"]),\n",
|
||
" \"ps_contracted_total\": round(PS_CONTRACTED_TOTAL, 2),\n",
|
||
" \"ps_milestones\": [{\"name\": m[\"name\"], \"date\": m[\"date\"].isoformat(),\n",
|
||
" \"amount\": m[\"amount\"]} for m in PS_MILESTONES],\n",
|
||
" \"managed_services_annual\": MANAGED_SERVICES_ANNUAL,\n",
|
||
" \"mcx_go_live\": MCX_GO_LIVE.isoformat(),\n",
|
||
" \"licence_includes_wfm_skus\": True,\n",
|
||
" \"wfm_regions\": WFM_REGIONS,\n",
|
||
" \"migration_and_wfm_in_base_price\": True,\n",
|
||
" \"discount_rate\": DISCOUNT_RATE,\n",
|
||
" \"baseline_annual\": round(BASELINE_ANNUAL),\n",
|
||
" \"current_state_by_region\": {\n",
|
||
" r: {\"annual_cost\": round(float(current_state.loc[r, \"annual_cost\"])),\n",
|
||
" \"contract_termination\": current_state.loc[r, \"contract_termination\"].isoformat()}\n",
|
||
" for r in REGIONS},\n",
|
||
" },\n",
|
||
"}, indent=2))\n",
|
||
"backstage(\"```\")"
|
||
]
|
||
}
|
||
],
|
||
"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": {
|
||
"098f9852a68245118ba3cb197da7c299": {
|
||
"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
|
||
}
|
||
},
|
||
"0d71f3541aea48d29335b75aabae1a05": {
|
||
"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
|
||
}
|
||
},
|
||
"1d4ddc81f2c249cf9be44e32e8213b85": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
|
||
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", 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 \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 }\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",
|
||
"appearance": "toggle",
|
||
"cell_id": "",
|
||
"disabled": false,
|
||
"hidden": false,
|
||
"label": "WFM — ASIA (implement)",
|
||
"last_changed_at": "",
|
||
"layout": "IPY_MODEL_b50136dc74fb4036b657d83d7b10c5e6",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"217923ff9f174ff0b1e46c612cb7cd44": {
|
||
"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
|
||
}
|
||
},
|
||
"28eb2b2d2a2a41adaad830ac204d7b39": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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": "ANZ — current platform cost ($/yr)",
|
||
"layout": "IPY_MODEL_343cc107bc7b4369a4e654d5a2a888b1",
|
||
"layout_path": null,
|
||
"max": 8000000.0,
|
||
"min": 0.0,
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 10000.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 677320.0
|
||
}
|
||
},
|
||
"2be8dde4d1644691add8dae88a36ac81": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
|
||
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", 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 \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 }\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",
|
||
"appearance": "toggle",
|
||
"cell_id": "",
|
||
"disabled": false,
|
||
"hidden": false,
|
||
"label": "WFM — NA (migrate existing users)",
|
||
"last_changed_at": "",
|
||
"layout": "IPY_MODEL_d9bccb3d954a4ad88eca96950703a866",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"343cc107bc7b4369a4e654d5a2a888b1": {
|
||
"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
|
||
}
|
||
},
|
||
"36fd828f151b4a7ba019f601b7a2099e": {
|
||
"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
|
||
}
|
||
},
|
||
"3d24b7abdffd4cb580cf7a42348d1cd0": {
|
||
"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
|
||
}
|
||
},
|
||
"423a009e5b26436c9ccdb69e031722a4": {
|
||
"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
|
||
}
|
||
},
|
||
"4d3b2b0c854b4bcab393ef4f89aee1b4": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.date.DateInputWidget",
|
||
"_css": "\n .mljar-date-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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\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": "ANZ — contract termination",
|
||
"layout": "IPY_MODEL_3d24b7abdffd4cb580cf7a42348d1cd0",
|
||
"layout_path": null,
|
||
"max": "",
|
||
"min": "",
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "2027-12-31"
|
||
}
|
||
},
|
||
"58dab83fd1e24bd8b609d9fe3fd8420c": {
|
||
"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
|
||
}
|
||
},
|
||
"5c47680e253c4f45bbbbfcc76612499b": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
|
||
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", 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 \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 }\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",
|
||
"appearance": "toggle",
|
||
"cell_id": "",
|
||
"disabled": false,
|
||
"hidden": false,
|
||
"label": "WFM — EMEA (upside option)",
|
||
"last_changed_at": "",
|
||
"layout": "IPY_MODEL_217923ff9f174ff0b1e46c612cb7cd44",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": false
|
||
}
|
||
},
|
||
"68b411c315ff4357815d7ff9801f445a": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.date.DateInputWidget",
|
||
"_css": "\n .mljar-date-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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\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": "ASIA — contract termination",
|
||
"layout": "IPY_MODEL_36fd828f151b4a7ba019f601b7a2099e",
|
||
"layout_path": null,
|
||
"max": "",
|
||
"min": "",
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "2027-12-31"
|
||
}
|
||
},
|
||
"6bc9b75143f244bbbb3a29cdbcb95e53": {
|
||
"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
|
||
}
|
||
},
|
||
"6d231d033d7b4f41b852046f069ebc3f": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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": "NA — current platform cost ($/yr)",
|
||
"layout": "IPY_MODEL_58dab83fd1e24bd8b609d9fe3fd8420c",
|
||
"layout_path": null,
|
||
"max": 8000000.0,
|
||
"min": 0.0,
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 10000.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 3348969.0
|
||
}
|
||
},
|
||
"720f723fcedb42efa4441ea0f5e2aae2": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.select.SelectWidget",
|
||
"_css": "\n .mljar-select-container {\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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n }\n\n .mljar-select-label {\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 }\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 border-width: 2px;\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: none;\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 margin-top: 6px;\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 container.appendChild(dropdown);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\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 };\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 control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\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)) {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\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 isEditing = false;\n setOpen(false);\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 document.removeEventListener(\"click\", handleDocumentClick);\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": [
|
||
"13.5% (deck)",
|
||
"8.0% (CTM treasury)"
|
||
],
|
||
"disabled": false,
|
||
"hidden": false,
|
||
"label": "NPV discount rate",
|
||
"layout": "IPY_MODEL_0d71f3541aea48d29335b75aabae1a05",
|
||
"layout_path": null,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "13.5% (deck)"
|
||
}
|
||
},
|
||
"74d727dbe17e46cb9c8da2283a3acb50": {
|
||
"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
|
||
}
|
||
},
|
||
"800dd2c22a2c4172ae2a846f620f97d9": {
|
||
"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
|
||
}
|
||
},
|
||
"8e89c81717ff408d9e5cb1a949ab337c": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.date.DateInputWidget",
|
||
"_css": "\n .mljar-date-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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\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": "EMEA — contract termination",
|
||
"layout": "IPY_MODEL_098f9852a68245118ba3cb197da7c299",
|
||
"layout_path": null,
|
||
"max": "",
|
||
"min": "",
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "2027-12-31"
|
||
}
|
||
},
|
||
"9a1300f4e5374fa6bb1c9717866f8ce1": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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": "EMEA — current platform cost ($/yr)",
|
||
"layout": "IPY_MODEL_423a009e5b26436c9ccdb69e031722a4",
|
||
"layout_path": null,
|
||
"max": 8000000.0,
|
||
"min": 0.0,
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 10000.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 1204124.0
|
||
}
|
||
},
|
||
"b2ec5338e0ba4055b77a57edd08b464c": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.date.DateInputWidget",
|
||
"_css": "\n .mljar-date-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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\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": "NA — contract termination",
|
||
"layout": "IPY_MODEL_e7597a4eb1c14475a440ac83ee50ce1c",
|
||
"layout_path": null,
|
||
"max": "",
|
||
"min": "",
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "2027-12-31"
|
||
}
|
||
},
|
||
"b50136dc74fb4036b657d83d7b10c5e6": {
|
||
"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
|
||
}
|
||
},
|
||
"cc5b28489de346d3b59e3ace05fb2ebb": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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 licence run-rate ($/yr)",
|
||
"layout": "IPY_MODEL_800dd2c22a2c4172ae2a846f620f97d9",
|
||
"layout_path": null,
|
||
"max": 10000000.0,
|
||
"min": 0.0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 100000.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 3200000.0
|
||
}
|
||
},
|
||
"cddbd4fd3deb4ed1a29dad5cf082939b": {
|
||
"model_module": "anywidget",
|
||
"model_module_version": "~0.11.*",
|
||
"model_name": "AnyModel",
|
||
"state": {
|
||
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
|
||
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
|
||
"_dom_classes": [],
|
||
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", 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 \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 }\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",
|
||
"appearance": "toggle",
|
||
"cell_id": "",
|
||
"disabled": false,
|
||
"hidden": false,
|
||
"label": "WFM — ANZ (implement)",
|
||
"last_changed_at": "",
|
||
"layout": "IPY_MODEL_de9d84197e314233938eb4ad7b19ad0b",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"cdf8fe1626a54e7eb048e9e8d27cb2a4": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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": "ASIA — current platform cost ($/yr)",
|
||
"layout": "IPY_MODEL_74d727dbe17e46cb9c8da2283a3acb50",
|
||
"layout_path": null,
|
||
"max": 8000000.0,
|
||
"min": 0.0,
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 10000.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 2069588.0
|
||
}
|
||
},
|
||
"cf504293e15c4cd99f1a492d73d8e263": {
|
||
"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_6bc9b75143f244bbbb3a29cdbcb95e53",
|
||
"layout_path": null,
|
||
"placeholder": "",
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"style": "IPY_MODEL_f1dd0e56a7224aafa77c1c3c23e9fdb0",
|
||
"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'});\">Current state & contracts</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'});\">Scenario scope & licences</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'});\">WFM 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'});\">Cost stack</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'});\">Sensitivity</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'});\">Risks & notes</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-9');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('9 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
|
||
}
|
||
},
|
||
"d9afd02316174736b31a7964fe071605": {
|
||
"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
|
||
}
|
||
},
|
||
"d9bccb3d954a4ad88eca96950703a866": {
|
||
"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
|
||
}
|
||
},
|
||
"ddacc3befdc54c278abccd4a5b6b98b5": {
|
||
"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 margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\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 border-width: 2px;\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 ramp — licence-free months",
|
||
"layout": "IPY_MODEL_d9afd02316174736b31a7964fe071605",
|
||
"layout_path": null,
|
||
"max": 24.0,
|
||
"min": 0.0,
|
||
"position": "inline",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"step": 1.0,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": 6.0
|
||
}
|
||
},
|
||
"de9d84197e314233938eb4ad7b19ad0b": {
|
||
"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
|
||
}
|
||
},
|
||
"e7597a4eb1c14475a440ac83ee50ce1c": {
|
||
"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
|
||
}
|
||
},
|
||
"f1dd0e56a7224aafa77c1c3c23e9fdb0": {
|
||
"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
|
||
}
|
||
}
|
||
},
|
||
"version_major": 2,
|
||
"version_minor": 0
|
||
}
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|