Upload 2 files
Browse files- app.js +33 -14
- index.html +7 -7
app.js
CHANGED
|
@@ -41,7 +41,7 @@ function loadTab(id){
|
|
| 41 |
if(id==='tab-adoption'){buildAdoptionPct();buildAdoptionAbs();buildGrowthSignals();buildTrendBars();}
|
| 42 |
if(id==='tab-competitive'){buildCompShare();buildCompRatio();buildScatterUC();}
|
| 43 |
if(id==='tab-engagement'){buildEngagement();buildScatterEng();}
|
| 44 |
-
if(id==='tab-opportunity'){
|
| 45 |
if(id==='tab-specialty'){buildSpecialtyStack();buildSpecialtyPct();}
|
| 46 |
|
| 47 |
}
|
|
@@ -99,25 +99,44 @@ function buildScatterEng(){const ctx=document.getElementById('chart-scatter-eng'
|
|
| 99 |
new Chart(ctx,{type:'scatter',data:{datasets:[{label:'SEG_A',data:mk(400,5.28,0.0005),backgroundColor:CA+'66',pointRadius:3},{label:'SEG_B',data:mk(300,8.94,0.0018),backgroundColor:CB+'66',pointRadius:3},{label:'SEG_C',data:mk(200,8.71,0.0017),backgroundColor:CC+'66',pointRadius:3}]},options:{maintainAspectRatio:false,responsive:true,plugins:{legend:{position:'bottom'}},scales:{x:{title:{display:true,text:'Total Rep Visits (86 wks)'},grid:{color:'#f1f5f9'}},y:{title:{display:true,text:'Pfizer TRx / week'},grid:{color:'#f1f5f9'}}}}});}
|
| 100 |
|
| 101 |
/* ==================== TAB 6: OPPORTUNITY ==================== */
|
| 102 |
-
function
|
| 103 |
-
const bins=[1750,20,31,30,27,47,295,308,514,1108,890,790,752,754,650,466,258,105,51,8,14,10,18,13,17,27,33,20,16,10];
|
| 104 |
-
const edges=[];for(let i=0;i<=30;i++)edges.push(0.295+i*(0.941-0.295)/30);
|
| 105 |
-
const labels=edges.slice(0,-1).map((e,i)=>((e+edges[i+1])/2).toFixed(2));
|
| 106 |
-
mkBar('chart-opp-hist',labels,[{data:bins,backgroundColor:CU+'cc',borderRadius:2,borderSkipped:false}],{plugins:{legend:{display:false}},x:{ticks:{maxTicksLimit:10,font:{size:10}}}});
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
function buildOpportunityScatter(){
|
| 110 |
fetch('opportunity_data.json').then(r=>r.json()).then(data=>{
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
const ctx=document.getElementById('chart-opp-scatter');if(!ctx)return;
|
| 114 |
const chart=new Chart(ctx,{type:'scatter',data:{datasets:[
|
| 115 |
-
{label:'No Rep Visits',data:nv,backgroundColor:RED+'
|
| 116 |
-
{label:'Covered',data:cv,backgroundColor:CB+'
|
| 117 |
]},options:{maintainAspectRatio:false,responsive:true,
|
| 118 |
plugins:{legend:{position:'bottom'},tooltip:{callbacks:{
|
| 119 |
title:pts=>{const p=pts[0];return p.datasetIndex===0?'ID: '+p.raw.id:'Covered HCP';},
|
| 120 |
-
label:p=>[`UC TRx: ${p.raw.
|
| 121 |
}}},
|
| 122 |
scales:{x:{title:{display:true,text:'UC TRx Mean (weekly)'},grid:{color:'#f1f5f9'}},y:{title:{display:true,text:'Opportunity Score'},grid:{color:'#f1f5f9'}}},
|
| 123 |
onClick:(evt,els)=>{
|
|
|
|
| 41 |
if(id==='tab-adoption'){buildAdoptionPct();buildAdoptionAbs();buildGrowthSignals();buildTrendBars();}
|
| 42 |
if(id==='tab-competitive'){buildCompShare();buildCompRatio();buildScatterUC();}
|
| 43 |
if(id==='tab-engagement'){buildEngagement();buildScatterEng();}
|
| 44 |
+
if(id==='tab-opportunity'){buildOpportunityCharts();}
|
| 45 |
if(id==='tab-specialty'){buildSpecialtyStack();buildSpecialtyPct();}
|
| 46 |
|
| 47 |
}
|
|
|
|
| 99 |
new Chart(ctx,{type:'scatter',data:{datasets:[{label:'SEG_A',data:mk(400,5.28,0.0005),backgroundColor:CA+'66',pointRadius:3},{label:'SEG_B',data:mk(300,8.94,0.0018),backgroundColor:CB+'66',pointRadius:3},{label:'SEG_C',data:mk(200,8.71,0.0017),backgroundColor:CC+'66',pointRadius:3}]},options:{maintainAspectRatio:false,responsive:true,plugins:{legend:{position:'bottom'}},scales:{x:{title:{display:true,text:'Total Rep Visits (86 wks)'},grid:{color:'#f1f5f9'}},y:{title:{display:true,text:'Pfizer TRx / week'},grid:{color:'#f1f5f9'}}}}});}
|
| 100 |
|
| 101 |
/* ==================== TAB 6: OPPORTUNITY ==================== */
|
| 102 |
+
function buildOpportunityCharts(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
fetch('opportunity_data.json').then(r=>r.json()).then(data=>{
|
| 104 |
+
// Add a tiny random jitter to the y-axis (Score) so overlapping points are visible
|
| 105 |
+
// We keep the original 'uc' and 'sc' to show in tooltips
|
| 106 |
+
const jitter = () => (Math.random() - 0.5) * 0.04;
|
| 107 |
+
const nv=data.noVisits.map(h=>({x:h.uc, y:Math.max(0, h.sc + jitter()), ...h}));
|
| 108 |
+
const cv=data.covered.map(h=>({x:h.uc, y:Math.max(0, h.sc + jitter()), ...h}));
|
| 109 |
+
|
| 110 |
+
// Histogram
|
| 111 |
+
const all = [...data.noVisits, ...data.covered];
|
| 112 |
+
const scores = all.map(h=>h.sc);
|
| 113 |
+
const minSc = Math.min(...scores);
|
| 114 |
+
const maxSc = Math.max(...scores);
|
| 115 |
+
|
| 116 |
+
const numBins = 20;
|
| 117 |
+
const binWidth = (maxSc > minSc) ? (maxSc - minSc) / numBins : 1;
|
| 118 |
+
let edges = [];
|
| 119 |
+
for(let i=0; i<=numBins; i++) edges.push(minSc + i * binWidth);
|
| 120 |
+
|
| 121 |
+
let bins = Array(numBins).fill(0);
|
| 122 |
+
scores.forEach(s => {
|
| 123 |
+
let b = Math.floor((s - minSc) / binWidth);
|
| 124 |
+
if (b >= numBins) b = numBins - 1;
|
| 125 |
+
bins[b]++;
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
const histLabels = edges.slice(0, -1).map((e, i) => ((e + edges[i+1])/2).toFixed(2));
|
| 129 |
+
mkBar('chart-opp-hist', histLabels, [{data:bins, backgroundColor:CU+'cc', borderRadius:2, borderSkipped:false}], {plugins:{legend:{display:false}},x:{ticks:{maxTicksLimit:10,font:{size:10}}}});
|
| 130 |
+
|
| 131 |
+
// Scatter Plot
|
| 132 |
const ctx=document.getElementById('chart-opp-scatter');if(!ctx)return;
|
| 133 |
const chart=new Chart(ctx,{type:'scatter',data:{datasets:[
|
| 134 |
+
{label:'No Rep Visits',data:nv,backgroundColor:RED+'aa',pointRadius:4,pointStyle:'circle'},
|
| 135 |
+
{label:'Covered',data:cv,backgroundColor:CB+'88',pointRadius:4,pointStyle:'rect'}
|
| 136 |
]},options:{maintainAspectRatio:false,responsive:true,
|
| 137 |
plugins:{legend:{position:'bottom'},tooltip:{callbacks:{
|
| 138 |
title:pts=>{const p=pts[0];return p.datasetIndex===0?'ID: '+p.raw.id:'Covered HCP';},
|
| 139 |
+
label:p=>[`UC TRx: ${p.raw.uc.toFixed(4)}/wk`,`Score: ${p.raw.sc.toFixed(4)}`,p.raw.sp?`Specialty: ${p.raw.sp}`:'']
|
| 140 |
}}},
|
| 141 |
scales:{x:{title:{display:true,text:'UC TRx Mean (weekly)'},grid:{color:'#f1f5f9'}},y:{title:{display:true,text:'Opportunity Score'},grid:{color:'#f1f5f9'}}},
|
| 142 |
onClick:(evt,els)=>{
|
index.html
CHANGED
|
@@ -41,7 +41,7 @@
|
|
| 41 |
<div class="grid-5">
|
| 42 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Total HCPs</span><div class="kpi-icon" style="background:#f1f5f9;color:#64748b"><i class="fas fa-users"></i></div></div><div class="kpi-value" data-count="20931">0</div><div class="kpi-sub">86-week longitudinal panel</div></div>
|
| 43 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Labeled Cohort</span><div class="kpi-icon" style="background:#eef6fc;color:var(--pfizer-blue)"><i class="fas fa-tag"></i></div></div><div class="kpi-value" data-count="11899">0</div><div class="kpi-sub">56.9% of total market</div></div>
|
| 44 |
-
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Unlabeled Pool</span><div class="kpi-icon" style="background:#f5f3ff;color:var(--seg-unlabeled)"><i class="fas fa-search"></i></div></div><div class="kpi-value" data-count="
|
| 45 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Feature Columns</span><div class="kpi-icon" style="background:#ecfaff;color:var(--pfizer-sky)"><i class="fas fa-database"></i></div></div><div class="kpi-value" data-count="191">0</div><div class="kpi-sub">7 feature blocks engineered</div></div>
|
| 46 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Deep Learning Recall</span><div class="kpi-icon" style="background:#ecfdf5;color:var(--accent-green)"><i class="fas fa-brain"></i></div></div><div class="kpi-value" data-count="75" data-suffix="%">0</div><div class="kpi-sub">On minority class (SEG_C)</div></div>
|
| 47 |
</div>
|
|
@@ -204,18 +204,18 @@
|
|
| 204 |
|
| 205 |
<!-- ==================== TAB 6: UNLABELED OPPORTUNITY ==================== -->
|
| 206 |
<div id="tab-opportunity" class="tab-content">
|
| 207 |
-
<div class="section-header"><div class="section-icon"><i class="fas fa-crosshairs"></i></div><div><div class="section-title">Unlabeled HCP Opportunity</div><div class="section-subtitle">Prioritizing
|
| 208 |
|
| 209 |
<div class="grid-3" style="margin-bottom:24px">
|
| 210 |
-
<div class="card tier-card tier-1"><div class="tier-value" style="color:var(--accent-green)">
|
| 211 |
-
<div class="card tier-card tier-2"><div class="tier-value" style="color:var(--accent-amber)">
|
| 212 |
-
<div class="card tier-card tier-3"><div class="tier-value" style="color:var(--text-muted)">
|
| 213 |
</div>
|
| 214 |
|
| 215 |
-
<div class="alert-box alert-warning"><i class="fas fa-exclamation-triangle"></i><span><strong>Coverage Gap:</strong>
|
| 216 |
|
| 217 |
<div class="grid-2" style="margin-top:24px">
|
| 218 |
-
<div class="card"><div class="chart-title">Opportunity Score Distribution (
|
| 219 |
<div class="card"><div class="chart-title">Click a red point to identify the HCP below ↓</div><div class="chart-container" style="height:300px"><canvas id="chart-opp-scatter"></canvas></div></div>
|
| 220 |
</div>
|
| 221 |
|
|
|
|
| 41 |
<div class="grid-5">
|
| 42 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Total HCPs</span><div class="kpi-icon" style="background:#f1f5f9;color:#64748b"><i class="fas fa-users"></i></div></div><div class="kpi-value" data-count="20931">0</div><div class="kpi-sub">86-week longitudinal panel</div></div>
|
| 43 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Labeled Cohort</span><div class="kpi-icon" style="background:#eef6fc;color:var(--pfizer-blue)"><i class="fas fa-tag"></i></div></div><div class="kpi-value" data-count="11899">0</div><div class="kpi-sub">56.9% of total market</div></div>
|
| 44 |
+
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Unlabeled Pool</span><div class="kpi-icon" style="background:#f5f3ff;color:var(--seg-unlabeled)"><i class="fas fa-search"></i></div></div><div class="kpi-value" data-count="633">0</div><div class="kpi-sub">Pending classification</div></div>
|
| 45 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Feature Columns</span><div class="kpi-icon" style="background:#ecfaff;color:var(--pfizer-sky)"><i class="fas fa-database"></i></div></div><div class="kpi-value" data-count="191">0</div><div class="kpi-sub">7 feature blocks engineered</div></div>
|
| 46 |
<div class="card kpi-card"><div class="kpi-top"><span class="kpi-label">Deep Learning Recall</span><div class="kpi-icon" style="background:#ecfdf5;color:var(--accent-green)"><i class="fas fa-brain"></i></div></div><div class="kpi-value" data-count="75" data-suffix="%">0</div><div class="kpi-sub">On minority class (SEG_C)</div></div>
|
| 47 |
</div>
|
|
|
|
| 204 |
|
| 205 |
<!-- ==================== TAB 6: UNLABELED OPPORTUNITY ==================== -->
|
| 206 |
<div id="tab-opportunity" class="tab-content">
|
| 207 |
+
<div class="section-header"><div class="section-icon"><i class="fas fa-crosshairs"></i></div><div><div class="section-title">Unlabeled HCP Opportunity</div><div class="section-subtitle">Prioritizing 633 unclassified HCPs for commercial outreach</div></div></div>
|
| 208 |
|
| 209 |
<div class="grid-3" style="margin-bottom:24px">
|
| 210 |
+
<div class="card tier-card tier-1"><div class="tier-value" style="color:var(--accent-green)">43</div><div class="tier-label">Tier 1 — Immediate</div><div class="tier-desc">Score ≥ 0.60. Highest prescribing + growth signals.</div></div>
|
| 211 |
+
<div class="card tier-card tier-2"><div class="tier-value" style="color:var(--accent-amber)">22</div><div class="tier-label">Tier 2 — Validate</div><div class="tier-desc">Score 0.35–0.60. Moderate opportunity, needs validation.</div></div>
|
| 212 |
+
<div class="card tier-card tier-3"><div class="tier-value" style="color:var(--text-muted)">568</div><div class="tier-label">Tier 3 — Monitor</div><div class="tier-desc">Score < 0.35. Low activity, monitor for emergence.</div></div>
|
| 213 |
</div>
|
| 214 |
|
| 215 |
+
<div class="alert-box alert-warning"><i class="fas fa-exclamation-triangle"></i><span><strong>Coverage Gap:</strong> 347 of 633 unlabeled HCPs (54.8%) have zero rep visits. Among Tier 1 (high-opportunity) HCPs, many prescribe actively but have never been contacted by a sales representative.</span></div>
|
| 216 |
|
| 217 |
<div class="grid-2" style="margin-top:24px">
|
| 218 |
+
<div class="card"><div class="chart-title">Opportunity Score Distribution (633 Unlabeled HCPs)</div><div class="chart-container" style="height:300px"><canvas id="chart-opp-hist"></canvas></div></div>
|
| 219 |
<div class="card"><div class="chart-title">Click a red point to identify the HCP below ↓</div><div class="chart-container" style="height:300px"><canvas id="chart-opp-scatter"></canvas></div></div>
|
| 220 |
</div>
|
| 221 |
|