7392 lines
328 KiB
Plaintext
7392 lines
328 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** — the verbatim $2.4M professional services +\n",
|
||
"$167K training. The only 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 · 12-month ramp credit |\n",
|
||
"| Base PS + training | Same deck | Verbatim $2.4M + $167K, year 1 — **includes migration + WFM** |\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-08T12:20:32.301747Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.301520Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.712911Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.712211Z"
|
||
}
|
||
},
|
||
"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",
|
||
" money, html_money, month_label, ps_costs_by_year,\n",
|
||
" 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",
|
||
" \"Base PS + training (incl. migration + WFM)\": \"#1baf7a\",\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-08T12:20:32.715665Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.715313Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.728544Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.727683Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
|
||
"position": "sidebar",
|
||
"widget": "MarkdownWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
|
||
"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 = 12`: licences are free for the first 12 months,\n",
|
||
" so the licence run-rate bills from month 13."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "f8863db7",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T12:20:32.730401Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.730193Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.768973Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.768081Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x783236e37e00>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "27e2171a593043338205c5b352f20f3d",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "27e2171a593043338205c5b352f20f3d",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7831ee7ffc50>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "60a9596904a34374bc605974ac3a3235",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "60a9596904a34374bc605974ac3a3235",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x783236e37cb0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x783236c016d0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x783236c01e50>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "bae439b7a2d04ab29390996726534753",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "bae439b7a2d04ab29390996726534753",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7831ee84afd0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x783236c02210>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "f8196ba7d89c487eb23ad614f78881fe",
|
||
"position": "inline",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "f8196ba7d89c487eb23ad614f78881fe",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7831ee84b490>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
|
||
"position": "inline",
|
||
"widget": "DateInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.date.DateInputWidget object at 0x7831ee84b5c0>"
|
||
]
|
||
},
|
||
"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-08T12:20:32.771129Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.770911Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.786705Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.786022Z"
|
||
}
|
||
},
|
||
"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**: the verbatim $2.4M PS + $167K\n",
|
||
" training in year 1. No extra cost lines for migration or WFM.\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-08T12:20:32.788467Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.788266Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.804322Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.803665Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
|
||
"position": "sidebar",
|
||
"widget": "NumberInputWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.number.NumberInputWidget object at 0x7831ee6969f0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x7831ee876ba0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "caae1a34df0945249b1a3d39320540b1",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "caae1a34df0945249b1a3d39320540b1",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x783236c02850>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x783236c02710>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
|
||
"position": "sidebar",
|
||
"widget": "CheckboxWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.checkbox.CheckboxWidget object at 0x7831ee84be10>"
|
||
]
|
||
},
|
||
"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-08T12:20:32.806129Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.805883Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.821726Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.820984Z"
|
||
}
|
||
},
|
||
"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 12 months\n",
|
||
"ramp-adjusted licences by year: {2026: '$0K', 2027: '$3.2M', 2028: '$3.2M'}\n",
|
||
"base PS + training (incl. migration + WFM): $2.6M, year 1 only\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() # verbatim $2.4M PS + $167K training, year 1\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\"base PS + training (incl. migration + WFM): {money(ps_by_year[2026])}, year 1 only\")\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-08T12:20:32.823759Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.823547Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:32.868665Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:32.867804Z"
|
||
}
|
||
},
|
||
"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-08T12:20:32.871525Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:32.871295Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.302766Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.302073Z"
|
||
}
|
||
},
|
||
"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, base PS, double-billing\n",
|
||
"\n",
|
||
"Three lines only. 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",
|
||
"two contract mechanics it ignored: the **ramp credit** (licences bill from month 13)\n",
|
||
"and the **term-contract run-off** (double-billing until 31 Dec 2027)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"id": "ccde902f",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T12:20:34.305733Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.305440Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.317837Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.317271Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Migration programme cost: {2026: '$9.9M', 2027: '$10.5M', 2028: '$3.2M'} → 3-yr $23.6M\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>0</td>\n",
|
||
" <td>3,200,000</td>\n",
|
||
" <td>3,200,000</td>\n",
|
||
" <td>6,400,000</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Base PS + training (incl. migration + WFM)</th>\n",
|
||
" <td>2,567,000</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>2,567,000</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>9,867,001</td>\n",
|
||
" <td>10,500,001</td>\n",
|
||
" <td>3,200,000</td>\n",
|
||
" <td>23,567,002</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" 2026 2027 \\\n",
|
||
"CCaaS platform licences (contracted, ramp-adjus... 0 3,200,000 \n",
|
||
"Base PS + training (incl. migration + WFM) 2,567,000 0 \n",
|
||
"Existing platform (term-contract run-off) 7,300,001 7,300,001 \n",
|
||
"TOTAL 9,867,001 10,500,001 \n",
|
||
"\n",
|
||
" 2028 3-yr \n",
|
||
"CCaaS platform licences (contracted, ramp-adjus... 3,200,000 6,400,000 \n",
|
||
"Base PS + training (incl. migration + WFM) 0 2,567,000 \n",
|
||
"Existing platform (term-contract run-off) 0 14,600,002 \n",
|
||
"TOTAL 3,200,000 23,567,002 "
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"migration_costs = pd.DataFrame({\n",
|
||
" \"CCaaS platform licences (contracted, ramp-adjusted)\": licence_by_year,\n",
|
||
" \"Base PS + training (incl. migration + WFM)\": ps_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\"])]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "c058e795",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T12:20:34.320002Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.319861Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.345827Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.345186Z"
|
||
}
|
||
},
|
||
"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": [
|
||
0.0,
|
||
3200000.0,
|
||
3200000.0
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"marker": {
|
||
"color": "#1baf7a",
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
}
|
||
},
|
||
"name": "Base PS + training (incl. migration + WFM)",
|
||
"type": "bar",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": [
|
||
2567000.0,
|
||
0.0,
|
||
0.0
|
||
]
|
||
},
|
||
{
|
||
"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": "AAAAIN/RYkEAAACgaWxzQQAAAKCpeXZB",
|
||
"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>$9.9M</b>",
|
||
"x": 0,
|
||
"y": 9867001.0,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$10.5M</b>",
|
||
"x": 1,
|
||
"y": 10500001.0,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$3.2M</b>",
|
||
"x": 2,
|
||
"y": 3200000.0,
|
||
"yshift": 12
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#52514e",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "3-yr <b>$23.6M</b> — $1.7M above doing nothing",
|
||
"x": 2,
|
||
"xshift": -70,
|
||
"y": 23567002.0,
|
||
"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 12 months, then bill at the run-rate</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 12 months, then bill at the run-rate\", 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",
|
||
"At the **contracted licence rate ($3.2M/yr)** the case turns positive inside the deck's\n",
|
||
"own 2026–2028 window, with payback in late 2028. At the **deck's pitched $4.3M/yr** the\n",
|
||
"same case stays net-negative through 2028 — the KPI table shows both columns, so the walk\n",
|
||
"from the Genesys report to the contracted reality is explicit. Where a case doesn't pay\n",
|
||
"back in-window, the **run-rate breakeven** KPI extrapolates: steady state =\n",
|
||
"(baseline − licence run-rate) + scoped WFM annual values.\n",
|
||
"\n",
|
||
"*Not modelled: early-termination fees, and migration costs beyond the base PS line.*"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"id": "d8293152",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T12:20:34.347730Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.347609Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.352044Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.351451Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/mercury+json": {
|
||
"model_id": "bb915be640904627a7e37f0597028fab",
|
||
"position": "sidebar",
|
||
"widget": "SelectWidget"
|
||
},
|
||
"application/vnd.jupyter.widget-view+json": {
|
||
"model_id": "bb915be640904627a7e37f0597028fab",
|
||
"version_major": 2,
|
||
"version_minor": 1
|
||
},
|
||
"text/plain": [
|
||
"<mercury.select.SelectWidget object at 0x7831aba912b0>"
|
||
]
|
||
},
|
||
"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-08T12:20:34.353609Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.353505Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.357689Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.357172Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"net by year: {2026: '-$2.6M', 2027: '-$3.1M', 2028: '$6.3M'} → 3-yr $647K\n",
|
||
"steady-state run-rate saving $7.0M/yr ($4.1M licences + $2.9M WFM annuals) → breakeven 35 months (~Nov 2028)\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",
|
||
"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(wfm_annual_runrate(WFM_REGIONS))} WFM annuals) → breakeven {BREAKEVEN}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "9d135a19",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-08T12:20:34.359533Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.359414Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.381700Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.381025Z"
|
||
}
|
||
},
|
||
"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": [
|
||
-2567001.0,
|
||
-3200001.0,
|
||
4100000.0
|
||
]
|
||
},
|
||
{
|
||
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
|
||
"line": {
|
||
"color": "#52514e",
|
||
"width": 2
|
||
},
|
||
"marker": {
|
||
"line": {
|
||
"color": "#fcfcfb",
|
||
"width": 2
|
||
},
|
||
"size": 8
|
||
},
|
||
"mode": "lines+markers",
|
||
"name": "Cumulative net",
|
||
"type": "scatter",
|
||
"x": [
|
||
"2026",
|
||
"2027",
|
||
"2028"
|
||
],
|
||
"y": {
|
||
"bdata": "AAAAgKyVQ8HsxE5sq5ZVwQAAAACsviNB",
|
||
"dtype": "f8"
|
||
}
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$2.6M</b>",
|
||
"x": 0,
|
||
"y": -2567001.0,
|
||
"yshift": -14
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$5.7M</b>",
|
||
"x": 1,
|
||
"y": -5659309.692307692,
|
||
"yshift": -14
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$647K</b>",
|
||
"x": 2,
|
||
"y": 646998.0,
|
||
"yshift": 14
|
||
},
|
||
{
|
||
"align": "left",
|
||
"bgcolor": "#fcfcfb",
|
||
"bordercolor": "#e1e0d9",
|
||
"borderwidth": 1,
|
||
"font": {
|
||
"color": "#52514e",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "3-yr net <b>$647K</b> · ROI <b>39%</b><br>NPV@13.5% <b>-$349K</b> · payback <b>35 months (~Nov 2028)</b><br>run-rate <b>$7.0M/yr</b> post-window → breakeven <b>35 months (~Nov 2028)</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-08T12:20:34.384035Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.383904Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.393837Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.393300Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"The licence correction moves the 3-yr case by $2.2M vs the deck's pitched rate. 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 rate ($4.3M/yr)</th>\n",
|
||
" <th>Contracted ($3.2M/yr)</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>$3.9M</td>\n",
|
||
" <td>$1.7M</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3-yr net</th>\n",
|
||
" <td>-$1.6M</td>\n",
|
||
" <td>$647K</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>ROI (net ÷ incremental cost)</th>\n",
|
||
" <td>-40%</td>\n",
|
||
" <td>39%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV @ 13.5% (deck rate)</th>\n",
|
||
" <td>-$2.0M</td>\n",
|
||
" <td>-$349K</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>NPV @ 8.0% (CTM treasury)</th>\n",
|
||
" <td>-$1.8M</td>\n",
|
||
" <td>-$22K</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Payback (2026–28 window)</th>\n",
|
||
" <td>beyond 2028</td>\n",
|
||
" <td>35 months (~Nov 2028)</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Steady-state run-rate saving</th>\n",
|
||
" <td>$5.9M/yr</td>\n",
|
||
" <td>$7.0M/yr</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Breakeven</th>\n",
|
||
" <td>40 months (~Apr 2029, extrapolated)</td>\n",
|
||
" <td>35 months (~Nov 2028)</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Deck rate ($4.3M/yr) \\\n",
|
||
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
|
||
"3-yr incremental cost $3.9M \n",
|
||
"3-yr net -$1.6M \n",
|
||
"ROI (net ÷ incremental cost) -40% \n",
|
||
"NPV @ 13.5% (deck rate) -$2.0M \n",
|
||
"NPV @ 8.0% (CTM treasury) -$1.8M \n",
|
||
"Payback (2026–28 window) beyond 2028 \n",
|
||
"Steady-state run-rate saving $5.9M/yr \n",
|
||
"Breakeven 40 months (~Apr 2029, extrapolated) \n",
|
||
"\n",
|
||
" Contracted ($3.2M/yr) \n",
|
||
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
|
||
"3-yr incremental cost $1.7M \n",
|
||
"3-yr net $647K \n",
|
||
"ROI (net ÷ incremental cost) 39% \n",
|
||
"NPV @ 13.5% (deck rate) -$349K \n",
|
||
"NPV @ 8.0% (CTM treasury) -$22K \n",
|
||
"Payback (2026–28 window) 35 months (~Nov 2028) \n",
|
||
"Steady-state run-rate saving $7.0M/yr \n",
|
||
"Breakeven 35 months (~Nov 2028) "
|
||
]
|
||
},
|
||
"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 same case at the deck's pitched rate.\n",
|
||
"_lic_deck = licence_costs_by_year(RAMP_MONTHS, TCO_VERBATIM[\"ccaas_annual\"])\n",
|
||
"total_deck = {y: current_by_year[y] + _lic_deck[y] + ps_by_year[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 rate ({money(TCO_VERBATIM['ccaas_annual'])}/yr)\":\n",
|
||
" kpi_display(kpi_deck, net_deck, RUNRATE_DECK, BREAKEVEN_DECK),\n",
|
||
" f\"Contracted ({money(LICENCE_ANNUAL)}/yr)\":\n",
|
||
" kpi_display(kpi, net_by, RUNRATE_SAVING, BREAKEVEN),\n",
|
||
"})\n",
|
||
"print(f\"The licence correction moves the 3-yr case by \"\n",
|
||
" f\"{money(kpi['net_3yr'] - kpi_deck['net_3yr'])} vs the deck's pitched rate. \"\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-08T12:20:34.395983Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.395834Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.432996Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.432388Z"
|
||
}
|
||
},
|
||
"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": [
|
||
[
|
||
-2553002.0000000005,
|
||
-4353002.0,
|
||
-5853002.0
|
||
],
|
||
[
|
||
-953002.0000000005,
|
||
-2453002.0000000005,
|
||
-3703002.0000000005
|
||
],
|
||
[
|
||
646997.9999999995,
|
||
-553002.0000000005,
|
||
-1553002.0000000005
|
||
],
|
||
[
|
||
2246998.0,
|
||
1346997.9999999998,
|
||
596997.9999999998
|
||
],
|
||
[
|
||
3846997.9999999995,
|
||
3246997.9999999995,
|
||
2746997.9999999995
|
||
]
|
||
],
|
||
"zmid": 0
|
||
}
|
||
],
|
||
"layout": {
|
||
"annotations": [
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$2.6M</b>",
|
||
"x": 0,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$4.4M</b>",
|
||
"x": 1,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$5.9M</b>",
|
||
"x": 2,
|
||
"y": 0
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$953K</b>",
|
||
"x": 0,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$2.5M</b>",
|
||
"x": 1,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$3.7M</b>",
|
||
"x": 2,
|
||
"y": 1
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$647K</b>",
|
||
"x": 0,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$553K</b>",
|
||
"x": 1,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>-$1.6M</b>",
|
||
"x": 2,
|
||
"y": 2
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$2.2M</b>",
|
||
"x": 0,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$1.3M</b>",
|
||
"x": 1,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$597K</b>",
|
||
"x": 2,
|
||
"y": 3
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#ffffff",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$3.8M</b>",
|
||
"x": 0,
|
||
"y": 4
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$3.2M</b>",
|
||
"x": 1,
|
||
"y": 4
|
||
},
|
||
{
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"size": 12
|
||
},
|
||
"showarrow": false,
|
||
"text": "<b>$2.7M</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 · 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 (12-mo ramp × $3.2M/yr): $647K · deck corner (12-mo × $4.3M/yr): -$1.6M — the licence correction is what pulls the window positive.\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] 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 · WFM scope and contract dates held at \"\n",
|
||
" \"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 corner \"\n",
|
||
" f\"({DEFAULT_RAMP_MONTHS}-mo × {money(TCO_VERBATIM['ccaas_annual'])}/yr): \"\n",
|
||
" f\"{money(net_3yr(DEFAULT_RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} — \"\n",
|
||
" f\"the licence correction is what pulls the window positive.\")"
|
||
]
|
||
},
|
||
{
|
||
"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-08T12:20:34.435111Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.434996Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.448521Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.447923Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"All assertions passed.\n",
|
||
" WFM benefits 3-yr $2.3M · programme cost 3-yr $23.6M · net 3-yr $647K · breakeven 35 months (~Nov 2028)\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",
|
||
"_l12 = licence_costs_by_year(12)\n",
|
||
"_approx(_l12[2026], 0)\n",
|
||
"_approx(_l12[2027], 4_300_000)\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",
|
||
"# 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)\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(12, tco(\"ccaas_annual\"))\n",
|
||
"_approx(_lc[2027], 3_200_000)\n",
|
||
"_approx(runrate_saving_annual(tco(\"ccaas_annual\")), 7_000_000)\n",
|
||
"\n",
|
||
"# Section 5 — default-flow pins (pure engine defaults, no widget state)\n",
|
||
"_cur_d = current_costs_by_year(_seed_check)\n",
|
||
"_ps_d = ps_costs_by_year()\n",
|
||
"_tot_d = {y: _cur_d[y] + _l12[y] + _ps_d[y] for y in YEARS}\n",
|
||
"_ben_d = {y: float(_by_d.get(y, 0.0)) for y in YEARS}\n",
|
||
"_inc_d, _net_d = case_flows(_tot_d, _ben_d)\n",
|
||
"_approx(sum(_net_d.values()), -1_553_000, tol=1) # $2.314M WFM − $3.867M incremental\n",
|
||
"assert runrate_breakeven_label(_net_d, runrate_saving_annual()) == \\\n",
|
||
" \"40 months (~Apr 2029, extrapolated)\"\n",
|
||
"_tot_c = {y: _cur_d[y] + _lc[y] + _ps_d[y] for y in YEARS}\n",
|
||
"_inc_c, _net_c = case_flows(_tot_c, _ben_d)\n",
|
||
"_approx(sum(_net_c.values()), 647_000, tol=1) # contracted rate → positive in-window\n",
|
||
"assert runrate_breakeven_label(_net_c, runrate_saving_annual(tco(\"ccaas_annual\"))) == \\\n",
|
||
" \"35 months (~Nov 2028)\"\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()) + ps_by_year[2026] + 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}\")"
|
||
]
|
||
},
|
||
{
|
||
"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 contracted licence rate is what makes the migration stand on its own**: at the\n",
|
||
" signed $3.2M/yr the case is positive in-window (payback ~Nov 2028); at the deck's\n",
|
||
" pitched $4.3M/yr it would run net −$1.6M through 2028, breaking even ~April 2029 on\n",
|
||
" run-rate alone. Keep both KPI columns in front of CTM — the walk from the deck's own\n",
|
||
" numbers is the credibility story. (With-AI case: the Corrected 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",
|
||
"- Not modelled: early-termination fees, co-term options, migration costs beyond the base\n",
|
||
" PS line, 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 $2.4M PS really include the NA WFM migration\n",
|
||
" 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-08T12:20:34.450522Z",
|
||
"iopub.status.busy": "2026-07-08T12:20:34.450397Z",
|
||
"iopub.status.idle": "2026-07-08T12:20:34.473012Z",
|
||
"shell.execute_reply": "2026-07-08T12:20:34.472367Z"
|
||
}
|
||
},
|
||
"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) | 0 | 3,200,000 | 3,200,000 | 6,400,000 |\n",
|
||
"| Base PS + training (incl. migration + WFM) | 2,567,000 | 0 | 0 | 2,567,000 |\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 | 9,867,001 | 10,500,001 | 3,200,000 |\n",
|
||
"| incremental cost | 2,567,001 | 3,200,001 | -4,100,000 |\n",
|
||
"| net | -2,567,001 | -3,092,309 | 6,306,308 |\n",
|
||
"\n",
|
||
"#### KPIs — migration + WFM (no AI)\n",
|
||
"\n",
|
||
"| | Deck rate ($4.3M/yr) | Contracted ($3.2M/yr) |\n",
|
||
"|:-------------------------------------|:------------------------------------|:------------------------|\n",
|
||
"| 3-yr WFM benefits (verbatim, scoped) | $2.3M | $2.3M |\n",
|
||
"| 3-yr incremental cost | $3.9M | $1.7M |\n",
|
||
"| 3-yr net | -$1.6M | $647K |\n",
|
||
"| ROI (net ÷ incremental cost) | -40% | 39% |\n",
|
||
"| NPV @ 13.5% (deck rate) | -$2.0M | -$349K |\n",
|
||
"| NPV @ 8.0% (CTM treasury) | -$1.8M | -$22K |\n",
|
||
"| Payback (2026–28 window) | beyond 2028 | 35 months (~Nov 2028) |\n",
|
||
"| Steady-state run-rate saving | $5.9M/yr | $7.0M/yr |\n",
|
||
"| Breakeven | 40 months (~Apr 2029, extrapolated) | 35 months (~Nov 2028) |\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\": 9867001,\n",
|
||
" \"2027\": 10500001,\n",
|
||
" \"2028\": 3200000\n",
|
||
" },\n",
|
||
" \"net_by_year\": {\n",
|
||
" \"2026\": -2567001,\n",
|
||
" \"2027\": -3092309,\n",
|
||
" \"2028\": 6306308\n",
|
||
" },\n",
|
||
" \"kpis\": {\n",
|
||
" \"benefits_3yr\": 2314000,\n",
|
||
" \"incremental_cost_3yr\": 1667002,\n",
|
||
" \"net_3yr\": 646998,\n",
|
||
" \"roi\": 0.3881,\n",
|
||
" \"npv\": -349036,\n",
|
||
" \"discount_rate\": 0.135,\n",
|
||
" \"payback\": \"35 months (~Nov 2028)\"\n",
|
||
" },\n",
|
||
" \"kpis_at_deck_rate\": {\n",
|
||
" \"benefits_3yr\": 2314000,\n",
|
||
" \"incremental_cost_3yr\": 3867002,\n",
|
||
" \"net_3yr\": -1553002,\n",
|
||
" \"roi\": -0.4016,\n",
|
||
" \"npv\": -1955248,\n",
|
||
" \"discount_rate\": 0.135,\n",
|
||
" \"payback\": \"beyond 2028\"\n",
|
||
" },\n",
|
||
" \"runrate_saving_annual\": 7000000,\n",
|
||
" \"breakeven\": \"35 months (~Nov 2028)\",\n",
|
||
" \"assumptions\": {\n",
|
||
" \"ramp_months\": 12,\n",
|
||
" \"licence_annual\": 3200000,\n",
|
||
" \"deck_ccaas_annual\": 4300000,\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",
|
||
" \"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": {
|
||
"12974d31f88c40d99967fc5d44b9e2a4": {
|
||
"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
|
||
}
|
||
},
|
||
"1beaba7af87e4528abd06da3151e882f": {
|
||
"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
|
||
}
|
||
},
|
||
"1c591bdaa1e44a55be8d24db8956b3d1": {
|
||
"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_9d4f1c2f10084d87bbfb422c3247f7e1",
|
||
"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
|
||
}
|
||
},
|
||
"27e2171a593043338205c5b352f20f3d": {
|
||
"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_7c01b290a6594aa69680bdd3a4e74995",
|
||
"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
|
||
}
|
||
},
|
||
"2a43f7bc543d446ead3eeaaadbbf3cea": {
|
||
"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_45fdc31c02434d799c78a20846c791dc",
|
||
"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"
|
||
}
|
||
},
|
||
"2bf25ab141c646aeae3b8e9b80b6762d": {
|
||
"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_92aaf404ceee451f84aeb8f3252558e0",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"2f3ac4164f5a4454ba631d0df39e5d87": {
|
||
"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_9a295ac0af8348ebb661f2a0a9be8d17",
|
||
"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"
|
||
}
|
||
},
|
||
"3208166b83d6414c93131c6d9a6b1dac": {
|
||
"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
|
||
}
|
||
},
|
||
"3446e8dd427b4abe8d361ec75bb0a9fa": {
|
||
"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_f9673c814e8148b1be991bcf12f12fff",
|
||
"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": 12.0
|
||
}
|
||
},
|
||
"3b92df6e04544844a57103b779776678": {
|
||
"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
|
||
}
|
||
},
|
||
"4275d017a33a4f7d87a8ba63fca41b03": {
|
||
"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
|
||
}
|
||
},
|
||
"45fdc31c02434d799c78a20846c791dc": {
|
||
"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
|
||
}
|
||
},
|
||
"60a9596904a34374bc605974ac3a3235": {
|
||
"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_ed3779c8295a44339d7441a254909d33",
|
||
"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"
|
||
}
|
||
},
|
||
"6b1f795e977941baa1ca60f5f93bb458": {
|
||
"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
|
||
}
|
||
},
|
||
"6bf6331dbeea4a2e932d9ba01fd28557": {
|
||
"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_3b92df6e04544844a57103b779776678",
|
||
"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
|
||
}
|
||
},
|
||
"78688c06ca0b4ee19c1a0b81f18d628a": {
|
||
"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_82f7314d49cb445481ab1f5a82fd31d6",
|
||
"layout_path": null,
|
||
"placeholder": "",
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"style": "IPY_MODEL_4275d017a33a4f7d87a8ba63fca41b03",
|
||
"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>"
|
||
}
|
||
},
|
||
"7c01b290a6594aa69680bdd3a4e74995": {
|
||
"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
|
||
}
|
||
},
|
||
"82f7314d49cb445481ab1f5a82fd31d6": {
|
||
"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
|
||
}
|
||
},
|
||
"8b8b8d01f0ca46f39c16bd16b93a1475": {
|
||
"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_6b1f795e977941baa1ca60f5f93bb458",
|
||
"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"
|
||
}
|
||
},
|
||
"92aaf404ceee451f84aeb8f3252558e0": {
|
||
"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
|
||
}
|
||
},
|
||
"9a295ac0af8348ebb661f2a0a9be8d17": {
|
||
"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
|
||
}
|
||
},
|
||
"9d4f1c2f10084d87bbfb422c3247f7e1": {
|
||
"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
|
||
}
|
||
},
|
||
"9ee437efc7884fe9a1f870a0aead3e90": {
|
||
"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
|
||
}
|
||
},
|
||
"9f85cf3f71b842ba83ed70bd17ea748f": {
|
||
"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_3208166b83d6414c93131c6d9a6b1dac",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": false
|
||
}
|
||
},
|
||
"a3797b4763c8482f82c23bea6b27de8f": {
|
||
"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
|
||
}
|
||
},
|
||
"bae439b7a2d04ab29390996726534753": {
|
||
"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_12974d31f88c40d99967fc5d44b9e2a4",
|
||
"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
|
||
}
|
||
},
|
||
"bb915be640904627a7e37f0597028fab": {
|
||
"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_9ee437efc7884fe9a1f870a0aead3e90",
|
||
"layout_path": null,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": "13.5% (deck)"
|
||
}
|
||
},
|
||
"c76324ffcf3e4df5a078d6b3fb6ac13c": {
|
||
"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
|
||
}
|
||
},
|
||
"caae1a34df0945249b1a3d39320540b1": {
|
||
"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_c76324ffcf3e4df5a078d6b3fb6ac13c",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"ed3779c8295a44339d7441a254909d33": {
|
||
"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
|
||
}
|
||
},
|
||
"f7b01a3662ce4ee4a8396fa5df4f048f": {
|
||
"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_a3797b4763c8482f82c23bea6b27de8f",
|
||
"layout_path": null,
|
||
"n_toggles": 0,
|
||
"position": "sidebar",
|
||
"render_slot_id": null,
|
||
"source_cell_id": null,
|
||
"tabbable": null,
|
||
"tooltip": null,
|
||
"url_key": "",
|
||
"value": true
|
||
}
|
||
},
|
||
"f8196ba7d89c487eb23ad614f78881fe": {
|
||
"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_1beaba7af87e4528abd06da3151e882f",
|
||
"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
|
||
}
|
||
},
|
||
"f9673c814e8148b1be991bcf12f12fff": {
|
||
"model_module": "@jupyter-widgets/base",
|
||
"model_module_version": "2.0.0",
|
||
"model_name": "LayoutModel",
|
||
"state": {
|
||
"_model_module": "@jupyter-widgets/base",
|
||
"_model_module_version": "2.0.0",
|
||
"_model_name": "LayoutModel",
|
||
"_view_count": null,
|
||
"_view_module": "@jupyter-widgets/base",
|
||
"_view_module_version": "2.0.0",
|
||
"_view_name": "LayoutView",
|
||
"align_content": null,
|
||
"align_items": null,
|
||
"align_self": null,
|
||
"border_bottom": null,
|
||
"border_left": null,
|
||
"border_right": null,
|
||
"border_top": null,
|
||
"bottom": null,
|
||
"display": null,
|
||
"flex": null,
|
||
"flex_flow": null,
|
||
"grid_area": null,
|
||
"grid_auto_columns": null,
|
||
"grid_auto_flow": null,
|
||
"grid_auto_rows": null,
|
||
"grid_column": null,
|
||
"grid_gap": null,
|
||
"grid_row": null,
|
||
"grid_template_areas": null,
|
||
"grid_template_columns": null,
|
||
"grid_template_rows": null,
|
||
"height": null,
|
||
"justify_content": null,
|
||
"justify_items": null,
|
||
"left": null,
|
||
"margin": null,
|
||
"max_height": null,
|
||
"max_width": null,
|
||
"min_height": null,
|
||
"min_width": null,
|
||
"object_fit": null,
|
||
"object_position": null,
|
||
"order": null,
|
||
"overflow": null,
|
||
"padding": null,
|
||
"right": null,
|
||
"top": null,
|
||
"visibility": null,
|
||
"width": null
|
||
}
|
||
}
|
||
},
|
||
"version_major": 2,
|
||
"version_minor": 0
|
||
}
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|