{
const p = selected
const correct = p.correct
const bgColor = correct ? "#D5F5E3" : "#FADBD8"
const borderColor = correct ? "#27AE60" : "#C0392B"
const icon = correct ? "✅" : "❌"
const verdict = correct ? "CORRECT CLASSIFICATION" : "MISCLASSIFIED"
const tierColor = {High:"#C0392B", Medium:"#E67E22", Low:"#27AE60"}[p.risk_tier_chr] || "#7F8C8D"
return htl.html`
<div style="border-left:6px solid ${borderColor}; background:${bgColor};
padding:1rem 1.4rem; border-radius:8px; margin-bottom:1rem;">
<div style="font-size:1.1rem; font-weight:700; color:${borderColor};">
${icon} ${verdict}
</div>
<div style="display:grid; grid-template-columns:1fr 1fr 1fr 1fr;
gap:0.8rem; margin-top:0.8rem;">
<div>
<div style="font-size:0.75rem; color:#555;">Patient ID</div>
<div style="font-size:1.2rem; font-weight:700;">${p.patient_label}</div>
</div>
<div>
<div style="font-size:0.75rem; color:#555;">Risk Score</div>
<div style="font-size:1.2rem; font-weight:700; color:${tierColor};">
${p.risk_score}/100
</div>
</div>
<div>
<div style="font-size:0.75rem; color:#555;">Predicted</div>
<div style="font-size:1.1rem; font-weight:600;">
${p.pred_label === "at_risk" ? "🔴 At Risk" : "🟢 Engaged"}
</div>
</div>
<div>
<div style="font-size:0.75rem; color:#555;">Actual</div>
<div style="font-size:1.1rem; font-weight:600;">
${p.true_label === "at_risk" ? "🔴 At Risk" : "🟢 Engaged"}
</div>
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr 1fr;
gap:0.8rem; margin-top:0.6rem;">
<div>
<div style="font-size:0.75rem; color:#555;">Risk Tier</div>
<div style="font-weight:600; color:${tierColor};">${p.risk_tier_chr}</div>
</div>
<div>
<div style="font-size:0.75rem; color:#555;">Model Confidence</div>
<div style="font-weight:600;">${p.confidence}</div>
</div>
<div>
<div style="font-size:0.75rem; color:#555;">Percentile vs Training</div>
<div style="font-weight:600;">${p.pct_vs_train}th percentile</div>
</div>
</div>
</div>
`
}{
const p = selected
const tier = p.risk_tier_chr
const score = p.risk_score
const actions = {
High: [
"🚨 Immediate HCP escalation — schedule within 1 week",
"🧠 Motivational counselling session (address stigma/discomfort)",
"👨👩👧 Involve family or carer in next appointment",
"📱 Simplified device recommendation — reduce tech barrier",
"📞 Weekly check-in calls for first month post-fitting",
"🏥 Cognitive/mental health referral if DemTect < 13 or MCS < 40"
],
Medium: [
"📅 Extended fitting session (+30 min) — allow more adjustment time",
"📞 30-day proactive check-in call",
"📱 Technology onboarding support — app walkthrough",
"👥 Peer support group referral (hearing aid user community)",
"🗓️ Flag for 6-week outcome review"
],
Low: [
"✅ Standard fitting pathway",
"📅 Routine 3-month follow-up",
"📱 Standard app onboarding",
"📋 No additional intervention required"
]
}
const tierColor = {High:"#C0392B", Medium:"#E67E22", Low:"#27AE60"}[tier]
const tierBg = {High:"#FADBD8", Medium:"#FDEBD0", Low:"#D5F5E3"}[tier]
return htl.html`
<div style="background:${tierBg}; border-left:5px solid ${tierColor};
border-radius:8px; padding:1rem 1.2rem;">
<div style="font-weight:700; color:${tierColor}; margin-bottom:0.6rem;">
💊 Personalised HCP Recommendation — ${tier} Risk (${score}/100)
</div>
<ul style="margin:0; padding-left:1.2rem;">
${actions[tier].map(a => htl.html`
<li style="font-size:0.87rem; padding:0.2rem 0;">${a}</li>`)}
</ul>
</div>
`
}{
const p = selected
const filtered = patients.filter(d => tier_filter.includes(d.risk_tier_chr))
const tierColor = {High:"#C0392B", Medium:"#E67E22", Low:"#27AE60"}
const plot = Plot.plot({
width: 560, height: 380,
marginLeft: 60, marginBottom: 50,
x: { label: "PTA Better Ear (dB HL)" },
y: { label: "Technology Readiness Score" },
marks: [
Plot.dot(filtered.filter(d => d.patient_label !== p.patient_label), {
x: d => d["PTA Better Ear (dB HL)"],
y: d => d["Technology Readiness"],
fill: d => tierColor[d.risk_tier_chr] || "#999",
opacity: 0.35, r: 4,
title: d => `${d.patient_label}\nRisk: ${d.risk_score}/100`
}),
Plot.ruleX([35], { stroke: "#ccc", strokeDasharray: "4,4" }),
Plot.ruleY([2.5], { stroke: "#ccc", strokeDasharray: "4,4" }),
Plot.dot([p], {
x: d => d["PTA Better Ear (dB HL)"],
y: d => d["Technology Readiness"],
fill: d => tierColor[d.risk_tier_chr] || "#1F5C8B",
stroke: "black", strokeWidth: 2, r: 10,
title: d => `${d.patient_label} (selected)\nRisk: ${d.risk_score}/100`
}),
Plot.text([p], {
x: d => d["PTA Better Ear (dB HL)"],
y: d => d["Technology Readiness"],
text: d => d.patient_label,
dy: -14, fontSize: 11, fontWeight: "bold"
})
]
})
return htl.html`
<div>
<div style="font-weight:700; color:#1F5C8B; margin-bottom:0.5rem;">
Patient Position: PTA vs Technology Readiness
</div>
<div style="font-size:0.82rem; color:#7F8C8D; margin-bottom:0.8rem;">
Selected patient shown as large dot with black border.
Dashed lines = moderate HL boundary (35 dB) and median tech readiness.
</div>
${plot}
</div>
`
}{
const p = selected
const tierColor = {High:"#C0392B", Medium:"#E67E22", Low:"#27AE60"}
const hist = Plot.plot({
width: 500, height: 220,
marginLeft: 50, marginBottom: 50,
x: { label: "Risk Score (0–100)" },
y: { label: "Count" },
marks: [
Plot.rectY(patients, Plot.binX({ y: "count" }, {
x: "risk_score",
fill: d => tierColor[d.risk_tier_chr] || "#999",
thresholds: 25, opacity: 0.7
})),
Plot.ruleX([p.risk_score], { stroke: "black", strokeWidth: 2.5 }),
Plot.text([p], {
x: "risk_score", y: 0,
text: d => `← ${d.patient_label} (${d.risk_score})`,
dy: -8, dx: 4, fontSize: 10, fontWeight: "bold"
}),
Plot.ruleX([40], { stroke: "#E67E22", strokeDasharray: "4,4", strokeWidth: 1 }),
Plot.ruleX([70], { stroke: "#C0392B", strokeDasharray: "4,4", strokeWidth: 1 })
]
})
return htl.html`
<div style="margin-top:1rem;">
<div style="font-weight:700; color:#1F5C8B; margin-bottom:0.5rem;">
Risk Score Distribution — Selected Patient Highlighted
</div>
${hist}
</div>
`
}{
const p = selected
const drivers = p.top3_drivers.split(" | ")
return htl.html`
<div style="background:white; border:1px solid #E0E0E0; border-radius:8px;
padding:1rem 1.2rem; margin-bottom:1rem;">
<div style="font-weight:700; color:#1F5C8B; margin-bottom:0.6rem;">
🔍 Top 3 Risk Drivers (SHAP)
</div>
${drivers.map((d, i) => {
const isRisk = d.includes("increases risk")
const color = isRisk ? "#C0392B" : "#27AE60"
const arrow = isRisk ? "↑" : "↓"
return htl.html`
<div style="padding:0.3rem 0; border-bottom:1px solid #F5F5F5;
font-size:0.88rem; color:${color};">
<strong>${arrow} ${i+1}.</strong> ${d}
</div>`
})}
</div>
`
}{
const p = selected
const groups = [
{ group: "Audiological",
features: ["PTA Better Ear (dB HL)","PTA Worse Ear (dB HL)","Audiogram Asymmetry (dB)"] },
{ group: "Technology",
features: ["Technology Readiness","Tech Optimism","Tech Discomfort (reversed)","Digital Engagement"] },
{ group: "Cognitive & Health",
features: ["Cognitive Score (DemTect)","Cognitive Impaired",
"Physical Health (SF-12 PCS)","Mental Health (SF-12 MCS)"] },
{ group: "Demographics",
features: ["Age (years)","Sex (Female=1)","Education Level","Income Level"] },
{ group: "Hearing History",
features: ["HL in Noise","HL in Quiet","Progressive HL","Fluctuating HL","Family History of HL"] }
]
const rows = groups.flatMap(g =>
g.features.map(f => ({
Group: g.group,
Feature: f,
Value: p[f] !== undefined ? p[f] : "—"
}))
)
return Inputs.table(rows, {
columns: ["Group", "Feature", "Value"],
width: { Group: 160, Feature: 260, Value: 100 },
rows: 20,
sort: false
})
}