<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM Engagement & Attribution — v5 Prototype</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@300;400;500;600;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root {
--bg-0: #0E1116;
--bg-1: #161A22;
--bg-2: #1E232D;
--bg-3: #262C38;
--border: #2A3140;
--border-strong: #3A4358;
--t-1: #F2EDE3;
--t-2: #ADB1BC;
--t-3: #6A6F7C;
--t-4: #454953;
--acc-sage: #6FAE94;
--acc-sage-d: #4A8770;
--acc-burnt: #D87544;
--acc-burnt-d: #B05B33;
--acc-blue: #6B8FB5;
--acc-violet: #8B7BB8;
--acc-rust: #B8654D;
--acc-cream: #D8C9A8;
--p-ssisa: #6B8FB5;
--p-cia: #6FAE94;
--p-psa: #D8C9A8;
--p-eas: #D87544;
--p-cisa: #8B7BB8;
--p-gia: #B8654D;
--p-smisa: #C49B6C;
--p-multi: #5A6F87;
--p-sipp: #4A5266;
--grid: rgba(255,255,255,0.04);
--tick: #6A6F7C;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--bg-0); color: var(--t-1);
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 400; font-size: 14px; line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.topbar {
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, #11151C 0%, #0E1116 100%);
padding: 20px 36px;
display: flex; justify-content: space-between; align-items: center; gap: 24px;
}
.topbar h1 {
font-family: 'IBM Plex Serif', serif; font-weight: 400; font-size: 22px;
margin: 0; letter-spacing: -0.01em;
}
.topbar h1 .em { color: var(--acc-cream); font-style: italic; font-weight: 300; }
.topbar .meta {
font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--t-3);
text-transform: uppercase; letter-spacing: 0.08em; text-align: right; line-height: 1.7;
}
.topbar .meta .live::before {
content: '●'; color: var(--acc-sage); margin-right: 6px; animation: pulse 2.4s infinite;
}
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
.tabs {
display: flex; gap: 0; padding: 0 36px;
background: var(--bg-0); border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 10;
}
.tab {
padding: 16px 22px; cursor: pointer;
font-family: 'IBM Plex Mono', monospace; font-size: 11px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--t-3); border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s; user-select: none;
}
.tab:hover { color: var(--t-1); }
.tab.active { color: var(--t-1); border-bottom-color: var(--acc-cream); }
.tab .idx { color: var(--t-4); margin-right: 8px; }
.tab.active .idx { color: var(--acc-cream); }
.month-filter {
padding: 18px 36px; background: var(--bg-1);
border-bottom: 1px solid var(--border);
display: flex; gap: 8px; align-items: center;
}
.month-filter .label {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--t-3); margin-right: 12px;
}
.month-chip {
padding: 6px 14px; border: 1px solid var(--border); background: transparent;
color: var(--t-2); font-family: 'IBM Plex Mono', monospace; font-size: 11px;
text-transform: uppercase; letter-spacing: 0.05em; cursor: pointer; transition: all 0.15s;
}
.month-chip:hover { color: var(--t-1); border-color: var(--border-strong); }
.month-chip.active {
background: var(--acc-cream); color: var(--bg-0); border-color: var(--acc-cream);
}
.month-chip.partial::after { content: ' †'; color: var(--acc-burnt); }
.month-chip.active.partial::after { color: var(--bg-0); }
.panel {
display: none; padding: 32px 36px 80px;
animation: fadeIn 0.3s ease-out;
}
.panel.active { display: block; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.methodology-note {
background: linear-gradient(90deg, rgba(216,201,168,0.05) 0%, rgba(216,201,168,0.01) 100%);
border-left: 2px solid var(--acc-cream);
padding: 14px 22px;
margin-bottom: 28px;
display: flex; align-items: flex-start; gap: 14px;
}
.methodology-note .tag {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--acc-cream); padding-top: 2px; flex-shrink: 0;
}
.methodology-note .body {
font-size: 13px; color: var(--t-2); line-height: 1.6;
}
.methodology-note .body strong { color: var(--t-1); font-weight: 500; }
/* ── KPI CARDS ────────────────────────────────────── */
.kpi-row {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
background: var(--border); border: 1px solid var(--border); margin-bottom: 32px;
}
.kpi { background: var(--bg-1); padding: 24px 28px; position: relative; }
.kpi .eyebrow {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--t-3); margin-bottom: 14px;
}
.kpi .number {
font-family: 'IBM Plex Serif', serif; font-weight: 400; font-size: 40px;
color: var(--t-1); letter-spacing: -0.02em; line-height: 1; margin-bottom: 10px;
}
.kpi .number.compact { font-size: 28px; }
.kpi .number .unit { font-size: 18px; color: var(--t-3); margin-left: 4px; }
.kpi .compare {
font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--t-2);
display: flex; align-items: baseline; gap: 8px;
}
.kpi .delta { font-weight: 500; }
.kpi .delta.up { color: var(--acc-sage); }
.kpi .delta.down { color: var(--acc-burnt); }
.kpi .delta.flat { color: var(--t-3); }
.kpi .sub {
font-size: 11px; color: var(--t-3); margin-top: 6px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.section { margin-bottom: 40px; }
.section h2 {
font-family: 'IBM Plex Serif', serif; font-weight: 400; font-size: 22px;
margin: 0 0 6px; color: var(--t-1); letter-spacing: -0.01em;
}
.section .subtitle {
font-family: 'IBM Plex Sans', sans-serif; font-size: 13px;
color: var(--t-3); margin-bottom: 22px; max-width: 720px;
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.card {
background: var(--bg-1); border: 1px solid var(--border);
padding: 22px 26px;
}
.card h3 {
font-family: 'IBM Plex Sans', sans-serif; font-weight: 500; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--t-2); margin: 0 0 6px;
}
.card .h3-sub {
font-family: 'IBM Plex Sans', sans-serif; font-size: 12px;
color: var(--t-3); margin-bottom: 18px;
}
.chart-wrap { position: relative; height: 280px; }
.chart-wrap.tall { height: 340px; }
.chart-wrap.short { height: 220px; }
.chart-wrap.flex { height: 100%; min-height: 220px; }
.empty-state {
height: 100%; display: flex; align-items: center; justify-content: center;
flex-direction: column; gap: 8px;
}
.empty-state .icon {
font-family: 'IBM Plex Mono', monospace; font-size: 24px; color: var(--t-4);
}
.empty-state .msg {
font-family: 'IBM Plex Sans', sans-serif; font-size: 12.5px;
color: var(--t-3); text-align: center; max-width: 280px;
}
/* ── TABLES ───────────────────────────────────────── */
table.dt { width: 100%; border-collapse: collapse; font-size: 12.5px; }
table.dt thead th {
text-align: left; font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.08em; color: var(--t-3);
padding: 10px 12px; border-bottom: 1px solid var(--border-strong);
background: var(--bg-1); position: sticky; top: 0;
}
table.dt thead th.r { text-align: right; }
table.dt tbody td {
padding: 11px 12px; border-bottom: 1px solid var(--border); color: var(--t-1);
}
table.dt tbody td.r { text-align: right; font-family: 'IBM Plex Mono', monospace; }
table.dt tbody td.dim { color: var(--t-2); }
table.dt tbody tr:hover { background: var(--bg-2); }
.cat-badge {
display: inline-block; padding: 2px 8px;
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.05em;
background: var(--bg-3); color: var(--t-2); border-radius: 2px;
}
.cat-badge.AC { background: rgba(111,174,148,0.15); color: var(--acc-sage); }
.cat-badge.PR { background: rgba(216,201,168,0.12); color: var(--acc-cream); }
.cat-badge.RT { background: rgba(107,143,181,0.15); color: var(--acc-blue); }
.cat-badge.GR { background: rgba(139,123,184,0.15); color: var(--acc-violet); }
.cat-badge.AB { background: rgba(184,101,77,0.15); color: var(--acc-rust); }
.cat-badge.SO { background: rgba(216,117,68,0.12); color: var(--acc-burnt); }
.cat-badge.OT { background: var(--bg-3); color: var(--t-2); }
.type-badge {
display: inline-block; padding: 1px 6px;
font-family: 'IBM Plex Mono', monospace; font-size: 9.5px;
text-transform: uppercase; letter-spacing: 0.06em;
border: 1px solid var(--border-strong); border-radius: 2px;
color: var(--t-2);
}
.type-badge.auto { border-color: var(--acc-sage); color: var(--acc-sage); }
.type-badge.solus { border-color: var(--acc-burnt); color: var(--acc-burnt); }
/* ── TOP PERFORMERS (Communications tab) ──────────── */
.top-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
}
.top-list { padding: 22px 0; }
.top-list .item {
display: grid; grid-template-columns: 30px 1fr auto;
align-items: center; gap: 12px;
padding: 10px 26px;
border-bottom: 1px solid var(--border);
}
.top-list .item:last-child { border-bottom: none; }
.top-list .rank {
font-family: 'IBM Plex Mono', monospace; font-size: 12px;
color: var(--t-3); font-weight: 500;
}
.top-list .name {
font-size: 12.5px; color: var(--t-1);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.top-list .name .sub {
display: block; font-family: 'IBM Plex Mono', monospace;
font-size: 10px; color: var(--t-3); margin-top: 3px;
text-transform: uppercase; letter-spacing: 0.05em;
}
.top-list .value {
font-family: 'IBM Plex Serif', serif; font-size: 22px;
color: var(--acc-cream); white-space: nowrap;
}
.top-list .value .vsub {
display: block; font-family: 'IBM Plex Mono', monospace;
font-size: 9.5px; color: var(--t-3); margin-top: 2px;
text-transform: uppercase; letter-spacing: 0.06em; text-align: right;
}
/* ── KEY FINDINGS (EDITORIAL) ─────────────────────── */
.editorial { max-width: 920px; }
.editorial .lede {
font-family: 'IBM Plex Serif', serif; font-weight: 300; font-size: 19px;
line-height: 1.55; color: var(--t-1); margin: 0 0 32px;
padding-bottom: 24px; border-bottom: 1px solid var(--border);
}
.editorial .lede .em { font-style: italic; color: var(--acc-cream); }
.editorial article { margin-bottom: 40px; }
.editorial article h3 {
font-family: 'IBM Plex Serif', serif; font-weight: 400; font-size: 22px;
margin: 0 0 6px; letter-spacing: -0.01em;
}
.editorial article .kicker {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.12em;
color: var(--acc-burnt); margin-bottom: 8px;
}
.editorial article .kicker.steady { color: var(--acc-sage); }
.editorial article .kicker.info { color: var(--acc-blue); }
.editorial article .kicker.struct { color: var(--acc-violet); }
.editorial .section-divider {
margin: 48px 0 28px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-strong);
display: flex; align-items: baseline; gap: 16px;
}
.editorial .section-divider .title {
font-family: 'IBM Plex Serif', serif;
font-weight: 400; font-size: 26px;
color: var(--t-1); letter-spacing: -0.01em;
}
.editorial .section-divider .descriptor {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--t-3);
}
.editorial .bl-note {
background: var(--bg-1); border: 1px solid var(--border);
padding: 12px 18px; margin: 0 0 18px;
font-size: 13px; color: var(--t-2); line-height: 1.6;
border-left: 2px solid var(--acc-cream);
}
.editorial .bl-note strong { color: var(--t-1); }
.editorial article p {
font-size: 14.5px; line-height: 1.7; color: var(--t-2); margin: 0 0 14px;
}
.editorial article p strong { color: var(--t-1); font-weight: 500; }
.pullquote {
margin: 20px 0; padding: 16px 22px; border-left: 2px solid var(--acc-cream);
background: var(--bg-1); font-family: 'IBM Plex Serif', serif;
font-style: italic; font-size: 15px; color: var(--t-1); line-height: 1.55;
}
.data-strip {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
background: var(--border); border: 1px solid var(--border); margin: 20px 0;
}
.data-strip .cell { background: var(--bg-1); padding: 12px 14px; }
.data-strip .cell .lbl {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--t-3); margin-bottom: 4px;
}
.data-strip .cell .val {
font-family: 'IBM Plex Mono', monospace; font-size: 14px;
color: var(--t-1); font-weight: 500;
}
.data-strip .cell .val.alert { color: var(--acc-burnt); }
.data-strip .cell .val.good { color: var(--acc-sage); }
.footnote {
margin-top: 60px; padding: 22px 26px;
background: var(--bg-1); border: 1px solid var(--border);
border-left: 2px solid var(--acc-cream);
}
.footnote h4 {
font-family: 'IBM Plex Mono', monospace; font-size: 11px;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--acc-cream); margin: 0 0 10px;
}
.footnote p {
margin: 0 0 8px; font-size: 12.5px; color: var(--t-2); line-height: 1.6;
}
.footnote ul { padding-left: 18px; margin: 8px 0 0; }
.footnote li { font-size: 12.5px; color: var(--t-2); line-height: 1.6; margin-bottom: 4px; }
.toolbar {
display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap;
}
.pill-btn {
padding: 5px 12px; border: 1px solid var(--border); background: transparent;
color: var(--t-2); font-family: 'IBM Plex Mono', monospace; font-size: 10px;
text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer;
}
.pill-btn:hover { color: var(--t-1); }
.pill-btn.active {
background: var(--bg-3); color: var(--t-1); border-color: var(--border-strong);
}
input.search-box {
background: var(--bg-2); border: 1px solid var(--border); color: var(--t-1);
padding: 7px 12px; font-family: 'IBM Plex Sans', sans-serif; font-size: 12px;
min-width: 240px; outline: none;
}
input.search-box:focus { border-color: var(--border-strong); }
.table-scroll {
max-height: 600px; overflow-y: auto; border: 1px solid var(--border);
}
.table-scroll::-webkit-scrollbar { width: 10px; }
.table-scroll::-webkit-scrollbar-track { background: var(--bg-1); }
.table-scroll::-webkit-scrollbar-thumb { background: var(--bg-3); }
@media (max-width: 1100px) {
.grid-2, .grid-3, .top-grid { grid-template-columns: 1fr; }
.kpi-row { grid-template-columns: repeat(2, 1fr); }
.data-strip { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<header class="topbar">
<h1>CRM Engagement & Attribution<span class="em"> · </span><span class="em">prototype v5</span></h1>
<div class="meta">
<div class="live">Source: MKTG CRM Engagement Dashboard</div>
<div>Jan – May 2026 · As of 28 May</div>
</div>
</header>
<nav class="tabs">
<div class="tab active" data-tab="summary"><span class="idx">01</span>Summary</div>
<div class="tab" data-tab="comms"><span class="idx">02</span>Communications</div>
<div class="tab" data-tab="stage"><span class="idx">03</span>Lifecycle & Product</div>
<div class="tab" data-tab="findings"><span class="idx">04</span>Key Findings</div>
</nav>
<div class="month-filter" id="monthFilter">
<span class="label">Month</span>
<button class="month-chip" data-month="jan">Jan</button>
<button class="month-chip" data-month="feb">Feb</button>
<button class="month-chip" data-month="mar">Mar</button>
<button class="month-chip" data-month="apr">Apr</button>
<button class="month-chip active partial" data-month="may">May (to-date)</button>
</div>
<!-- ────────────────────────────────────────────────── -->
<!-- TAB 1: SUMMARY -->
<!-- ────────────────────────────────────────────────── -->
<main class="panel active" id="panel-summary">
<div class="methodology-note">
<div class="tag">Methodology</div>
<div class="body">
This dashboard answers <strong>"how is CRM performing?"</strong> through engagement and conversion rates. For <strong>attributed deposit volumes</strong> — i.e. how many deposits CRM contributed in absolute terms — refer to the Growth Reporting sheet. The two sources answer different questions; both are correct. See Findings 07 & 08 for the methodology and taxonomy context.
</div>
</div>
<div class="kpi-row" id="kpiRow"></div>
<div class="section">
<h2>Monthly trajectory</h2>
<div class="subtitle">Send volume across the period (clean count, no duplication) and weighted-average deposit rate (the share of post-open users who deposit within 7 days). The rate metric is the dashboard's closest analogue to "is CRM converting better or worse over time."</div>
<div class="grid-2">
<div class="card">
<h3>Total sends</h3>
<div class="h3-sub">All permission-based marketing communications</div>
<div class="chart-wrap"><canvas id="chartSends"></canvas></div>
</div>
<div class="card">
<h3>Weighted-avg deposit rate</h3>
<div class="h3-sub">Volume-weighted across the portfolio. Includes all products.</div>
<div class="chart-wrap"><canvas id="chartDepRate"></canvas></div>
</div>
</div>
</div>
<div class="section">
<h2>Top contributors this month</h2>
<div class="subtitle">Communications ranked by relative reach × conversion impact. Deposit rate shown prominently; send volume shown as scale context. Used for "which communications moved the most users."</div>
<div class="card">
<div class="table-scroll" style="max-height: 380px;">
<table class="dt" id="topContributorsTable">
<thead>
<tr>
<th>Communication</th>
<th>Type</th>
<th>Lifecycle</th>
<th class="r">Sends</th>
<th class="r">Top product</th>
<th class="r">Total deposit rate</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div class="footnote">
<h4>Summary tab — what's measured</h4>
<p><strong>Total sends.</strong> Number of permission-based communications delivered to recipients. Clean count, no duplication.</p>
<p><strong>Weighted-avg app-open rate.</strong> Share of recipients who started an app session within the canvas's conversion window. Sourced from the <span class="em">% opened app</span> field in the Start_App_Session export, summed across all communications and divided by total sends. <span class="em">This is not an email/push open rate.</span> It measures whether the comm led the recipient back into the app, regardless of channel. Because push, email, IAM and content cards behave very differently against this denominator (push taps go straight to the app; email opens don't; IAM and CC only impress when the user is already in-app), this rate moves with channel mix as much as with engagement quality. Read as a portfolio-level diagnostic, not a channel-comparable engagement metric.</p>
<p><strong>Weighted-avg deposit rate.</strong> Sum across all communications of (sends × total deposit conversion rate), divided by sum of sends. "Total deposit rate" per communication is the sum of its per-product 7-day post-open deposit rates. Rates are per-communication and don't compound across communications.</p>
<p><strong>Top contribution rate.</strong> The highest weighted-impact communication in the period, displayed as its deposit rate. Underlying ranking metric (sends × rate) is used for ordering but not shown as a count.</p>
</div>
</main>
<!-- ────────────────────────────────────────────────── -->
<!-- TAB 2: COMMUNICATIONS -->
<!-- ────────────────────────────────────────────────── -->
<main class="panel" id="panel-comms">
<div class="section">
<h2>Top performers — <span id="topPerformersMonth">April 2026</span></h2>
<div class="subtitle">Top 5 Solus and Top 5 Automations by weighted impact (volume-adjusted deposit rate) in the selected month. Rate is the headline; send volume is shown alongside for scale.</div>
<div class="top-grid">
<div class="card">
<h3>Top 5 Solus</h3>
<div class="h3-sub">One-off marketing campaigns</div>
<div class="top-list" id="topSolusList"></div>
</div>
<div class="card">
<h3>Top 5 Automations</h3>
<div class="h3-sub">Lifecycle-triggered ongoing canvases</div>
<div class="top-list" id="topAutomationsList"></div>
</div>
</div>
</div>
<div class="section">
<h2>Activity by Lifecycle</h2>
<div class="subtitle">Send volume composition across the five-month window, broken down by lifecycle. Always cross-month; not affected by the month filter above.</div>
<div class="card">
<div class="chart-wrap tall"><canvas id="chartLifecycleActivity"></canvas></div>
</div>
</div>
<div class="section">
<h2>All communications — <span id="commsMonth">April 2026</span></h2>
<div class="subtitle">Every Automation and Solus active in the selected month, with engagement and conversion metrics.</div>
<div class="toolbar">
<button class="pill-btn active" data-filter="all">All</button>
<button class="pill-btn" data-filter="automation">Automations</button>
<button class="pill-btn" data-filter="solus">Solus</button>
<span style="flex:1;"></span>
<input class="search-box" id="commSearch" placeholder="Search communication name or lifecycle…">
</div>
<div class="card" style="padding: 0;">
<div class="table-scroll">
<table class="dt" id="commsTable">
<thead>
<tr>
<th>Communication</th>
<th>Type</th>
<th>Lifecycle</th>
<th class="r">Sends</th>
<th class="r">App open %</th>
<th class="r">Top product</th>
<th class="r">Top product dep %</th>
<th class="r">Total dep %</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div class="footnote">
<h4>Communications tab — classification & methodology notes</h4>
<p><strong>Automations vs Solus.</strong> Classification is currently inferred from `comm_category` and naming convention. Once the underlying Superset dataset exposes a `comm_type` field cleanly, this filter becomes authoritative rather than inferred.</p>
<p><strong>Lifecycle.</strong> Maps `comm_category` to lifecycle codes per canonical Braze taxonomy: Activation (AC), Surveys / Research (PR — note: Superset labels this "Pre-activation" but the canonical code is PR=Product, used for surveys), Retention (RT), Growth (GR), Abandonment (AB), Solus (all Marketing-* categories). Prospect (PT, web onboarding) communications don't surface in this dashboard's data feed. Service/Retool/Transactional categories are excluded from CRM attribution per the methodology document.</p>
<p><strong>Total dep %.</strong> Sum of per-product 7-day deposit rates for the communication. Rates are per-communication and don't compound; this column is comparable between rows. "Weighted impact" used for top-5 ranking is sends × total dep %, kept internal because the resulting count is not a unique conversion figure.</p>
</div>
</main>
<!-- ────────────────────────────────────────────────── -->
<!-- TAB 3: LIFECYCLE & PRODUCT -->
<!-- ────────────────────────────────────────────────── -->
<main class="panel" id="panel-stage">
<div class="section">
<h2>Deposit rate by product, over time</h2>
<div class="subtitle">Volume-weighted deposit rate per product across the portfolio each month. Calculated as Σ(sends × product deposit rate) ÷ Σ(sends) across all communications. Reads as "of every recipient who opened a CRM communication, what share deposited into product X within 7 days."</div>
<div class="card">
<div class="chart-wrap tall"><canvas id="chartProductTrend"></canvas></div>
</div>
</div>
<div class="section">
<h2>Deposit rate by lifecycle, over time</h2>
<div class="subtitle">Same view, sliced by lifecycle area. Shows which parts of the CRM portfolio are converting most efficiently at the open-to-deposit step. Surveys (PR) are excluded — they're not a lifecycle and their self-selected audiences distort the comparison.</div>
<div class="card">
<div class="chart-wrap tall"><canvas id="chartLifecycleTrend"></canvas></div>
</div>
</div>
<div class="section">
<h2>Send volume by primary product target</h2>
<div class="subtitle">Stacked monthly view of where CRM is concentrating recipient reach, classified by each communication's primary product (inferred from category and name). Multi-product communications are grouped as "Multi/All." Shows portfolio attention over time without conversion-volume duplication.</div>
<div class="card">
<div class="chart-wrap tall"><canvas id="chartSendTarget"></canvas></div>
</div>
</div>
<div class="section">
<h2 id="depPacingTitle">Weighted deposit-driving impact — pacing vs prior month</h2>
<div class="subtitle">Cumulative weighted deposit-driving impact (sends × total deposit rate, summed) by day-of-month, selected month vs prior. Like Send Pacing was, this is smoothed from monthly totals — daily granularity is the unlock. The Y-axis is an aggregate "rate × reach" measure, not a unique deposit count (see Findings · M3).</div>
<div class="card">
<div class="chart-wrap tall" id="depPacingChartWrap">
<canvas id="chartDepPacing"></canvas>
</div>
</div>
</div>
<div class="footnote">
<h4>Lifecycle & Product tab — caveats</h4>
<p>Lifecycle allocation comes from `comm_category`. The canonical Braze taxonomy uses lifecycle codes anchored on communication name prefix (AC-/PR-/RT-/GR-/AB-/MK-/PT-). Note that PR is <strong>Product</strong> in the source taxonomy — used for surveys and research — even though the Superset dashboard labels these communications "Pre-activation." That's why PR communications show unusually high conversion rates (15–23% on some surveys): the audiences are small, intent-rich, and self-selected, not pre-funnel cohorts.</p>
<p>Deposit rates are weighted averages and are inherently per-communication — they don't compound across communications. The same user converting after opening two communications contributes to two rates independently, but each rate remains a clean within-communication measure.</p>
<p>Primary product target inferred from category for marketing communications (e.g. Marketing-PSA → PSA) and from communication name for automations (AC-CISA → CISA). Communications spanning multiple products (Marketing-Multi, Marketing-All, RT-Payday_Reminder, etc.) bucket into "Multi/All." Note: SISA is the canonical product code for Smart Cash ISA; the Superset export uses "smISA" as the column header, but both refer to the same product.</p>
</div>
</main>
<!-- ────────────────────────────────────────────────── -->
<!-- TAB 4: KEY FINDINGS -->
<!-- ────────────────────────────────────────────────── -->
<main class="panel" id="panel-findings">
<div class="editorial">
<p class="lede">
Five months of conversion data — January through mid-May 2026 — framed against the business line structure of the weekly delivery reporting. The dashboard's performance picture lands differently for each line: Cash sees most of CRM's volume contribution; Investments shows clean activation flows; Growth is missing meaningful visibility into web onboarding. Methodology and cross-cutting findings sit below. <span class="em">All are recoverable.</span>
</p>
<div class="section-divider">
<div class="title">By Business Line</div>
<div class="descriptor">CRM through the lens of weekly delivery reporting</div>
</div>
<div class="bl-note">
<strong>Cash</strong> — balance build and retention on rate-led Cash products (CIA, CISA, EAS, PSA, SISA). The largest single block of CRM volume contribution sits here, and the rate-led nature of the business line means CRM activity — boost expiries, payday reminders, balance build, prize draws — couples directly to deposit and retention outcomes.
</div>
<article>
<div class="kicker info">Cash · Methodology correction</div>
<h3>Payday Reminder is running — but the April export was snapshotted before it fired</h3>
<p>The v4 finding read this as a two-month contribution gap. With the operational context now in hand, it isn't one. The rename from <strong>RT-Payday_Reminder</strong> to <strong>MK-MULTI-DD-MM-YYYY-Payday_[Month]</strong> seen in March 2026 has held: April's canvas fired on the last working day of the month as <strong>MK-MULTI-30-04-2026-Payday_April</strong>. It does not appear in the April Superset export because the export's latest <span class="em">last_sent_at</span> is 28/04/2026 — the snapshot pre-dates the send (and its conversion window). Re-pull the April export with a window that captures 30/04 onwards and the canvas should populate.</p>
<p>May is not anomalous either. The canvas fires on the last working day of each month — Friday 29 May 2026 — so its absence in a 14 May export is the expected state, not a contribution issue.</p>
<div class="data-strip">
<div class="cell"><div class="lbl">Jan canvas</div><div class="val">RT-Payday_Reminder_January</div></div>
<div class="cell"><div class="lbl">Feb canvas</div><div class="val">RT-Payday_Reminder_February</div></div>
<div class="cell"><div class="lbl">Mar canvas</div><div class="val">MK-MULTI-26-03-2026-Payday_Reminder</div></div>
<div class="cell"><div class="lbl">Apr canvas</div><div class="val good">MK-MULTI-30-04-2026-Payday_April · fired, not in export</div></div>
<div class="cell"><div class="lbl">May canvas</div><div class="val">Fires 29/05 — not due yet</div></div>
</div>
<div class="pullquote">
The methodology lesson generalises. A canvas missing from a monthly export isn't evidence the canvas didn't run — always reconcile the export's latest <span class="em">last_sent_at</span> against the canvas's expected send date before reading absence as inaction. Last-working-day and other month-end cadences are the most exposed.
</div>
</article>
<article>
<div class="kicker steady">Cash · Product</div>
<h3>Smart Cash ISA (SISA) launched cleanly and is performing</h3>
<p>The SISA column first appeared with non-zero values in March (a handful of communications showing 0.1–0.8% — almost certainly pre-launch instrumentation or pilot sends). The actual launch ran April 8, 2026 via <strong>MK-SISA-08-04-2026-Smart_Cash_ISA_Launch</strong> (43.6k) and its resend at scale (445k).</p>
<div class="data-strip">
<div class="cell"><div class="lbl">SISA welcome — April</div><div class="val good">17.8% deposit</div></div>
<div class="cell"><div class="lbl">SISA welcome — May</div><div class="val good">11.0% deposit</div></div>
<div class="cell"><div class="lbl">PR survey — drop-off (Apr)</div><div class="val good">3.7% SISA deposit</div></div>
<div class="cell"><div class="lbl">Allowance reset push</div><div class="val">4.1% app-open</div></div>
<div class="cell"><div class="lbl">Internal transfer nudge</div><div class="val">3.4% SISA</div></div>
</div>
<p>The Welcome canvas (`MK-SISA-07-04-2026-Smart_Cash_ISA_Welcome`) is the strongest single performer in the entire dataset on product-specific conversion. 17.8% deposit rate in April; 11.0% in May. Even allowing for softer May volumes, this canvas is converting at multiples of any other communication.</p>
<p>The March SISA numbers should be flagged as pre-launch and excluded from MoM trending. <strong>Naming note:</strong> the canonical product code is SISA; the Superset export labels this product "smISA" — same product, two labels.</p>
</article>
<div class="bl-note">
<strong>Investments</strong> — new Invest activation (aligned with Growth) or cross-sell from Cash users into S&S ISA, GIA, SIPP. Two distinct motions; the dashboard surfaces activation cleanly but cross-sell signal is harder to read from this view alone.
</div>
<article>
<div class="kicker steady">Investments · Activation</div>
<h3>Investment activation flows are converting steadily</h3>
<p>AC-SSISA_v3 and AC-GIA — the two main Invest activation canvases — show stable performance across the five-month window. Deposit rates into the relevant Invest product:</p>
<div class="data-strip">
<div class="cell"><div class="lbl">AC-SSISA_v3 — S&S ISA</div><div class="val good">5.6–9.3% range</div></div>
<div class="cell"><div class="lbl">AC-GIA — GIA</div><div class="val good">2.9–7.6% range</div></div>
<div class="cell"><div class="lbl">AB-Investing_V2 — recovery</div><div class="val">2.6–4.9% S&S ISA</div></div>
<div class="cell"><div class="lbl">PR Investments Survey</div><div class="val good">11–15% S&S ISA</div></div>
<div class="cell"><div class="lbl">GR-Maxed_Out_ISA</div><div class="val">0.5–1.2% S&S ISA</div></div>
</div>
<p>The activation flows (AC-) are the cleanest signal for the Investments BL. The survey audience (PR-) converts at high rates but reflects intent-rich self-selection rather than funnel performance — see Methodology · M4.</p>
<p><strong>Cross-sell caveat:</strong> the 7-day post-open window in this dashboard is calibrated for cash-deposit decisions. Investing decisions involve more deliberation; users who open a Cash → Invest cross-sell email and decide to open an S&S ISA three weeks later won't show up in this attribution view. The dashboard likely understates the Investments BL's cross-sell impact.</p>
</article>
<div class="bl-note">
<strong>Growth</strong> — activating new customers. The CRM home for Growth is AC- (Activation, post-signup) and PT- (Prospect, web onboarding pre-app-download).
</div>
<article>
<div class="kicker">Growth · Visibility gap</div>
<h3>PT (Prospect / web onboarding) is invisible to this dashboard</h3>
<p>The canonical Braze taxonomy includes PT — Prospect — as a lifecycle covering web onboarding communications sent to users who have not yet downloaded the app. These communications don't appear in this dashboard's data feed at all.</p>
<p>For the Growth business line specifically: this is a meaningful gap. If Growth is measured on new customer activation, and PT is the CRM activity targeting the earliest stage of that journey, the dashboard offers an incomplete view of Growth's CRM contribution. AC- (post-signup activation) is visible and performs steadily — AC-CIA-New at 4.5–12.7% CIA deposit, AC-CISA-New at 5.6–9.3% CISA — but the pre-app step is dark.</p>
<p>The production dashboard's methodology page should make this explicit so Growth stakeholders don't read CRM-attributed performance as the complete top-of-funnel picture.</p>
</article>
<div class="bl-note">
<strong>Pension</strong> — SIPP. Per the canonical naming sheet, SIPP-specific CRM activity is predominantly Transactional (TR-) — beneficiary nudges, tax relief status, account opened confirmations. These categories are excluded from CRM attribution per the methodology document. No marketing-attributable Pension activity surfaces in this dashboard.
</div>
<div class="bl-note">
<strong>Wealth ME</strong> — getting the Wealth Planner tooling functional; no CRM campaigns yet per current team direction. The only tangentially related communication in the dataset is <strong>PR-ALL-07-03-2026-Wealth_Plan_Survey</strong> (5.2k sends, 16.9% app-open rate) — a research survey rather than tooling activation. Wealth Planner activation flows would appear here once the product is in market.
</div>
<div class="bl-note">
<strong>Scale Safe</strong> — operational guardrail projects. The CRM dimension manifests in Service (SV), Transactional (TR), and Incident (INC) categories — all explicitly excluded from CRM marketing attribution per the methodology document. No findings visible in this dashboard.
</div>
<div class="section-divider">
<div class="title">Methodology</div>
<div class="descriptor">Dashboard-level concerns to resolve before production</div>
</div>
<article>
<div class="kicker">Methodology · M1</div>
<h3>The "-93% CVR collapse" narrative isn't visible in the dashboard's deposit rate</h3>
<p>The CRM Attribution Methodology document attributes a <strong>-93% CVR drop to the February 2026 Payday Reminder restructure</strong>. The deposit rate shown in this dashboard (whatever its precise denominator — see M2) tells a different story:</p>
<div class="data-strip">
<div class="cell"><div class="lbl">Jan</div><div class="val">1.6% CIA dep</div></div>
<div class="cell"><div class="lbl">Feb</div><div class="val good">2.3% CIA dep</div></div>
<div class="cell"><div class="lbl">Mar</div><div class="val good">2.3% CIA dep</div></div>
<div class="cell"><div class="lbl">Apr</div><div class="val">fired 30/04 — not in export</div></div>
<div class="cell"><div class="lbl">May</div><div class="val">fires 29/05</div></div>
</div>
<p>The -93% claim must measure something different — most likely canvas-entry CVR using the canvas's primary conversion event. Two different denominators, two different stories. The dashboard's denominator (per M2) is closer to "did the message actually drive the action" than to Braze's native canvas CVR, but isn't directly comparable to it.</p>
</article>
<article>
<div class="kicker">Methodology · M2</div>
<h3>Three dashboard quirks to fix before production</h3>
<p><strong>Duplicate rows.</strong> Every month's export shows some communications repeated 3–4 times with different `last_sent_at` values but identical metrics. This prototype dedupes by `comm_name` + month and takes the first occurrence; the underlying SQL should be tightened to do the same.</p>
<p><strong>Low-volume stragglers.</strong> A handful of rows with 1–3 sends keep appearing for communications that have effectively ended (e.g. MK-PSA-27-03-2026-PSA500_LastChance with 1 send in April). Likely test sends or late triggers. The production dashboard should filter sub-50 by default, with a toggle to include them.</p>
<p><strong>"Send count" naming.</strong> Worth confirming what the column actually represents. If it's "people who opened the email" — which the table description suggests — then the dashboard is showing post-open conversion rates calculated against opens, not against recipients.</p>
</article>
<article>
<div class="kicker struct">Methodology · M3</div>
<h3>Why this dashboard shows rates, and Growth Reporting shows volumes</h3>
<p>The underlying Superset data records conversion percentages per communication. Each row is one communication; each user can appear in multiple rows. Aggregating these rates into estimated deposit volumes (sends × rate, summed) double-counts users who open multiple communications within a window. The resulting estimates sit orders of magnitude above Growth Reporting's attribution figures (4,737 attributed deposits in January) and would create internal confusion about which number is "real."</p>
<div class="pullquote">
The two numbers measure different things. This dashboard measures CRM <em>performance</em> through rates. Growth Reporting measures CRM <em>contribution</em> through attributed deposit counts.
</div>
<p>The structural fix for attributed volumes is last-touch attribution in Databricks. Each conversion in the product database gets assigned to the most recent communication exposure within the relevant window — counted once, attributed to one communication. Until that exists, the canonical "deposits driven" number lives in Growth Reporting and this dashboard supplements it.</p>
</article>
<article>
<div class="kicker">Methodology · M4</div>
<h3>The "Pre-activation" label is misleading — these are surveys, not funnel</h3>
<p>The Superset export categorises PR-prefixed communications as "Pre-activation." The canonical Braze taxonomy uses PR to mean Product — the lifecycle code for surveys and research, not pre-funnel activation. Concrete examples:</p>
<div class="data-strip">
<div class="cell"><div class="lbl">investments_survey<br>PR-ALL</div><div class="val good">14.5% S&S ISA</div></div>
<div class="cell"><div class="lbl">onboarding_drop_off<br>PR-SISA</div><div class="val good">23.7% CISA</div></div>
<div class="cell"><div class="lbl">transfer_out_survey<br>PR-MULTI</div><div class="val">0.9% CIA</div></div>
<div class="cell"><div class="lbl">Wealth_Plan_Survey<br>PR-ALL</div><div class="val good">9.5% CIA</div></div>
<div class="cell"><div class="lbl">Typical audience</div><div class="val">2–17k sends</div></div>
</div>
<p>The high conversion rates aren't evidence that the pre-activation funnel is highly effective. They're evidence that survey audiences are small, intent-rich, and self-selected. Reading these rates as funnel performance misframes the team's investment thesis. Two fixes: (1) relabel the lifecycle — done in this version — and (2) annotate these communications so production doesn't bucket survey performance into funnel performance.</p>
</article>
<article>
<div class="kicker struct">Methodology · M5</div>
<h3>Late-month sends were leaking into the following month's view — now bucketed by send date</h3>
<p>The Superset export includes any canvas whose 7-day conversion window touches the queried month, not just canvases that actually sent in that month. Concrete example: <strong>MK-MULTI-29-04-2026-SpringDraw_Week5</strong>, sent once on 30 April with 36.2k recipients, appeared in both the April export (36.2k sends, 12.4% CIA dep) and the May export (36.2k sends, 12.3% CIA dep) — same send, reported twice. <strong>MK-MULTI-22-04-2026-SpringDraw_Week4</strong> showed the same pattern with different cardinality, surfacing 3.29k engagement-bearing users in May for a comm sent only on 24 April.</p>
<p>This dashboard now buckets each communication into the month its <span class="em">first_sent_at</span> falls in, regardless of which Superset export window it surfaces in. The two April comms above are excluded from the May view in this version. The rule generalises: any late-month send (Payday Reminders, month-end Marketing campaigns) is exposed to this leakage, and the bucketing rule eliminates the cross-month double-count.</p>
<div class="pullquote">
Ongoing automations are unaffected — their monthly figures are already partitioned because Superset tracks them by send activity per window. The leakage only applies to one-off Solus with <span class="em">first_sent_at == last_sent_at</span> near a month boundary.
</div>
</article>
<div class="section-divider">
<div class="title">Cross-cutting</div>
<div class="descriptor">Observations that span multiple business lines</div>
</div>
<article>
<div class="kicker info">Cross-cutting · Activity</div>
<h3>RT-Reactivation_V2 has grown 8× in four months</h3>
<p>Send volume on the reactivation automation:</p>
<div class="data-strip">
<div class="cell"><div class="lbl">Jan</div><div class="val">4.48k</div></div>
<div class="cell"><div class="lbl">Feb</div><div class="val">3.98k</div></div>
<div class="cell"><div class="lbl">Mar</div><div class="val">13.4k</div></div>
<div class="cell"><div class="lbl">Apr</div><div class="val">36.8k</div></div>
<div class="cell"><div class="lbl">May (to-28)</div><div class="val">19.7k</div></div>
</div>
<p>Either deliberate audience expansion (a campaign decision) or eligibility broadened (an ops change). Deposit conversion has stayed muted — 0.4% CIA deposit in April, 0.4% in May — so the volume isn't translating into proportionate conversion. AB-Onboarding_Product_Abandonment shows a similar pattern: 66 → 288 → 642 → 6.83k → 556. Both warrant context from the CRM team before the dashboard codes them as "growing communications."</p>
</article>
<article>
<div class="kicker info">Cross-cutting · Channel</div>
<h3>IAM (in-app messages) outperform email by an order of magnitude</h3>
<p>April's MK-IAM-01-04-2026-SpringDraw_Week1 (145k sends, in-app message channel) delivered:</p>
<div class="data-strip">
<div class="cell"><div class="lbl">CISA deposit</div><div class="val good">11.7%</div></div>
<div class="cell"><div class="lbl">CIA deposit</div><div class="val good">9.1%</div></div>
<div class="cell"><div class="lbl">App open rate</div><div class="val good">46.6%</div></div>
<div class="cell"><div class="lbl">vs Email Week1</div><div class="val">~10× higher</div></div>
<div class="cell"><div class="lbl">Cost</div><div class="val">~zero marginal</div></div>
</div>
<p>Channel comparison should be a first-class dimension in the production dashboard. The SpringDraw_Week6 A/B test in May is another example (Week6_Experiment outperforms Week6 on every metric, sub-test only sent to 50.9k of 312k total). Without channel/variant as a dimension, these wins stay invisible.</p>
</article>
</div>
<div class="footnote">
<h4>What this prototype isn't</h4>
<p>This is a design artefact, not a production dashboard. It demonstrates intent, layout, and analytical framing so the Data & Insights team has something concrete to react to.</p>
<ul>
<li>It does not refresh live — figures reflect the snapshot you exported.</li>
<li>It does not show holdout/control group comparison — separate data dependency.</li>
<li>It does not implement product-specific conversion windows — current view uses the dashboard's flat 7-day window.</li>
<li>It does not display daily pacing — the underlying export is monthly. Daily granularity is the largest single unlock.</li>
<li>It does not show absolute attributed deposit volumes — that's Growth Reporting's job.</li>
<li>It does not include PT (Prospect / web onboarding) communications — those flow through a separate data path and are out of scope for this export.</li>
</ul>
</div>
</main>
<script>
// ───────────────────────────────────────────────────────────
// DATA — five months of deduplicated conversion data
// ───────────────────────────────────────────────────────────
const CAT_TO_LIFECYCLE = {
'Activation': 'AC',
'Pre-activation': 'PR',
'Retention': 'RT',
'Growth': 'GR',
'Abandonment': 'AB',
'Marketing - All': 'SO',
'Marketing - Multi': 'SO',
'Marketing - PSA': 'SO',
'Marketing - EAS': 'SO',
'Marketing - GIA': 'SO',
'Marketing - CB ISA': 'SO',
'Marketing - Investments': 'SO',
'Marketing - Smart ISA': 'SO',
'Marketing - Other': 'SO',
};
const isSolus = (name) => /\d{1,2}[-_]\d{1,2}[-_]\d{4}|\d{8}/.test(name);
const typeOf = (name) => isSolus(name) ? 'Solus' : 'Automation';
const DATA = {
jan: {
label: 'January 2026',
days: 31, elapsed: 31, partial: false,
comms: [
{ n: 'MK-PSA-16-1-2026-PSA500_Q1_Week3_Winner_Video', c: 'Marketing - PSA', s: 521000, app: 15.6, dep: { ssisa: 0.8, psa: 1.6, gia: 0.3, eas: 0.5, cisa: 2.1, cia: 2.8 } },
{ n: 'MK-MULTI-14-1-2026-Savings_challenges_2026', c: 'Marketing - Multi', s: 434000, app: 3.7, dep: { ssisa: 0.3, psa: 0.5, gia: 0.1, eas: 0.2, cisa: 0.7, cia: 1.0 } },
{ n: 'MK-MULTI-24-01-2026-3_Problems_Solved_CASH', c: 'Marketing - Multi', s: 400000, app: 4.7, dep: { ssisa: 0.5, psa: 0.7, gia: 0.2, eas: 0.4, cisa: 1.4, cia: 1.9 } },
{ n: 'MK-ALL-1-1-2026-2026_New_Year_New_You', c: 'Marketing - All', s: 386000, app: 11.1, dep: { ssisa: 0.9, psa: 1.6, gia: 0.4, eas: 0.6, cisa: 2.2, cia: 3.3 } },
{ n: 'MK-INVS-06-01-2026-0%_Fees_Last_Chance', c: 'Marketing - Investments', s: 384000, app: 11.6, dep: { ssisa: 0.1, psa: 1.0, gia: 0.1, eas: 0.4, cisa: 1.5, cia: 2.1 } },
{ n: 'MK-PSA-1-1-2026-PSA500_Q1_Week1', c: 'Marketing - PSA', s: 364000, app: 24.5, dep: { ssisa: 1.0, psa: 1.9, gia: 0.4, eas: 0.7, cisa: 2.8, cia: 3.5 } },
{ n: 'RT-Payday_Reminder_January_30012026', c: 'Retention', s: 257000, app: 5.5, dep: { ssisa: 0.3, psa: 0.7, gia: 0.1, eas: 0.3, cisa: 1.3, cia: 1.6 } },
{ n: 'MK-PSA-09-01-2026-PSA500_Q1_Week2_Winner_Teaser', c: 'Marketing - PSA', s: 234000, app: 6.8, dep: { ssisa: 0.6, psa: 1.4, gia: 0.2, eas: 0.4, cisa: 1.2, cia: 2.2 } },
{ n: 'MK-PSA-5-1-2026-Final_entries_December', c: 'Marketing - PSA', s: 96400, app: 40.1, dep: { ssisa: 2.5, psa: 9.1, gia: 1.1, eas: 1.5, cisa: 4.1, cia: 7.7 } },
{ n: 'PR-ALL-23-01-2025-commercial_test_Pie_tax_2', c: 'Pre-activation', s: 70400, app: 7.3, dep: { ssisa: 0.2, psa: 0.4, gia: 0.1, eas: 0.6, cisa: 1.5, cia: 0.9 } },
{ n: 'MK-CISA-20-1-2026-VRPs_Available', c: 'Marketing - CB ISA', s: 60100, app: 12.1, dep: { ssisa: 1.2, psa: 1.8, gia: 0.4, eas: 0.6, cisa: 5.2, cia: 3.9 } },
{ n: 'MK-INVS-13-01-2026-News_W18', c: 'Marketing - Investments', s: 54600, app: 2.6, dep: { ssisa: 0.4, psa: 0.2, gia: 0.1, eas: 0.1, cisa: 0.2, cia: 0.3 } },
{ n: 'RT-PSA_December_Winners_Announcement', c: 'Retention', s: 19300, app: 42.3, dep: { ssisa: 2.2, psa: 9.2, gia: 1.0, eas: 1.0, cisa: 2.4, cia: 5.1 } },
{ n: 'MK-EAS-21-1-2026-Winback_3.45%_Loyalty_rate', c: 'Marketing - EAS', s: 11100, app: 9.4, dep: { ssisa: 0.3, psa: 0.6, gia: 0.3, eas: 1.7, cisa: 0.8, cia: 1.3 } },
{ n: 'MK-EAS-22-1-2026-Winback_3.45%_Loyalty_rate_Push', c: 'Marketing - EAS', s: 8370, app: 3.9, dep: { ssisa: 0.1, psa: 0.1, gia: 0.0, eas: 0.4, cisa: 0.2, cia: 0.2 } },
{ n: 'RT-SSISA_Balance_Build', c: 'Retention', s: 6390, app: 27.2, dep: { ssisa: 8.9, psa: 3.8, gia: 0.7, eas: 0.9, cisa: 6.4, cia: 5.6 } },
{ n: 'AB-Investing_V2', c: 'Abandonment', s: 5360, app: 32.5, dep: { ssisa: 4.9, psa: 3.4, gia: 1.4, eas: 1.3, cisa: 5.8, cia: 6.5 } },
{ n: 'RT-Reactivation_V2', c: 'Retention', s: 4480, app: 5.7, dep: { ssisa: 0.5, psa: 1.1, gia: 0.2, eas: 0.4, cisa: 1.1, cia: 2.0 } },
{ n: 'RT-EAS_Loyalty_offer_IAM', c: 'Retention', s: 2880, app: 78.9, dep: { ssisa: 1.5, psa: 2.6, gia: 0.8, eas: 8.0, cisa: 4.4, cia: 6.2 } },
{ n: 'MK-ALL-Goal_Created', c: 'Marketing - All', s: 2520, app: 18.2, dep: { ssisa: 3.3, psa: 3.5, gia: 1.3, eas: 1.8, cisa: 4.1, cia: 6.5 } },
{ n: 'AC-CISA-Existing', c: 'Activation', s: 1440, app: 16.2, dep: { ssisa: 2.8, psa: 2.5, gia: 0.8, eas: 1.7, cisa: 2.9, cia: 4.0 } },
{ n: 'AC-SSISA_v3', c: 'Activation', s: 1430, app: 30.5, dep: { ssisa: 6.1, psa: 2.7, gia: 1.1, eas: 1.3, cisa: 7.6, cia: 6.8 } },
{ n: 'RT-CIA_4.76%_14Day_BoostedRate_Expiry', c: 'Retention', s: 1410, app: 21.9, dep: { ssisa: 1.1, psa: 1.6, gia: 0.7, eas: 1.1, cisa: 3.3, cia: 4.5 } },
{ n: 'AC-EAS-Existing', c: 'Activation', s: 1390, app: 22.6, dep: { ssisa: 1.4, psa: 3.2, gia: 0.1, eas: 7.1, cisa: 3.9, cia: 4.5 } },
{ n: 'GR-Maxed_Out_ISA_Allowance_v2', c: 'Growth', s: 1360, app: 28.8, dep: { ssisa: 1.2, psa: 1.5, gia: 0.6, eas: 1.0, cisa: 1.5, cia: 4.0 } },
{ n: 'MK-CISA-20-1-2026-VRPs_Available_Microsoft', c: 'Marketing - CB ISA', s: 1360, app: 12.8, dep: { ssisa: 0.8, psa: 1.9, gia: 0.4, eas: 0.4, cisa: 4.4, cia: 3.7 } },
{ n: 'AC-CIA-Existing', c: 'Activation', s: 812, app: 12.6, dep: { ssisa: 1.0, psa: 2.0, gia: 0.9, eas: 1.1, cisa: 1.2, cia: 4.8 } },
{ n: 'AC-CIA-New', c: 'Activation', s: 754, app: 8.8, dep: { ssisa: 0.4, psa: 1.1, gia: 0.1, eas: 0.7, cisa: 0.4, cia: 4.5 } },
{ n: 'AC-GIA', c: 'Activation', s: 709, app: 31.9, dep: { ssisa: 1.7, psa: 3.1, gia: 7.6, eas: 1.0, cisa: 4.2, cia: 5.5 } },
{ n: 'AC-CISA-New', c: 'Activation', s: 690, app: 11.3, dep: { ssisa: 0.7, psa: 1.7, gia: 0.0, eas: 0.6, cisa: 9.3, cia: 0.9 } },
]
},
feb: {
label: 'February 2026',
days: 28, elapsed: 28, partial: false,
comms: [
{ n: 'MK-PSA-11-02-2026-PSA500_Reannouncement', c: 'Marketing - PSA', s: 406000, app: 16.2, dep: { ssisa: 0.7, psa: 2.1, gia: 0.3, eas: 0.5, cisa: 2.0, cia: 2.6 } },
{ n: 'RT-Payday_Reminder_February_27022026', c: 'Retention', s: 264000, app: 5.2, dep: { ssisa: 0.8, psa: 1.2, gia: 0.3, eas: 0.5, cisa: 1.7, cia: 2.3 } },
{ n: 'MK-CIAS-03-02-2026-Give20_1K_Referral_Incentive', c: 'Marketing - Other', s: 96800, app: 36.3, dep: { ssisa: 2.0, psa: 2.7, gia: 0.9, eas: 1.7, cisa: 4.7, cia: 11.4 } },
{ n: 'MK-ALL-11-02-2026-BBA_Nudge_2026', c: 'Marketing - All', s: 73700, app: 6.3, dep: { ssisa: 0.7, psa: 1.8, gia: 0.2, eas: 0.5, cisa: 1.2, cia: 3.2 } },
{ n: 'MK-EAS-03-02-2026-0.31%_Boost_TOPUP25k', c: 'Marketing - EAS', s: 52400, app: 7.3, dep: { ssisa: 0.5, psa: 0.7, gia: 0.3, eas: 0.2, cisa: 1.5, cia: 2.0 } },
{ n: 'MK-INVS-07-02-2026-News_W19', c: 'Marketing - Investments', s: 45700, app: 2.3, dep: { ssisa: 0.4, psa: 0.3, gia: 0.2, eas: 0.1, cisa: 0.2, cia: 0.5 } },
{ n: 'MK-INVS-20-02-2026-ii-14-launch', c: 'Marketing - Investments', s: 34900, app: 13.0, dep: { ssisa: 0.7, psa: 2.3, gia: 0.6, eas: 0.8, cisa: 2.7, cia: 5.0 } },
{ n: 'MK-INVS-06-02-2026-ii-13-invest30', c: 'Marketing - Investments', s: 29900, app: 21.4, dep: { ssisa: 2.5, psa: 1.8, gia: 1.9, eas: 1.0, cisa: 2.6, cia: 3.3 } },
{ n: 'MK-ALL-27-02-2026-Referral_£50_Incentive', c: 'Marketing - All', s: 26600, app: 8.1, dep: { ssisa: 0.8, psa: 0.5, gia: 0.4, eas: 0.5, cisa: 1.6, cia: 1.6 } },
{ n: 'MK-INVS-10-02-2026-ii-13-invest30-reminder', c: 'Marketing - Investments', s: 26500, app: 18.8, dep: { ssisa: 1.7, psa: 1.7, gia: 1.4, eas: 0.8, cisa: 2.2, cia: 2.9 } },
{ n: 'MK-EAS-20-02-2026-0.31%_Boost_TOPUP25k', c: 'Marketing - EAS', s: 26100, app: 2.3, dep: { ssisa: 0.3, psa: 0.2, gia: 0.1, eas: 0.0, cisa: 0.8, cia: 0.8 } },
{ n: 'MK-EAS-10-02-2026-Winback_3.50%_Loyalty_rate', c: 'Marketing - EAS', s: 20500, app: 5.2, dep: { ssisa: 0.2, psa: 0.5, gia: 0.2, eas: 0.6, cisa: 0.4, cia: 0.6 } },
{ n: 'MK-PSA-26-02-2026-PSA_Referral_500Entries', c: 'Marketing - PSA', s: 13900, app: 16.2, dep: { ssisa: 2.7, psa: 9.9, gia: 1.2, eas: 1.5, cisa: 3.6, cia: 6.9 } },
{ n: 'RT-SSISA_Balance_Build', c: 'Retention', s: 5530, app: 23.1, dep: { ssisa: 5.8, psa: 3.3, gia: 0.4, eas: 0.8, cisa: 5.9, cia: 3.6 } },
{ n: 'GR-PSA500_£150_Incentive', c: 'Growth', s: 5270, app: 32.5, dep: { ssisa: 0.8, psa: 1.7, gia: 0.9, eas: 0.9, cisa: 0.4, cia: 2.1 } },
{ n: 'RT-Reactivation_V2', c: 'Retention', s: 3980, app: 4.4, dep: { ssisa: 0.4, psa: 0.7, gia: 0.3, eas: 0.3, cisa: 1.2, cia: 2.0 } },
{ n: 'AB-Investing_V2', c: 'Abandonment', s: 2800, app: 29.5, dep: { ssisa: 4.4, psa: 3.3, gia: 1.9, eas: 1.6, cisa: 6.1, cia: 6.0 } },
{ n: 'RT-CIA_Withdrawal_Cross-sell_EAS_v2', c: 'Retention', s: 2260, app: 28.4, dep: { ssisa: 1.1, psa: 1.9, gia: 0.8, eas: 0.3, cisa: 2.1, cia: 30.5 } },
{ n: 'AC-SSISA_v3', c: 'Activation', s: 2150, app: 33.6, dep: { ssisa: 9.3, psa: 2.6, gia: 0.6, eas: 1.2, cisa: 6.7, cia: 4.7 } },
{ n: 'AC-EAS-Existing', c: 'Activation', s: 1630, app: 22.0, dep: { ssisa: 1.3, psa: 2.5, gia: 1.4, eas: 7.9, cisa: 3.1, cia: 5.0 } },
{ n: 'AC-GIA', c: 'Activation', s: 1510, app: 34.2, dep: { ssisa: 1.8, psa: 3.6, gia: 6.2, eas: 1.5, cisa: 3.1, cia: 4.8 } },
{ n: 'MK-ALL-10-02-2026-Feb_deposit_incentive', c: 'Marketing - All', s: 1480, app: 39.5, dep: { ssisa: 3.2, psa: 10.6, gia: 0.9, eas: 3.5, cisa: 3.4, cia: 5.8 } },
{ n: 'MK-ALL-Goal_Created', c: 'Marketing - All', s: 1440, app: 16.4, dep: { ssisa: 3.4, psa: 4.0, gia: 1.2, eas: 2.2, cisa: 4.1, cia: 5.4 } },
{ n: 'PR-Onboarding', c: 'Pre-activation', s: 1330, app: 3.1, dep: { ssisa: 0.4, psa: 0.1, gia: 0.2, eas: 0.4, cisa: 0.5, cia: 0.7 } },
{ n: 'GR-Maxed_Out_ISA_Allowance_v2', c: 'Growth', s: 1260, app: 25.1, dep: { ssisa: 0.8, psa: 1.4, gia: 0.7, eas: 1.3, cisa: 1.3, cia: 3.3 } },
{ n: 'AC-CISA-Existing', c: 'Activation', s: 1170, app: 12.3, dep: { ssisa: 2.3, psa: 2.0, gia: 0.3, eas: 1.2, cisa: 3.6, cia: 4.2 } },
{ n: 'RT-Investments_Survey_1M', c: 'Retention', s: 1140, app: 25.3, dep: { ssisa: 4.2, psa: 3.3, gia: 2.2, eas: 1.3, cisa: 4.7, cia: 3.8 } },
{ n: 'AC-CIA-New', c: 'Activation', s: 936, app: 16.7, dep: { ssisa: 0.6, psa: 2.8, gia: 0.6, eas: 0.5, cisa: 0.7, cia: 12.7 } },
{ n: 'AC-CIA-Existing', c: 'Activation', s: 816, app: 11.6, dep: { ssisa: 1.5, psa: 1.3, gia: 0.7, eas: 1.1, cisa: 2.0, cia: 2.9 } },
{ n: 'RT-CIA_Withdrawal_Cross-sell_EAS', c: 'Retention', s: 728, app: 32.7, dep: { ssisa: 0.7, psa: 1.9, gia: 0.4, eas: 0.4, cisa: 1.4, cia: 25.3 } },
{ n: 'AC-CISA-New', c: 'Activation', s: 522, app: 11.5, dep: { ssisa: 1.0, psa: 1.3, gia: 0.6, eas: 1.3, cisa: 5.6, cia: 1.7 } },
{ n: 'RT-PSA_January_Winners_Announcement', c: 'Retention', s: 428, app: 37.6, dep: { ssisa: 2.8, psa: 10.3, gia: 1.2, eas: 0.9, cisa: 0.7, cia: 5.4 } },
]
},
mar: {
label: 'March 2026',
days: 31, elapsed: 31, partial: false,
comms: [
{ n: 'MK-PSA-18-03-2026-PSA500_Week3', c: 'Marketing - PSA', s: 439000, app: 15.1, dep: { ssisa: 0.8, psa: 2.4, gia: 0.3, eas: 0.6, cisa: 3.2, cia: 3.1 } },
{ n: 'MK-ALL-10-03-2026-One_month_to_go_ISA_Season', c: 'Marketing - All', s: 413000, app: 12.9, dep: { ssisa: 0.6, psa: 1.8, gia: 0.2, eas: 0.5, cisa: 2.3, cia: 2.7 } },
{ n: 'MK-ALL-24-03-2026-One_week_to_go_ISA_Season', c: 'Marketing - All', s: 409000, app: 18.6, dep: { ssisa: 1.3, psa: 2.7, gia: 0.4, eas: 1.0, cisa: 5.2, cia: 4.6 } },
{ n: 'MK-PSA-27-03-2026-PSA500_LastChance', c: 'Marketing - PSA', s: 269000, app: 13.9, dep: { ssisa: 1.1, psa: 2.1, gia: 0.3, eas: 0.7, cisa: 3.7, cia: 3.1 } },
{ n: 'MK-MULTI-26-03-2026-Payday_Reminder', c: 'Marketing - Multi', s: 268000, app: 6.0, dep: { ssisa: 0.7, psa: 1.3, gia: 0.2, eas: 0.5, cisa: 2.3, cia: 2.3 } },
{ n: 'MK-CISA-20-03-2026-Allowance_reminder', c: 'Marketing - CB ISA', s: 131000, app: 7.7, dep: { ssisa: 1.0, psa: 1.4, gia: 0.2, eas: 0.5, cisa: 5.6, cia: 2.4 } },
{ n: 'MK-INVS-19-03-2026-ii-15-launch', c: 'Marketing - Investments', s: 109000, app: 11.4, dep: { ssisa: 0.3, psa: 1.7, gia: 0.1, eas: 0.6, cisa: 2.2, cia: 3.6 } },
{ n: 'MK-ALL-PSA500_IAM_13032026', c: 'Marketing - All', s: 98600, app: 53.1, dep: { ssisa: 2.9, psa: 7.6, gia: 0.9, eas: 2.4, cisa: 12.5, cia: 10.8 } },
{ n: 'MK-INVS-13-03-2026-News_W20', c: 'Marketing - Investments', s: 54700, app: 0.7, dep: { ssisa: 0.1, psa: 0.1, gia: 0.0, eas: 0.0, cisa: 0.1, cia: 0.1 } },
{ n: 'MK-MULTI-28-03-2026-Founder_Letter_MaxedOut', c: 'Marketing - Multi', s: 28300, app: 12.1, dep: { ssisa: 0.3, psa: 1.3, gia: 0.5, eas: 1.0, cisa: 1.2, cia: 2.5 } },
{ n: 'PR-ALL-22-01-2026-investments_survey_ongoing', c: 'Pre-activation', s: 17300, app: 64.7, dep: { ssisa: 14.6, psa: 7.8, gia: 4.1, eas: 2.6, cisa: 12.2, cia: 11.2 } },
{ n: 'RT-Reactivation_V2', c: 'Retention', s: 13400, app: 3.1, dep: { ssisa: 0.1, psa: 0.6, gia: 0.0, eas: 0.1, cisa: 0.4, cia: 0.8 } },
{ n: 'AC-SSISA_v3', c: 'Activation', s: 5730, app: 27.0, dep: { ssisa: 6.6, psa: 3.1, gia: 0.7, eas: 0.8, cisa: 5.5, cia: 4.9 } },
{ n: 'PR-ALL-07-03-2026-Wealth_Plan_Survey', c: 'Pre-activation', s: 5230, app: 16.9, dep: { ssisa: 4.7, psa: 6.2, gia: 1.4, eas: 2.4, cisa: 4.8, cia: 9.5 } },
{ n: 'MK-PSA-26-02-2026-PSA_Referral_500Entries', c: 'Marketing - PSA', s: 5080, app: 86.8, dep: { ssisa: 4.9, psa: 19.6, gia: 2.1, eas: 3.0, cisa: 8.5, cia: 13.3 } },
{ n: 'RT-SSISA_Balance_Build', c: 'Retention', s: 4910, app: 19.0, dep: { ssisa: 4.1, psa: 3.0, gia: 0.2, eas: 0.5, cisa: 3.3, cia: 3.8 } },
{ n: 'GR-Maxed_Out_ISA_Allowance_v2', c: 'Growth', s: 2500, app: 22.8, dep: { ssisa: 0.9, psa: 1.6, gia: 0.3, eas: 0.7, cisa: 1.2, cia: 2.6 } },
{ n: 'AC-GIA', c: 'Activation', s: 2410, app: 23.8, dep: { ssisa: 2.6, psa: 4.2, gia: 4.1, eas: 1.2, cisa: 4.1, cia: 5.2 } },
{ n: 'RT-CIA_Withdrawal_Cross-sell_EAS_v2', c: 'Retention', s: 2400, app: 21.8, dep: { ssisa: 0.6, psa: 1.6, gia: 0.3, eas: 0.7, cisa: 1.8, cia: 17.6 } },
{ n: 'MK-ALL-27-02-2026-Referral_£50_Incentive', c: 'Marketing - All', s: 2240, app: 56.1, dep: { ssisa: 1.9, psa: 1.5, gia: 1.3, eas: 2.3, cisa: 9.7, cia: 7.7 } },
{ n: 'AB-SignUp_Abandonment', c: 'Abandonment', s: 1780, app: 0.5, dep: { ssisa: 0.0, psa: 0.0, gia: 0.0, eas: 0.0, cisa: 0.0, cia: 0.2 } },
{ n: 'AC-CIA-New', c: 'Activation', s: 1680, app: 9.5, dep: { ssisa: 0.5, psa: 1.1, gia: 0.0, eas: 0.1, cisa: 0.5, cia: 8.9 } },
{ n: 'PR-Onboarding_Mar26-PSA500', c: 'Pre-activation', s: 1670, app: 3.9, dep: { ssisa: 0.4, psa: 0.7, gia: 0.1, eas: 0.5, cisa: 1.0, cia: 0.7 } },
{ n: 'AC-EAS-Existing', c: 'Activation', s: 1550, app: 18.8, dep: { ssisa: 1.0, psa: 2.6, gia: 0.8, eas: 6.6, cisa: 2.7, cia: 5.1 } },
{ n: 'MK-ALL-Goal_Created', c: 'Marketing - All', s: 1440, app: 16.2, dep: { ssisa: 3.8, psa: 4.6, gia: 1.6, eas: 2.9, cisa: 3.9, cia: 6.0 } },
{ n: 'AC-CISA-Existing', c: 'Activation', s: 1360, app: 12.3, dep: { ssisa: 1.4, psa: 2.5, gia: 0.4, eas: 0.8, cisa: 4.0, cia: 3.4 } },
{ n: 'RT-Issue_4_EAS_auto_application', c: 'Retention', s: 1310, app: 14.5, dep: { ssisa: 1.9, psa: 3.2, gia: 0.7, eas: 5.1, cisa: 2.8, cia: 5.2 } },
{ n: 'AB-Investing_V2', c: 'Abandonment', s: 1300, app: 23.3, dep: { ssisa: 2.6, psa: 3.3, gia: 0.6, eas: 1.6, cisa: 6.9, cia: 5.4 } },
{ n: 'AC-CISA-New', c: 'Activation', s: 671, app: 11.6, dep: { ssisa: 0.4, psa: 1.0, gia: 0.3, eas: 0.3, cisa: 8.2, cia: 0.1 } },
{ n: 'RT-PSA_February_Winners_Announcement_2026', c: 'Retention', s: 429, app: 31.0, dep: { ssisa: 1.4, psa: 8.9, gia: 1.2, eas: 0.2, cisa: 0.7, cia: 2.8 } },
]
},
apr: {
label: 'April 2026',
days: 30, elapsed: 30, partial: false,
comms: [
{ n: 'MK-MULTI-02-04-2026-SpringDraw_Week1', c: 'Marketing - Multi', s: 744000, app: 3.5, dep: { ssisa: 0.2, psa: 0.5, gia: 0.1, eas: 0.1, cisa: 0.7, cia: 0.8, smisa: 0.3 } },
{ n: 'MK-ALL-06-04-2026-ISA_Allowance_Reset_Push', c: 'Marketing - All', s: 498000, app: 2.6, dep: { ssisa: 0.1, psa: 0.1, gia: 0.0, eas: 0.1, cisa: 0.3, cia: 0.2, smisa: 0.2 } },
{ n: 'MK-MULTI-17-04-2026-SpringDraw_Week3', c: 'Marketing - Multi', s: 489000, app: 16.6, dep: { ssisa: 1.0, psa: 1.7, gia: 0.3, eas: 0.6, cisa: 2.8, cia: 3.5, smisa: 0.6 } },
{ n: 'MK-MULTI-13-04-2026-SpringDraw_Week2', c: 'Marketing - Multi', s: 451000, app: 14.8, dep: { ssisa: 0.9, psa: 1.5, gia: 0.3, eas: 0.5, cisa: 2.1, cia: 3.0, smisa: 0.8 } },
{ n: 'MK-SISA-08-04-2026-Smart_Cash_ISA_Launch_(Resend)', c: 'Marketing - Smart ISA', s: 445000, app: 18.7, dep: { ssisa: 0.8, psa: 1.3, gia: 0.2, eas: 0.6, cisa: 2.5, cia: 3.0, smisa: 1.1 } },
{ n: 'MK-MULTI-22-04-2026-SpringDraw_Week4', c: 'Marketing - Multi', s: 439000, app: 15.1, dep: { ssisa: 1.5, psa: 2.2, gia: 0.4, eas: 0.9, cisa: 3.7, cia: 4.4, smisa: 1.0 } },
{ n: 'MK-MULTI-15-04-2026-Tax_Year_26/27_promo', c: 'Marketing - Multi', s: 313000, app: 19.1, dep: { ssisa: 1.2, psa: 1.0, gia: 0.2, eas: 0.7, cisa: 2.9, cia: 1.9, smisa: 1.1 } },
{ n: 'MK-ALL-07-04-2026-New_Tax_Year', c: 'Marketing - All', s: 267000, app: 28.7, dep: { ssisa: 1.6, psa: 2.5, gia: 0.4, eas: 1.1, cisa: 4.8, cia: 5.3, smisa: 1.9 } },
{ n: 'MK-SISA-27-04-2026-Internal_transfer_nudge', c: 'Marketing - Smart ISA', s: 160000, app: 15.1, dep: { ssisa: 1.3, psa: 1.0, gia: 0.3, eas: 0.5, cisa: 5.6, cia: 2.0, smisa: 1.3 } },
{ n: 'MK-IAM-01-04-2026-SpringDraw_Week1', c: 'Marketing - Other', s: 145000, app: 46.6, dep: { ssisa: 2.4, psa: 4.2, gia: 0.6, eas: 1.8, cisa: 11.7, cia: 9.1, smisa: 1.9 } },
{ n: 'MK-MULTI-28-04-2026-Tax_Year_26/27_promo_last_chance_v2', c: 'Marketing - Multi', s: 128000, app: 7.8, dep: { ssisa: 0.8, psa: 1.0, gia: 0.2, eas: 0.6, cisa: 2.3, cia: 2.0, smisa: 1.1 } },
{ n: 'MK-MULTI-22-04-2026-Tax_Year_26/27_promo_expansion', c: 'Marketing - Multi', s: 116000, app: 8.7, dep: { ssisa: 0.0, psa: 1.6, gia: 0.2, eas: 0.1, cisa: 0.1, cia: 4.4, smisa: 0.1 } },
{ n: 'MK-MULTI-20-04-2026-PSA_Winner_SpringDraw', c: 'Marketing - Multi', s: 67100, app: 11.1, dep: { ssisa: 1.4, psa: 4.0, gia: 0.4, eas: 0.9, cisa: 2.3, cia: 4.4, smisa: 1.1 } },
{ n: 'MK-SISA-08-04-2026-Smart_Cash_ISA_Launch', c: 'Marketing - Smart ISA', s: 43600, app: 10.6, dep: { ssisa: 0.9, psa: 1.3, gia: 0.3, eas: 0.5, cisa: 1.9, cia: 2.7, smisa: 1.0 } },
{ n: 'RT-Reactivation_V2', c: 'Retention', s: 36800, app: 2.1, dep: { ssisa: 0.1, psa: 0.2, gia: 0.0, eas: 0.0, cisa: 0.3, cia: 0.4, smisa: 0.0 } },
{ n: 'PR-ALL-22-01-2026-investments_survey_ongoing', c: 'Pre-activation', s: 11100, app: 61.7, dep: { ssisa: 14.5, psa: 6.1, gia: 3.5, eas: 2.6, cisa: 10.5, cia: 10.3, smisa: 5.1 } },
{ n: 'RT-PSA_March_Winners_Announcement_2026', c: 'Retention', s: 10900, app: 52.2, dep: { ssisa: 2.5, psa: 10.6, gia: 1.0, eas: 1.5, cisa: 5.4, cia: 6.4, smisa: 2.5 } },
{ n: 'MK-SISA-07-04-2026-Smart_Cash_ISA_Welcome', c: 'Marketing - Smart ISA', s: 8350, app: 37.0, dep: { ssisa: 3.3, psa: 3.0, gia: 0.8, eas: 2.0, cisa: 2.5, cia: 7.2, smisa: 17.8 } },
{ n: 'AB-Onboarding_Product_Abandonment', c: 'Abandonment', s: 6830, app: 11.7, dep: { ssisa: 0.0, psa: 0.0, gia: 0.0, eas: 0.0, cisa: 1.3, cia: 0.0, smisa: 0.0 } },
{ n: 'PR-SISA-22-04-2026-Onboarding_drop_off_survey', c: 'Pre-activation', s: 4320, app: 79.2, dep: { ssisa: 9.4, psa: 5.9, gia: 1.7, eas: 3.6, cisa: 23.7, cia: 10.4, smisa: 3.8 } },
{ n: 'RT-SSISA_Balance_Build', c: 'Retention', s: 4920, app: 25.3, dep: { ssisa: 6.9, psa: 2.5, gia: 1.0, eas: 1.0, cisa: 3.4, cia: 4.8, smisa: 1.5 } },
{ n: 'PR-MULTI-21-04-2026-Transfer_out_survey', c: 'Pre-activation', s: 5210, app: 5.5, dep: { ssisa: 0.3, psa: 0.5, gia: 0.5, eas: 0.2, cisa: 0.0, cia: 0.9, smisa: 0.2 } },
{ n: 'GR-Maxed_Out_ISA_Allowance_v2', c: 'Growth', s: 5400, app: 25.0, dep: { ssisa: 0.6, psa: 0.6, gia: 0.2, eas: 1.1, cisa: 2.6, cia: 1.7, smisa: 1.6 } },
{ n: 'AC-SSISA_v3', c: 'Activation', s: 5720, app: 29.7, dep: { ssisa: 7.6, psa: 2.3, gia: 0.5, eas: 0.9, cisa: 5.0, cia: 4.0, smisa: 2.8 } },
{ n: 'AC-GIA', c: 'Activation', s: 1910, app: 24.1, dep: { ssisa: 2.5, psa: 1.7, gia: 2.9, eas: 1.5, cisa: 4.3, cia: 4.7, smisa: 1.6 } },
{ n: 'RT-CIA_Withdrawal_Cross-sell_EAS_v2', c: 'Retention', s: 1880, app: 20.6, dep: { ssisa: 1.1, psa: 1.1, gia: 0.3, eas: 1.3, cisa: 2.0, cia: 10.9, smisa: 1.4 } },
{ n: 'MK-ALL-Goal_Created', c: 'Marketing - All', s: 1740, app: 16.5, dep: { ssisa: 2.9, psa: 2.7, gia: 0.5, eas: 2.2, cisa: 2.7, cia: 5.0, smisa: 2.1 } },
{ n: 'AC-EAS-Existing', c: 'Activation', s: 1550, app: 18.8, dep: { ssisa: 1.0, psa: 2.6, gia: 0.8, eas: 6.6, cisa: 2.7, cia: 5.1, smisa: 0.2 } },
{ n: 'AC-CISA-Existing', c: 'Activation', s: 1210, app: 15.8, dep: { ssisa: 1.9, psa: 2.5, gia: 0.5, eas: 0.5, cisa: 3.1, cia: 3.8, smisa: 1.1 } },
{ n: 'AC-CIA-New', c: 'Activation', s: 1070, app: 7.7, dep: { ssisa: 0.2, psa: 0.5, gia: 0.0, eas: 0.4, cisa: 0.3, cia: 4.9, smisa: 0.0 } },
{ n: 'AC-CISA-New', c: 'Activation', s: 719, app: 12.1, dep: { ssisa: 0.7, psa: 0.4, gia: 0.0, eas: 0.8, cisa: 7.4, cia: 0.6, smisa: 0.0 } },
]
},
may: {
label: 'May 2026 (to 28 May)',
days: 31, elapsed: 28, partial: true,
comms: [
{ n: 'MK-MULTI-21-05-2026-SpringDraw_Week8', c: 'Marketing - Multi', s: 719000, app: 5.4, dep: { ssisa: 0.4, psa: 0.6, gia: 0.1, eas: 0.2, cisa: 0.8, cia: 1.2, smisa: 0.2 } },
{ n: 'MK-MULTI-07-05-2026-SpringDraw_Week6', c: 'Marketing - Multi', s: 261000, app: 11.7, dep: { ssisa: 0.8, psa: 1.3, gia: 0.2, eas: 0.5, cisa: 1.5, cia: 2.4, smisa: 0.6 } },
{ n: 'MK-MULTI-13-05-2026-Monthly_Interest_update', c: 'Marketing - Multi', s: 169000, app: 10.3, dep: { ssisa: 0.9, psa: 1.5, gia: 0.3, eas: 0.6, cisa: 1.6, cia: 3.1, smisa: 1.1 } },
{ n: 'MK-MULTI-15-05-2026-SpringDraw_Week7', c: 'Marketing - Multi', s: 132000, app: 13.4, dep: { ssisa: 1.2, psa: 1.8, gia: 0.3, eas: 0.6, cisa: 2.1, cia: 3.3, smisa: 0.7 } },
{ n: 'MK-MULTI-15-05-2026-SpringDraw_Week7_Experiment', c: 'Marketing - Multi', s: 77000, app: 14.9, dep: { ssisa: 1.1, psa: 1.6, gia: 0.3, eas: 0.6, cisa: 2.2, cia: 3.5, smisa: 0.7 } },
{ n: 'MK-MULTI-07-05-2026-SpringDraw_Week6_Experiment', c: 'Marketing - Multi', s: 51000, app: 16.6, dep: { ssisa: 1.2, psa: 1.6, gia: 0.4, eas: 0.6, cisa: 2.1, cia: 3.1, smisa: 0.8 } },
{ n: 'RT-Reactivation_V2', c: 'Retention', s: 19700, app: 2.3, dep: { ssisa: 0.1, psa: 0.3, gia: 0.0, eas: 0.0, cisa: 0.2, cia: 0.5, smisa: 0.1 } },
{ n: 'MULTI_MK-MKT-0428_FREE_ADVICE_TEST_18052026', c: 'Marketing - Multi', s: 17900, app: 6.9, dep: { ssisa: 0.3, psa: 1.0, gia: 0.1, eas: 0.5, cisa: 1.5, cia: 2.4, smisa: 0.6 } },
{ n: 'RT-5.25%_14Day_BoostedRate_Expiry', c: 'Retention', s: 15500, app: 14.9, dep: { ssisa: 0.5, psa: 0.3, gia: 0.1, eas: 0.1, cisa: 3.2, cia: 0.4, smisa: 0.4 } },
{ n: 'RT-PSA_April_Winners_Announcement_2026', c: 'Retention', s: 7510, app: 45.3, dep: { ssisa: 1.7, psa: 11.1, gia: 1.0, eas: 1.0, cisa: 1.8, cia: 5.2, smisa: 0.6 } },
{ n: 'PR-ALL-22-01-2026-investments_survey_ongoing', c: 'Pre-activation', s: 5650, app: 58.0, dep: { ssisa: 13.2, psa: 3.8, gia: 3.5, eas: 2.0, cisa: 7.0, cia: 8.2, smisa: 3.4 } },
{ n: 'RT-CIA_4.76%_14Day_BoostedRate_Expiry', c: 'Retention', s: 3960, app: 14.4, dep: { ssisa: 0.3, psa: 0.4, gia: 0.1, eas: 0.4, cisa: 0.4, cia: 3.9, smisa: 0.3 } },
{ n: 'AC-SSISA_v3', c: 'Activation', s: 3240, app: 31.9, dep: { ssisa: 8.2, psa: 1.4, gia: 0.7, eas: 1.1, cisa: 2.8, cia: 3.5, smisa: 1.8 } },
{ n: 'MK-SISA-07-04-2026-Smart_Cash_ISA_Welcome', c: 'Marketing - Smart ISA', s: 3190, app: 34.9, dep: { ssisa: 3.1, psa: 2.2, gia: 0.6, eas: 1.5, cisa: 1.5, cia: 4.6, smisa: 16.0 } },
{ n: 'RT-SSISA_Balance_Build', c: 'Retention', s: 2890, app: 23.1, dep: { ssisa: 6.5, psa: 1.5, gia: 0.9, eas: 1.2, cisa: 2.0, cia: 2.6, smisa: 1.1 } },
{ n: 'MK-INVS-20-05-2026-0%_Fees_Promotion', c: 'Marketing - Investments', s: 2720, app: 5.5, dep: { ssisa: 0.2, psa: 0.4, gia: 0.2, eas: 0.2, cisa: 1.4, cia: 0.6, smisa: 0.4 } },
{ n: 'RT-EAS_14Day_BoostedRate_Expiry', c: 'Retention', s: 1800, app: 14.5, dep: { ssisa: 0.1, psa: 0.5, gia: 0.4, eas: 1.0, cisa: 0.1, cia: 0.3, smisa: 0.0 } },
{ n: 'PR-Onboarding', c: 'Pre-activation', s: 1500, app: 1.9, dep: { ssisa: 0.2, psa: 0.5, gia: 0.0, eas: 0.0, cisa: 0.1, cia: 0.3, smisa: 0.2 } },
{ n: 'GR-Maxed_Out_ISA_Allowance_v2', c: 'Growth', s: 1350, app: 23.2, dep: { ssisa: 0.8, psa: 1.0, gia: 0.5, eas: 1.4, cisa: 0.7, cia: 2.1, smisa: 0.9 } },
{ n: 'MK-ALL-Goal_Created', c: 'Marketing - All', s: 1240, app: 16.4, dep: { ssisa: 2.8, psa: 3.1, gia: 0.8, eas: 1.9, cisa: 2.5, cia: 4.8, smisa: 2.6 } },
{ n: 'AC-GIA', c: 'Activation', s: 1180, app: 28.1, dep: { ssisa: 3.2, psa: 1.9, gia: 3.6, eas: 2.0, cisa: 2.4, cia: 4.6, smisa: 1.9 } },
{ n: 'RT-CIA_Withdrawal_Cross-sell_EAS_v2', c: 'Retention', s: 1130, app: 14.9, dep: { ssisa: 0.3, psa: 0.8, gia: 0.1, eas: 0.8, cisa: 0.8, cia: 8.2, smisa: 0.8 } },
{ n: 'AB-Onboarding_Product_Abandonment', c: 'Abandonment', s: 1070, app: 2.8, dep: { ssisa: 0.0, psa: 0.0, gia: 0.0, eas: 0.0, cisa: 0.0, cia: 0.0, smisa: 0.0 } },
{ n: 'AB-Investing_V2', c: 'Abandonment', s: 1050, app: 26.1, dep: { ssisa: 3.0, psa: 2.1, gia: 0.6, eas: 2.3, cisa: 4.7, cia: 4.3, smisa: 1.6 } },
{ n: 'GR-Investments_Beginners', c: 'Growth', s: 1030, app: 6.3, dep: { ssisa: 0.0, psa: 2.8, gia: 0.0, eas: 0.6, cisa: 0.9, cia: 1.2, smisa: 0.4 } },
{ n: 'AB-SSISA_Transfers_V2', c: 'Abandonment', s: 950, app: 42.7, dep: { ssisa: 9.4, psa: 3.6, gia: 2.7, eas: 1.6, cisa: 3.2, cia: 5.6, smisa: 2.6 } },
{ n: 'RT-Investments_Survey_1M', c: 'Retention', s: 931, app: 20.6, dep: { ssisa: 5.9, psa: 1.4, gia: 1.2, eas: 0.6, cisa: 1.3, cia: 2.8, smisa: 1.0 } },
{ n: 'PR-SSISA_v2', c: 'Pre-activation', s: 777, app: 31.1, dep: { ssisa: 8.8, psa: 1.2, gia: 1.3, eas: 1.0, cisa: 1.7, cia: 1.2, smisa: 3.7 } },
{ n: 'AC-CIA-New', c: 'Activation', s: 748, app: 7.4, dep: { ssisa: 0.4, psa: 0.7, gia: 0.0, eas: 0.3, cisa: 0.0, cia: 4.0, smisa: 0.1 } },
{ n: 'AC-CISA-Existing', c: 'Activation', s: 683, app: 19.9, dep: { ssisa: 1.0, psa: 2.5, gia: 0.7, eas: 1.2, cisa: 0.7, cia: 3.4, smisa: 1.9 } },
{ n: 'GR-Investments_Experienced', c: 'Growth', s: 613, app: 8.5, dep: { ssisa: 0.0, psa: 0.7, gia: 0.2, eas: 0.0, cisa: 0.3, cia: 4.2, smisa: 0.5 } },
{ n: 'GR-Investments_Intermediate', c: 'Growth', s: 465, app: 7.1, dep: { ssisa: 0.0, psa: 1.5, gia: 0.0, eas: 0.4, cisa: 1.5, cia: 2.8, smisa: 0.4 } },
{ n: 'AC-CISA-New', c: 'Activation', s: 424, app: 9.9, dep: { ssisa: 0.5, psa: 0.2, gia: 0.0, eas: 0.2, cisa: 5.7, cia: 0.2, smisa: 1.7 } },
{ n: 'AC-CIA-Existing', c: 'Activation', s: 287, app: 12.5, dep: { ssisa: 0.3, psa: 1.4, gia: 0.7, eas: 2.1, cisa: 1.0, cia: 3.8, smisa: 0.3 } },
]
}
};
// ───────────────────────────────────────────────────────────
// HELPERS
// ───────────────────────────────────────────────────────────
function getCSS(v) {
return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
}
const fmt = (n) => {
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
if (n >= 1e4) return (n/1e3).toFixed(0) + 'k';
if (n >= 1e3) return (n/1e3).toFixed(2) + 'k';
return Math.round(n).toLocaleString();
};
const lifecycleOf = (cat) => CAT_TO_LIFECYCLE[cat] || 'OT';
const lifecycleName = (l) => ({
AC: 'Activation', PR: 'Surveys / Research', RT: 'Retention',
GR: 'Growth (X-Sell)', AB: 'Abandonment', SO: 'Solus', OT: 'Other'
})[l] || l;
const PRODUCTS = ['ssisa','cia','psa','eas','cisa','gia','smisa'];
const PRODUCT_LABEL = { ssisa: 'S&S ISA', cia: 'CIA', psa: 'PSA', eas: 'EAS', cisa: 'CISA', gia: 'GIA', smisa: 'SISA', sipp: 'SIPP', multi: 'Multi/All' };
const PRODUCT_COLOR = {
ssisa: getCSS('--p-ssisa'), cia: getCSS('--p-cia'),
psa: getCSS('--p-psa'), eas: getCSS('--p-eas'),
cisa: getCSS('--p-cisa'), gia: getCSS('--p-gia'),
smisa: getCSS('--p-smisa'), sipp: getCSS('--p-sipp'),
multi: getCSS('--p-multi')
};
const LIFECYCLE_COLOR = {
AC: getCSS('--acc-sage'), PR: getCSS('--acc-cream'),
RT: getCSS('--acc-blue'), GR: getCSS('--acc-violet'),
AB: getCSS('--acc-rust'), SO: getCSS('--acc-burnt'),
OT: getCSS('--t-3')
};
function totalDepRate(comm) {
return PRODUCTS.reduce((a, p) => a + (comm.dep[p] || 0), 0);
}
function rankImpact(comm) {
return comm.s * totalDepRate(comm);
}
function totalSends(month) { return month.comms.reduce((a,c) => a + c.s, 0); }
function totalOpens(month) { return month.comms.reduce((a,c) => a + c.s * (c.app/100), 0); }
function weightedAvgOpenRate(month) {
const sends = totalSends(month);
return sends > 0 ? (totalOpens(month) / sends) * 100 : 0;
}
function weightedAvgDepRate(month) {
const sends = totalSends(month);
const wSum = month.comms.reduce((a,c) => a + c.s * totalDepRate(c), 0);
return sends > 0 ? (wSum / sends) : 0;
}
function depRateForProduct(month, product) {
const sends = totalSends(month);
const wSum = month.comms.reduce((a,c) => a + c.s * (c.dep[product] || 0), 0);
return sends > 0 ? (wSum / sends) : 0;
}
function depRateForLifecycle(month, lifecycle) {
const filtered = month.comms.filter(c => lifecycleOf(c.c) === lifecycle);
const sends = filtered.reduce((a,c) => a + c.s, 0);
const wSum = filtered.reduce((a,c) => a + c.s * totalDepRate(c), 0);
return sends > 0 ? (wSum / sends) : 0;
}
function sendsByLifecycle(month) {
const out = {};
month.comms.forEach(c => {
const l = lifecycleOf(c.c);
out[l] = (out[l] || 0) + c.s;
});
return out;
}
function topProduct(comm) {
let best = { product: '-', pct: 0 };
PRODUCTS.forEach(p => {
const v = comm.dep[p] || 0;
if (v > best.pct) best = { product: PRODUCT_LABEL[p], pct: v };
});
return best;
}
function topRateComm(month) {
return month.comms.reduce((best, c) => {
const r = totalDepRate(c);
if (!best || r > best.r) return { name: c.n, r, sends: c.s };
return best;
}, null);
}
function primaryProductTarget(comm) {
const cat = comm.c;
if (cat === 'Marketing - PSA') return 'psa';
if (cat === 'Marketing - EAS') return 'eas';
if (cat === 'Marketing - CB ISA') return 'cisa';
if (cat === 'Marketing - Investments') return 'gia';
if (cat === 'Marketing - Smart ISA') return 'smisa';
if (cat === 'Marketing - All' || cat === 'Marketing - Multi') return 'multi';
const name = comm.n.toUpperCase();
if (name.includes('SSISA') || name.includes('S&S')) return 'ssisa';
if (name.includes('CISA')) return 'cisa';
if (name.includes('SISA') || name.includes('SMART_CASH') || name.includes('SMART_ISA')) return 'smisa';
if (name.includes('-PSA') || name.includes('_PSA') || name.includes('PSA500')) return 'psa';
if (name.includes('-EAS') || name.includes('_EAS')) return 'eas';
if (name.includes('-INVS') || name.includes('INVEST')) return 'gia';
if (name.includes('-CIA') || name.includes('_CIA')) return 'cia';
if (name.includes('-GIA') || name.includes('_GIA')) return 'gia';
return 'multi';
}
function sendsByProductTarget(month) {
const out = {};
month.comms.forEach(c => {
const p = primaryProductTarget(c);
out[p] = (out[p] || 0) + c.s;
});
return out;
}
// ───────────────────────────────────────────────────────────
// STATE
// ───────────────────────────────────────────────────────────
let activeMonth = 'may';
let activeTab = 'summary';
let commFilter = 'all';
const MONTHS = ['jan','feb','mar','apr','may'];
const MONTH_SHORT = { jan: 'Jan', feb: 'Feb', mar: 'Mar', apr: 'Apr', may: 'May' };
function priorMonthKey(m) {
const i = MONTHS.indexOf(m);
return i > 0 ? MONTHS[i-1] : null;
}
// ───────────────────────────────────────────────────────────
// RENDERING — SUMMARY TAB
// ───────────────────────────────────────────────────────────
function renderKPIs() {
const m = DATA[activeMonth];
const sends = totalSends(m);
const openRate = weightedAvgOpenRate(m);
const depRate = weightedAvgDepRate(m);
const topRate = topRateComm(m);
let priorM, priorLabel, factor;
if (m.partial) {
priorM = DATA.apr;
priorLabel = `vs Day ${m.elapsed} Apr`;
factor = m.elapsed / priorM.days;
} else {
const pk = priorMonthKey(activeMonth);
priorM = pk ? DATA[pk] : null;
priorLabel = priorM ? `vs ${MONTH_SHORT[pk]}` : '';
factor = 1;
}
// For rates, no factor adjustment — they're per-comm, not period-scaled
const priorSends = priorM ? totalSends(priorM) * factor : 0;
const priorOpenRate = priorM ? weightedAvgOpenRate(priorM) : 0;
const priorDepRate = priorM ? weightedAvgDepRate(priorM) : 0;
const sDelta = priorM && priorSends ? ((sends - priorSends) / priorSends) * 100 : 0;
const oDelta = priorM && priorOpenRate ? ((openRate - priorOpenRate) / priorOpenRate) * 100 : 0;
const dDelta = priorM && priorDepRate ? ((depRate - priorDepRate) / priorDepRate) * 100 : 0;
const fmtDelta = (d) => {
if (!priorM) return '';
const sign = d >= 0 ? '+' : '';
const cls = Math.abs(d) < 3 ? 'flat' : (d > 0 ? 'up' : 'down');
return `<span class="delta ${cls}">${sign}${d.toFixed(1)}%</span>`;
};
// Truncate long comm names
const truncName = (s, n = 36) => s.length > n ? s.slice(0, n - 1) + '…' : s;
document.getElementById('kpiRow').innerHTML = `
<div class="kpi">
<div class="eyebrow">Recipient Sends</div>
<div class="number">${fmt(sends)}<span class="unit"></span></div>
<div class="compare">${fmtDelta(sDelta)} <span>${priorLabel}</span></div>
</div>
<div class="kpi">
<div class="eyebrow">Weighted-avg app-open rate</div>
<div class="number">${openRate.toFixed(1)}<span class="unit">%</span></div>
<div class="compare">${fmtDelta(oDelta)} <span>${priorLabel}</span></div>
</div>
<div class="kpi">
<div class="eyebrow">Weighted-avg deposit rate</div>
<div class="number">${depRate.toFixed(2)}<span class="unit">%</span></div>
<div class="compare">${fmtDelta(dDelta)} <span>${priorLabel}</span></div>
</div>
<div class="kpi">
<div class="eyebrow">Top deposit-rate comm</div>
<div class="number compact">${topRate ? topRate.r.toFixed(1) : '0'}<span class="unit">%</span></div>
<div class="sub">${topRate ? truncName(topRate.name) : '—'}</div>
</div>
`;
}
function renderTopContributors() {
const m = DATA[activeMonth];
const sorted = [...m.comms]
.map(c => ({ ...c, impact: rankImpact(c), tdr: totalDepRate(c), tp: topProduct(c), lc: lifecycleOf(c.c), tp_type: typeOf(c.n) }))
.sort((a,b) => b.impact - a.impact)
.slice(0, 15);
const tbody = document.querySelector('#topContributorsTable tbody');
tbody.innerHTML = sorted.map(c => `
<tr>
<td>${c.n}</td>
<td><span class="type-badge ${c.tp_type === 'Solus' ? 'solus' : 'auto'}">${c.tp_type}</span></td>
<td><span class="cat-badge ${c.lc}">${c.lc}</span></td>
<td class="r">${fmt(c.s)}</td>
<td class="r dim">${c.tp.product} (${c.tp.pct.toFixed(1)}%)</td>
<td class="r"><strong>${c.tdr.toFixed(1)}%</strong></td>
</tr>
`).join('');
}
let charts = {};
function destroyChart(name) { if (charts[name]) { charts[name].destroy(); delete charts[name]; } }
function destroyAllCharts() { Object.keys(charts).forEach(destroyChart); }
const baseChartOpts = (fmtFn) => ({
responsive: true,
maintainAspectRatio: false,
animation: { duration: 600, easing: 'easeOutQuart' },
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
display: true, position: 'bottom',
labels: { color: getCSS('--t-2'), boxWidth: 10, padding: 14, font: { size: 11, family: 'IBM Plex Mono' } }
},
tooltip: {
backgroundColor: getCSS('--bg-2'),
borderColor: getCSS('--border-strong'),
borderWidth: 1,
titleColor: getCSS('--t-1'),
bodyColor: getCSS('--t-2'),
padding: 10,
titleFont: { family: 'IBM Plex Sans', size: 12, weight: '500' },
bodyFont: { family: 'IBM Plex Mono', size: 11 },
callbacks: fmtFn ? { label: (ctx) => `${ctx.dataset.label}: ${fmtFn(ctx.parsed.y)}` } : {}
}
},
scales: {
x: {
grid: { color: getCSS('--grid') },
ticks: { color: getCSS('--tick'), font: { family: 'IBM Plex Mono', size: 11 } },
border: { color: 'transparent' }
},
y: {
grid: { color: getCSS('--grid') },
ticks: { color: getCSS('--tick'), font: { family: 'IBM Plex Mono', size: 11 }, callback: fmtFn || (v => v) },
border: { color: 'transparent' }
}
}
});
const fmtPct = (v) => `${(v).toFixed(2)}%`;
function renderTrajectoryCharts() {
destroyChart('sends');
destroyChart('depRate');
const labels = MONTHS.map(m => DATA[m].label.replace(' 2026','').replace(' (to 28 May)',' MTD'));
const sendData = MONTHS.map(m => totalSends(DATA[m]));
const rateData = MONTHS.map(m => weightedAvgDepRate(DATA[m]));
const sendColors = MONTHS.map(m => m === activeMonth ? getCSS('--acc-cream') : getCSS('--acc-blue'));
const rateColors = MONTHS.map(m => m === activeMonth ? getCSS('--acc-cream') : getCSS('--acc-sage'));
charts.sends = new Chart(document.getElementById('chartSends'), {
type: 'bar',
data: { labels, datasets: [{ label: 'Total sends', data: sendData, backgroundColor: sendColors, borderRadius: 2, barThickness: 36 }] },
options: { ...baseChartOpts(fmt), plugins: { ...baseChartOpts().plugins, legend: { display: false } } }
});
charts.depRate = new Chart(document.getElementById('chartDepRate'), {
type: 'bar',
data: { labels, datasets: [{ label: 'Weighted-avg deposit rate', data: rateData, backgroundColor: rateColors, borderRadius: 2, barThickness: 36 }] },
options: { ...baseChartOpts(fmtPct), plugins: { ...baseChartOpts().plugins, legend: { display: false } } }
});
}
// ───────────────────────────────────────────────────────────
// RENDERING — COMMUNICATIONS TAB
// ───────────────────────────────────────────────────────────
function renderTopPerformers() {
const m = DATA[activeMonth];
document.getElementById('topPerformersMonth').textContent = m.label;
const enriched = m.comms
.map(c => ({ ...c, impact: rankImpact(c), tdr: totalDepRate(c), tp: topProduct(c), tp_type: typeOf(c.n), lc: lifecycleOf(c.c) }))
.sort((a,b) => b.impact - a.impact);
const topSolus = enriched.filter(c => c.tp_type === 'Solus').slice(0, 5);
const topAuto = enriched.filter(c => c.tp_type === 'Automation').slice(0, 5);
const renderList = (items, id) => {
document.getElementById(id).innerHTML = items.length === 0
? `<div class="item"><div class="rank">—</div><div class="name dim">No data in selected month</div><div class="value"></div></div>`
: items.map((c, i) => `
<div class="item">
<div class="rank">${(i+1).toString().padStart(2,'0')}</div>
<div class="name">${c.n}<span class="sub">${c.lc} · ${fmt(c.s)} sends · ${c.tp.product} ${c.tp.pct.toFixed(1)}%</span></div>
<div class="value">${c.tdr.toFixed(1)}%<span class="vsub">Total dep. rate</span></div>
</div>
`).join('');
};
renderList(topSolus, 'topSolusList');
renderList(topAuto, 'topAutomationsList');
}
function renderLifecycleActivity() {
destroyChart('lifecycleActivity');
const LIFECYCLES = ['SO','RT','AC','PR','GR','AB'];
const labels = MONTHS.map(m => DATA[m].label.replace(' 2026','').replace(' (to 28 May)',' MTD'));
const datasets = LIFECYCLES.map(l => ({
label: lifecycleName(l),
data: MONTHS.map(m => (sendsByLifecycle(DATA[m])[l] || 0)),
backgroundColor: LIFECYCLE_COLOR[l],
borderRadius: 2,
barThickness: 40
}));
charts.lifecycleActivity = new Chart(document.getElementById('chartLifecycleActivity'), {
type: 'bar',
data: { labels, datasets },
options: {
...baseChartOpts(fmt),
scales: {
...baseChartOpts().scales,
x: { ...baseChartOpts().scales.x, stacked: true },
y: { ...baseChartOpts().scales.y, stacked: true, ticks: { ...baseChartOpts().scales.y.ticks, callback: fmt } }
}
}
});
}
function renderCommsTable() {
const m = DATA[activeMonth];
document.getElementById('commsMonth').textContent = m.label;
const filtered = m.comms.filter(c => {
const t = typeOf(c.n);
if (commFilter === 'automation') return t === 'Automation';
if (commFilter === 'solus') return t === 'Solus';
return true;
});
const search = document.getElementById('commSearch').value.toLowerCase();
const visible = filtered.filter(c => !search || c.n.toLowerCase().includes(search) || c.c.toLowerCase().includes(search));
const tbody = document.querySelector('#commsTable tbody');
tbody.innerHTML = visible
.map(c => ({ ...c, impact: rankImpact(c), tdr: totalDepRate(c), tp: topProduct(c), lc: lifecycleOf(c.c), tp_type: typeOf(c.n) }))
.sort((a,b) => b.impact - a.impact)
.map(c => `
<tr>
<td>${c.n}</td>
<td><span class="type-badge ${c.tp_type === 'Solus' ? 'solus' : 'auto'}">${c.tp_type}</span></td>
<td><span class="cat-badge ${c.lc}">${c.lc}</span></td>
<td class="r">${fmt(c.s)}</td>
<td class="r dim">${c.app.toFixed(1)}%</td>
<td class="r dim">${c.tp.product}</td>
<td class="r"><strong>${c.tp.pct.toFixed(1)}%</strong></td>
<td class="r"><strong>${c.tdr.toFixed(1)}%</strong></td>
</tr>
`).join('');
}
// ───────────────────────────────────────────────────────────
// RENDERING — LIFECYCLE & PRODUCT TAB
// ───────────────────────────────────────────────────────────
function renderProductTrend() {
destroyChart('productTrend');
const labels = MONTHS.map(m => DATA[m].label.replace(' 2026','').replace(' (to 28 May)',' MTD'));
const datasets = PRODUCTS.map(p => ({
label: PRODUCT_LABEL[p],
data: MONTHS.map(m => depRateForProduct(DATA[m], p)),
borderColor: PRODUCT_COLOR[p],
backgroundColor: PRODUCT_COLOR[p],
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
tension: 0.25,
fill: false
}));
charts.productTrend = new Chart(document.getElementById('chartProductTrend'), {
type: 'line',
data: { labels, datasets },
options: {
...baseChartOpts(fmtPct),
scales: {
...baseChartOpts().scales,
y: { ...baseChartOpts().scales.y, title: { display: true, text: 'Weighted-avg deposit rate (%)', color: getCSS('--t-3'), font: { family: 'IBM Plex Mono', size: 10 } } }
}
}
});
}
function renderLifecycleTrend() {
destroyChart('lifecycleTrend');
const labels = MONTHS.map(m => DATA[m].label.replace(' 2026','').replace(' (to 28 May)',' MTD'));
// Surveys (PR) excluded — they're not a lifecycle, and their self-selected audiences
// distort the lifecycle comparison. Surveys remain visible in the comms table.
const LIFECYCLES = ['SO','RT','AC','GR','AB'];
const datasets = LIFECYCLES.map(l => ({
label: lifecycleName(l),
data: MONTHS.map(m => depRateForLifecycle(DATA[m], l)),
borderColor: LIFECYCLE_COLOR[l],
backgroundColor: LIFECYCLE_COLOR[l],
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
tension: 0.25,
fill: false
}));
charts.lifecycleTrend = new Chart(document.getElementById('chartLifecycleTrend'), {
type: 'line',
data: { labels, datasets },
options: {
...baseChartOpts(fmtPct),
scales: {
...baseChartOpts().scales,
y: { ...baseChartOpts().scales.y, title: { display: true, text: 'Weighted-avg total deposit rate (%)', color: getCSS('--t-3'), font: { family: 'IBM Plex Mono', size: 10 } } }
}
}
});
}
function renderSendTarget() {
destroyChart('sendTarget');
const labels = MONTHS.map(m => DATA[m].label.replace(' 2026','').replace(' (to 28 May)',' MTD'));
const TARGETS = ['multi','psa','cisa','smisa','eas','gia','cia','ssisa'];
const datasets = TARGETS.map(p => ({
label: PRODUCT_LABEL[p],
data: MONTHS.map(m => sendsByProductTarget(DATA[m])[p] || 0),
backgroundColor: PRODUCT_COLOR[p],
borderRadius: 2,
barThickness: 40
}));
charts.sendTarget = new Chart(document.getElementById('chartSendTarget'), {
type: 'bar',
data: { labels, datasets },
options: {
...baseChartOpts(fmt),
scales: {
...baseChartOpts().scales,
x: { ...baseChartOpts().scales.x, stacked: true },
y: { ...baseChartOpts().scales.y, stacked: true, ticks: { ...baseChartOpts().scales.y.ticks, callback: fmt } }
}
}
});
}
function renderDepositPacing() {
destroyChart('depPacing');
const selectedKey = activeMonth;
const priorKey = priorMonthKey(selectedKey);
const selectedM = DATA[selectedKey];
const titleEl = document.getElementById('depPacingTitle');
if (priorKey) {
titleEl.textContent = `Weighted deposit-driving impact — ${selectedM.label} vs ${DATA[priorKey].label} (cumulative)`;
} else {
titleEl.textContent = `Weighted deposit-driving impact — ${selectedM.label}`;
}
const wrap = document.getElementById('depPacingChartWrap');
if (!priorKey) {
wrap.innerHTML = `<div class="empty-state">
<div class="icon">—</div>
<div class="msg">No prior month available in the dataset. Pacing comparison requires the month before the selected month.</div>
</div>`;
return;
}
if (!document.getElementById('chartDepPacing')) {
wrap.innerHTML = `<canvas id="chartDepPacing"></canvas>`;
}
const priorM = DATA[priorKey];
const maxDays = Math.max(selectedM.days, priorM.days);
const dayLabels = Array.from({length: maxDays}, (_, i) => `D${i+1}`);
// Cumulative "deposit-driving impact" = Σ(sends × total dep rate / 100) up to day N
// Approximated by daily-averaging from the monthly aggregate (same approach as the
// legacy Send Pacing chart). Not a unique-depositor count — see Findings · M3.
const impactTotal = (month) => month.comms.reduce((a,c) => a + c.s * (totalDepRate(c) / 100), 0);
const selDaily = impactTotal(selectedM) / selectedM.elapsed;
const priorDaily = impactTotal(priorM) / priorM.elapsed;
const selCumul = dayLabels.map((_, i) => i < selectedM.elapsed ? selDaily * (i+1) : null);
const priorCumul = dayLabels.map((_, i) => i < priorM.elapsed ? priorDaily * (i+1) : null);
charts.depPacing = new Chart(document.getElementById('chartDepPacing'), {
type: 'line',
data: {
labels: dayLabels,
datasets: [
{
label: `${MONTH_SHORT[priorKey]} cumulative impact`,
data: priorCumul,
borderColor: getCSS('--acc-blue'),
backgroundColor: 'transparent',
borderWidth: 2,
pointRadius: 0,
tension: 0.2,
spanGaps: false,
},
{
label: `${MONTH_SHORT[selectedKey]} cumulative impact`,
data: selCumul,
borderColor: getCSS('--acc-burnt'),
backgroundColor: 'transparent',
borderWidth: 2.5,
pointRadius: 0,
tension: 0.2,
spanGaps: false,
}
]
},
options: {
...baseChartOpts(fmt),
scales: {
...baseChartOpts().scales,
y: { ...baseChartOpts().scales.y, title: { display: true, text: 'Cumulative weighted impact (sends × dep rate)', color: getCSS('--t-3'), font: { family: 'IBM Plex Mono', size: 10 } } }
}
}
});
}
// ───────────────────────────────────────────────────────────
// EVENTS
// ───────────────────────────────────────────────────────────
document.querySelectorAll('.tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
document.querySelectorAll('.panel').forEach(x => x.classList.remove('active'));
t.classList.add('active');
activeTab = t.dataset.tab;
document.getElementById('panel-' + activeTab).classList.add('active');
redraw();
});
});
document.querySelectorAll('.month-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('.month-chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
activeMonth = c.dataset.month;
redraw();
});
});
document.querySelectorAll('.pill-btn').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.pill-btn').forEach(x => x.classList.remove('active'));
b.classList.add('active');
commFilter = b.dataset.filter;
renderCommsTable();
});
});
document.getElementById('commSearch').addEventListener('input', renderCommsTable);
function redraw() {
if (activeTab === 'summary') {
renderKPIs();
renderTopContributors();
renderTrajectoryCharts();
} else if (activeTab === 'comms') {
renderTopPerformers();
renderLifecycleActivity();
renderCommsTable();
} else if (activeTab === 'stage') {
renderProductTrend();
renderLifecycleTrend();
renderSendTarget();
renderDepositPacing();
}
}
window.addEventListener('load', () => { redraw(); });
</script>
</body>
</html>