|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>DentPal — Perio Chart v3 (OD Convention)</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #eef0f4;
|
|
--surface: #ffffff;
|
|
--surface2: #f5f6f9;
|
|
--border: #dde1ea;
|
|
--border-dark: #b0b8c8;
|
|
--text: #1c2033;
|
|
--muted: #6b7491;
|
|
--hdr-bg: #1c2033;
|
|
--hdr-text: #e8ecf6;
|
|
--upper-col: #1d4ed8;
|
|
--lower-col: #6d28d9;
|
|
--red: #dc2626;
|
|
--amber: #d97706;
|
|
--bleed-dot: #ef4444;
|
|
--plaq-dot: #2563eb;
|
|
--calc-dot: #16a34a;
|
|
--supp-dot: #f59e0b;
|
|
--graph-bg: #f9fafb;
|
|
--cell-w: 28px;
|
|
--label-w: 110px;
|
|
--marker-w: 22px;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: 'DM Sans', sans-serif; background: var(--bg); color: var(--text); padding: 16px; font-size: 12px; }
|
|
|
|
/* ── Shell ── */
|
|
.shell { background: var(--surface); border-radius: 10px; overflow: hidden; box-shadow: 0 2px 16px rgba(0,0,0,.08); }
|
|
|
|
.topbar {
|
|
background: var(--hdr-bg); color: var(--hdr-text);
|
|
padding: 10px 16px; display: flex; align-items: center; gap: 12px;
|
|
}
|
|
.topbar h1 { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; }
|
|
.topbar .tag { font-family: 'JetBrains Mono', monospace; font-size: 10px; background: var(--upper-col); color: #fff; padding: 2px 8px; border-radius: 3px; letter-spacing: .5px; }
|
|
|
|
.infobar { background: var(--surface2); border-bottom: 1px solid var(--border); padding: 8px 16px; display: flex; gap: 20px; flex-wrap: wrap; align-items: center; }
|
|
.info-field { display: flex; flex-direction: column; gap: 1px; }
|
|
.info-field label { font-size: 9px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .7px; }
|
|
.info-field span { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; }
|
|
|
|
.exambar { background: var(--surface); border-bottom: 1px solid var(--border); padding: 8px 16px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
.exambar label { font-size: 10px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .7px; }
|
|
.exambar select { font-family: 'JetBrains Mono', monospace; font-size: 11px; border: 1px solid var(--border-dark); border-radius: 4px; padding: 3px 8px; background: var(--surface); }
|
|
.sc-badge { font-family: 'JetBrains Mono', monospace; font-size: 10px; padding: 2px 8px; border-radius: 3px; font-weight: 700; }
|
|
.sc-n { background:#dcfce7;color:#166534; } .sc-m { background:#fef9c3;color:#854d0e; }
|
|
.sc-s { background:#fee2e2;color:#991b1b; } .sc-e { background:#ede9fe;color:#5b21b6; }
|
|
.sc-k { background:#f3f4f6;color:#374151; }
|
|
|
|
/* ── Body: chart + stats side-by-side ── */
|
|
.body-wrap { display: flex; gap: 0; }
|
|
|
|
/* ── Stats panel (right, OD-style) ── */
|
|
.stats-panel {
|
|
width: 180px; min-width: 180px;
|
|
border-left: 1px solid var(--border);
|
|
padding: 12px 10px; background: var(--surface2);
|
|
display: flex; flex-direction: column; gap: 10px;
|
|
}
|
|
.sp-title { font-size: 9px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .8px; margin-bottom: 2px; }
|
|
.sp-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
.sp-row:last-child { border-bottom: none; }
|
|
.sp-label { font-size: 10px; color: var(--text); }
|
|
.sp-val { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 700; }
|
|
.sp-val.red { color: var(--red); } .sp-val.ok { color: #16a34a; } .sp-val.warn { color: var(--amber); }
|
|
.sp-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
.numbers-red-table { width: 100%; border-collapse: collapse; font-size: 10px; font-family: 'JetBrains Mono', monospace; }
|
|
.numbers-red-table td { padding: 2px 4px; }
|
|
.numbers-red-table .nr-label { color: var(--muted); }
|
|
.numbers-red-table .nr-thresh { color: var(--red); font-weight: 600; }
|
|
.numbers-red-table .nr-count { color: var(--text); font-weight: 700; text-align: right; }
|
|
|
|
/* ── Chart scroll area ── */
|
|
.chart-scroll { flex: 1; overflow-x: auto; padding: 12px 0 12px 12px; }
|
|
|
|
/* ── Jaw block ── */
|
|
.jaw-block { margin-bottom: 8px; }
|
|
.jaw-label { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; padding: 0 4px; }
|
|
.jaw-label-text { font-family: 'JetBrains Mono', monospace; font-size: 10px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; padding: 2px 10px; border-radius: 3px; color: #fff; }
|
|
.jaw-label-text.upper { background: var(--upper-col); }
|
|
.jaw-label-text.lower { background: var(--lower-col); }
|
|
.jaw-divider { flex: 1; height: 1px; background: var(--border); }
|
|
|
|
/* ── Grid ── */
|
|
.grid { display: table; border-collapse: collapse; font-family: 'JetBrains Mono', monospace; font-size: 10.5px; }
|
|
.grow { display: table-row; }
|
|
|
|
/* Section marker (F / L) */
|
|
.fl-marker {
|
|
display: table-cell; width: var(--marker-w); min-width: var(--marker-w);
|
|
text-align: center; vertical-align: middle;
|
|
font-size: 11px; font-weight: 700;
|
|
padding: 0 2px;
|
|
}
|
|
.fl-marker.f { color: var(--upper-col); }
|
|
.fl-marker.l { color: var(--lower-col); }
|
|
.fl-marker.blank { color: transparent; }
|
|
|
|
/* Row label */
|
|
.rlabel {
|
|
display: table-cell; vertical-align: middle; white-space: nowrap;
|
|
width: var(--label-w); min-width: var(--label-w);
|
|
padding: 1px 6px 1px 0;
|
|
font-size: 9.5px; color: var(--muted); font-weight: 500;
|
|
border-right: 2px solid var(--border-dark);
|
|
font-family: 'DM Sans', sans-serif;
|
|
}
|
|
.rlabel.auto { font-style: italic; color: #7c3aed; }
|
|
.rlabel.section { font-size: 10px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: .8px; }
|
|
.rlabel.upper-lbl { color: var(--upper-col); }
|
|
.rlabel.lower-lbl { color: var(--lower-col); }
|
|
|
|
/* Data cell — triple values */
|
|
.dcell {
|
|
display: table-cell; vertical-align: middle;
|
|
border-left: 1px solid var(--border);
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0;
|
|
}
|
|
.dcell.skipped { background: #f3f4f6; }
|
|
|
|
.triple { display: flex; width: calc(var(--cell-w)*3); min-width: calc(var(--cell-w)*3); }
|
|
.v {
|
|
flex: 1; text-align: center; line-height: 18px; min-height: 18px;
|
|
font-size: 10.5px; font-family: 'JetBrains Mono', monospace; font-weight: 500;
|
|
cursor: default;
|
|
}
|
|
.v.red { color: var(--red); font-weight: 700; }
|
|
.v.black { color: var(--text); }
|
|
.v.amber { color: var(--amber); font-weight: 600; }
|
|
.v.gray { color: #c0c4d0; }
|
|
.v.skip { color: #d1d5db; }
|
|
|
|
/* Single cell (mobility, furcation) */
|
|
.scell {
|
|
display: table-cell; vertical-align: middle; text-align: center;
|
|
border-left: 1px solid var(--border);
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
width: calc(var(--cell-w)*3); min-width: calc(var(--cell-w)*3);
|
|
font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
|
|
line-height: 18px; min-height: 18px;
|
|
cursor: default;
|
|
}
|
|
.scell.skipped { background: #f3f4f6; }
|
|
|
|
/* Tooth number row */
|
|
.tnum-cell {
|
|
display: table-cell; text-align: center; vertical-align: middle;
|
|
width: calc(var(--cell-w)*3); min-width: calc(var(--cell-w)*3);
|
|
border-left: 1px solid var(--border);
|
|
background: var(--surface2);
|
|
font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700;
|
|
padding: 4px 0; color: var(--text);
|
|
}
|
|
.tnum-cell.skipped { color: #9ca3af; text-decoration: line-through; background: #f3f4f6; }
|
|
|
|
/* Bleeding dots row */
|
|
.dots-cell {
|
|
display: table-cell; vertical-align: middle;
|
|
border-left: 1px solid var(--border);
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 1px 0;
|
|
}
|
|
.dots-triple { display: flex; width: calc(var(--cell-w)*3); min-width: calc(var(--cell-w)*3); align-items: center; justify-content: space-around; padding: 0 2px; }
|
|
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
|
.dot.on-bleed { background: var(--bleed-dot); }
|
|
.dot.on-plaq { background: var(--plaq-dot); }
|
|
.dot.on-calc { background: var(--calc-dot); }
|
|
.dot.on-supp { background: var(--supp-dot); }
|
|
.dot.off { background: #e5e7eb; }
|
|
.dot.skip { background: #f3f4f6; }
|
|
|
|
/* ── GRAPH SECTION ── */
|
|
.graph-row .dcell, .graph-row .scell { border: none; padding: 0; background: transparent; }
|
|
.graph-outer {
|
|
width: calc(var(--cell-w)*3); min-width: calc(var(--cell-w)*3);
|
|
height: 72px;
|
|
border-left: 1px solid var(--border);
|
|
background: repeating-linear-gradient(
|
|
to bottom, transparent 0px, transparent 11px,
|
|
rgba(180,186,200,.2) 11px, rgba(180,186,200,.2) 12px
|
|
);
|
|
display: flex; align-items: flex-end; gap: 1px; padding: 0 2px;
|
|
border-bottom: 2px solid var(--border-dark);
|
|
position: relative;
|
|
}
|
|
/* Upper jaw: bars hang from TOP */
|
|
.graph-outer.inverted {
|
|
align-items: flex-start;
|
|
border-bottom: none;
|
|
border-top: 2px solid var(--border-dark);
|
|
background: repeating-linear-gradient(
|
|
to top, transparent 0px, transparent 11px,
|
|
rgba(180,186,200,.2) 11px, rgba(180,186,200,.2) 12px
|
|
);
|
|
}
|
|
.graph-outer.skipped { background: #f3f4f6; }
|
|
.bar { flex: 1; border-radius: 1px; min-height: 2px; }
|
|
.bar.red { background: var(--red); opacity: .82; }
|
|
.bar.black { background: #374151; opacity: .68; }
|
|
.bar.skip { background: #e5e7eb; }
|
|
/* Upper: bar rounds at BOTTOM (hangs down) */
|
|
.graph-outer.inverted .bar { border-radius: 0 0 2px 2px; }
|
|
/* Lower: bar rounds at TOP (grows up) */
|
|
.graph-outer:not(.inverted) .bar { border-radius: 2px 2px 0 0; }
|
|
|
|
/* Spacer */
|
|
.spacer-row { height: 6px; display: table-row; }
|
|
|
|
/* Jaw separator */
|
|
.jaw-sep { height: 12px; display: table-row; background: var(--bg); }
|
|
|
|
/* Furcation / mobility coloring */
|
|
.furc-0 { color: #d1d5db; }
|
|
.furc-1,.furc-2,.furc-3,.furc-4 { color: var(--red); font-weight: 700; }
|
|
.mob-0 { color: #d1d5db; }
|
|
.mob-1,.mob-2,.mob-3 { color: var(--red); font-weight: 700; }
|
|
|
|
/* Tooltip */
|
|
[data-tip] { position: relative; }
|
|
[data-tip]:hover::after {
|
|
content: attr(data-tip); position: absolute; bottom: 120%; left: 50%;
|
|
transform: translateX(-50%); background: #1c2033; color: #fff;
|
|
font-size: 9px; padding: 2px 6px; border-radius: 3px; white-space: nowrap;
|
|
z-index: 200; pointer-events: none; font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
/* Legend strip */
|
|
.legend { padding: 6px 16px; border-top: 1px solid var(--border); background: var(--surface2); display: flex; gap: 14px; flex-wrap: wrap; font-size: 10px; color: var(--muted); align-items: center; }
|
|
.li { display: flex; align-items: center; gap: 4px; }
|
|
.li .dot { width: 8px; height: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
|
|
<div class="topbar">
|
|
<h1>Periodontal Chart</h1>
|
|
<span class="tag">DENTPAL</span>
|
|
<span style="font-size:10px;opacity:.5;font-family:'JetBrains Mono',monospace;margin-left:auto">v3 — OD Convention</span>
|
|
</div>
|
|
|
|
<div class="infobar">
|
|
<div class="info-field"><label>Patient</label><span>John A. Doe</span></div>
|
|
<div class="info-field"><label>DOB</label><span>14 Mar 1978</span></div>
|
|
<div class="info-field"><label>ID</label><span>PT-00412</span></div>
|
|
<div class="info-field"><label>Provider</label><span>Dr. R. Sharma</span></div>
|
|
<div class="info-field"><label>Recorded by</label><span>K. Patel (Hygienist)</span></div>
|
|
</div>
|
|
|
|
<div class="exambar">
|
|
<label>Exam</label>
|
|
<select id="examSel" onchange="loadExam(this.value)">
|
|
<option value="sc1">11 Mar 2026 — Scenario 1: Healthy Baseline</option>
|
|
<option value="sc2">05 Sep 2025 — Scenario 2: Moderate Periodontitis</option>
|
|
<option value="sc3">18 Mar 2025 — Scenario 3: Severe (All Flags)</option>
|
|
<option value="sc4">02 Jan 2025 — Scenario 4: Edge Cases (AG≤0, GM<0, Boundaries)</option>
|
|
<option value="sc5">15 Jun 2024 — Scenario 5: Missing Teeth #1 #16 #17 #32</option>
|
|
</select>
|
|
<span id="scBadge" class="sc-badge"></span>
|
|
<div style="margin-left:auto;font-size:9px;color:var(--muted);font-family:'JetBrains Mono',monospace">
|
|
Upper bars ▼ down · Lower bars ▲ up · OD convention
|
|
</div>
|
|
</div>
|
|
|
|
<div class="body-wrap">
|
|
<!-- Chart area -->
|
|
<div class="chart-scroll" id="chartArea"></div>
|
|
<!-- Stats panel -->
|
|
<div class="stats-panel" id="statsPanel"></div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<span style="font-size:9px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.8px">Legend:</span>
|
|
<span class="li"><span class="dot" style="background:var(--bleed-dot);border-radius:50%"></span>Bleeding</span>
|
|
<span class="li"><span class="dot" style="background:var(--plaq-dot);border-radius:50%"></span>Plaque</span>
|
|
<span class="li"><span class="dot" style="background:var(--calc-dot);border-radius:50%"></span>Calculus</span>
|
|
<span class="li"><span class="dot" style="background:var(--supp-dot);border-radius:50%"></span>Suppuration</span>
|
|
<span class="li" style="margin-left:8px"><span style="color:var(--red);font-weight:700;font-family:'JetBrains Mono',monospace">3</span> = red threshold met</span>
|
|
<span class="li"><span style="color:var(--muted);font-style:italic;font-size:9px">auto</span> = calculated row</span>
|
|
<span class="li"><span style="background:#f3f4f6;padding:0 4px;border-radius:2px">✕</span> = skipped/missing tooth</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════
|
|
// ANSWERS FROM OD IMAGE — applied to v3
|
|
// Q1: Upper bars DOWN (inverted), Lower bars UP ✓
|
|
// Q2: Furcation red if >= 1 ✓
|
|
// Q3: Mobility red if >= 1 ✓
|
|
// Q4: BleedSupPlaqCalc = 4 separate boolean dot indicators ✓
|
|
// Q5: Missing surface = dash (–) ✓
|
|
// Q6: AG <= 0 = display as-is (amber for negative, no block) ✓
|
|
// Q7: Universal 1-32 ✓
|
|
// Q8: Exam note shown below patient bar area ✓
|
|
// Q9: Recession = separate row from GingMargin ✓
|
|
// Q10: No mm scale on graph (OD screenshot shows none) ✓
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
// ── CALCULATIONS ───────────────────────────────────
|
|
const calcAG = (mgj, pd) => (mgj == null || pd == null) ? null : mgj - pd;
|
|
const calcCAL = (pd, rec, gm) => {
|
|
if (pd == null) return null;
|
|
// Recession row > 0 = apical: CAL = PD + recession
|
|
if (rec != null && rec > 0) return pd + rec;
|
|
// GingMargin row < 0 = overgrowth: CAL = PD - |gm|
|
|
if (gm != null && gm < 0) return Math.max(0, pd - Math.abs(gm));
|
|
// GM = 0 or null: CAL = PD
|
|
return pd;
|
|
};
|
|
|
|
// ── COLOUR RULES (from OD screenshot right panel) ──
|
|
const pdC = v => v == null ? 'gray' : v >= 4 ? 'red' : 'black';
|
|
const mgjC = v => v == null ? 'gray' : v <= 2 ? 'red' : 'black';
|
|
const agC = v => v == null ? 'gray' : v < 0 ? 'amber' : v <= 3 ? 'red' : 'black';
|
|
const calC = v => v == null ? 'gray' : v >= 6 ? 'red' : 'black';
|
|
const recC = v => v == null ? 'gray' : v >= 2 ? 'red' : 'black';
|
|
const furcC = v => v == null ? 'furc-0' : v >= 1 ? 'furc-1' : 'furc-0'; // red if >= 1
|
|
const mobC = v => v == null ? 'mob-0' : v >= 1 ? 'mob-1' : 'mob-0'; // red if >= 1
|
|
|
|
const FURC_SYM = ['–','▲','▲▲','▲▲▲','●'];
|
|
const MOB_SYM = ['–','1','2','3'];
|
|
const SURF_F = ['MB','B','DB'];
|
|
const SURF_L = ['ML','L','DL'];
|
|
|
|
// ── DATA SCENARIOS ──────────────────────────────────
|
|
// Per tooth: facial{pd[3], gm[3], mgj[3], rec[3]}, lingual{...}, mobility, furcation
|
|
// bleeding/plaque/calculus/suppuration: [6] booleans — indices 0-2 facial, 3-5 lingual
|
|
// null = missing surface data (dash shown)
|
|
function mk(fPD,fGM,fMGJ,fRec, lPD,lGM,lMGJ,lRec, mob,furc, bl,pl,ca,su,skip=false) {
|
|
return { facial:{pd:fPD,gm:fGM,mgj:fMGJ,rec:fRec}, lingual:{pd:lPD,gm:lGM,mgj:lMGJ,rec:lRec},
|
|
mobility:mob, furcation:furc, bleeding:bl, plaque:pl, calculus:ca, suppuration:su, skip };
|
|
}
|
|
function healthy() {
|
|
return mk([2,2,2],[0,0,0],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],0,0,
|
|
[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]);
|
|
}
|
|
function skipped() {
|
|
return mk([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],0,0,
|
|
[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],true);
|
|
}
|
|
function buildTeeth(overrides) {
|
|
const T = {};
|
|
for (let i=1;i<=32;i++) T[i] = overrides[i] || healthy();
|
|
return T;
|
|
}
|
|
|
|
// SC1 — Healthy
|
|
const SC1 = buildTeeth({
|
|
3:mk([2,3,2],[0,0,0],[5,5,6],[0,0,0],[2,2,3],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
8:mk([1,2,1],[0,0,0],[6,5,5],[0,0,0],[1,1,2],[0,0,0],[5,6,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
});
|
|
|
|
// SC2 — Moderate
|
|
const SC2 = buildTeeth({
|
|
2:mk([4,5,4],[0,0,0],[5,5,5],[1,1,1],[3,4,3],[0,0,0],[4,5,4],[1,0,1],1,1,[1,1,0,0,1,0],[1,0,1,1,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
3:mk([5,4,5],[0,0,0],[5,5,4],[2,1,2],[4,5,4],[0,0,0],[4,4,5],[1,2,1],0,1,[1,1,1,1,0,1],[1,1,0,1,1,0],[1,0,0,0,1,0],[0,0,0,0,0,0]),
|
|
4:mk([3,4,3],[0,0,0],[4,5,4],[1,0,1],[3,3,4],[0,0,0],[5,5,5],[0,1,0],1,0,[0,1,0,0,0,1],[0,1,0,0,1,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
14:mk([4,5,4],[0,0,0],[4,4,5],[2,2,1],[4,4,5],[0,0,0],[4,5,4],[1,2,1],0,2,[1,1,1,1,1,0],[1,1,1,0,1,1],[1,0,1,0,0,1],[0,0,0,0,0,0]),
|
|
19:mk([5,5,4],[0,0,0],[5,5,4],[2,2,2],[4,5,4],[0,0,0],[4,4,5],[2,1,2],1,1,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,0,1,1,0,0],[0,1,0,0,0,0]),
|
|
30:mk([4,4,5],[0,0,0],[5,4,5],[1,2,1],[3,4,4],[0,0,0],[5,5,4],[0,2,1],0,2,[1,0,1,0,1,1],[0,1,0,1,0,1],[1,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
});
|
|
|
|
// SC3 — Severe
|
|
const SC3 = buildTeeth({
|
|
2:mk([7,8,6],[0,0,0],[4,4,5],[3,4,3],[6,7,6],[0,0,0],[4,4,4],[3,3,4],2,3,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1]),
|
|
3:mk([9,8,7],[0,0,0],[4,3,4],[4,5,4],[8,7,8],[0,0,0],[4,4,3],[4,4,3],3,4,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1]),
|
|
14:mk([6,7,6],[0,0,0],[4,4,4],[3,3,3],[6,6,7],[0,0,0],[4,3,4],[3,4,3],2,3,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1],[1,0,1,1,0,1]),
|
|
18:mk([6,8,7],[0,0,0],[4,4,3],[4,3,4],[7,6,8],[0,0,0],[4,3,4],[4,4,3],2,3,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,0,1,1,1]),
|
|
31:mk([9,7,8],[0,0,0],[3,3,4],[5,4,5],[8,7,9],[0,0,0],[3,3,3],[5,4,5],3,4,[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1],[1,1,1,1,1,1]),
|
|
});
|
|
|
|
// SC4 — Edge cases
|
|
const SC4 = buildTeeth({
|
|
// AG=0: MGJ=4, PD=4
|
|
5:mk([4,4,4],[0,0,0],[4,4,4],[0,0,0],[3,3,3],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// AG<0: MGJ=3, PD=5
|
|
6:mk([5,5,5],[0,0,0],[3,3,3],[0,0,0],[3,3,3],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// GM<0 overgrowth: recession=0, gm=-2 → CAL = PD-|gm|
|
|
7:mk([3,3,3],[-2,-2,-2],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// GM=0 at CEJ
|
|
8:mk([3,2,3],[0,0,0],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// CAL=0: PD=2, gm=-2
|
|
9:mk([2,2,2],[-2,-2,-2],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// PD=3 boundary → black
|
|
10:mk([3,3,3],[0,0,0],[5,5,5],[0,0,0],[3,3,3],[0,0,0],[5,5,5],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// PD=4 boundary → red
|
|
11:mk([4,4,4],[0,0,0],[6,6,6],[0,0,0],[4,4,4],[0,0,0],[6,6,6],[0,0,0],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// Partial surface data — tooth 12 only has middle surface
|
|
12:mk([null,3,null],[null,0,null],[null,5,null],[null,0,null],[null,2,null],[null,0,null],[null,5,null],[null,0,null],0,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// Furc=1 → red (>= 1 rule confirmed from OD)
|
|
13:mk([2,2,2],[0,0,0],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],0,1,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
// Mob=1 → red (>= 1 rule confirmed from OD)
|
|
14:mk([2,2,2],[0,0,0],[5,5,5],[0,0,0],[2,2,2],[0,0,0],[5,5,5],[0,0,0],1,0,[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
});
|
|
|
|
// SC5 — Missing teeth
|
|
const SC5 = buildTeeth({
|
|
1:skipped(), 16:skipped(), 17:skipped(), 32:skipped(),
|
|
5:mk([4,5,4],[0,0,0],[5,5,4],[2,1,2],[3,4,3],[0,0,0],[4,5,5],[1,1,0],1,1,[1,1,0,0,1,0],[1,0,1,1,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]),
|
|
12:mk([5,5,4],[0,0,0],[4,4,5],[2,2,1],[4,4,5],[0,0,0],[4,5,4],[1,2,1],0,2,[1,1,1,1,1,0],[1,1,1,0,1,1],[1,0,1,0,0,1],[0,0,0,0,0,0]),
|
|
});
|
|
|
|
const EXAMS = {sc1:SC1,sc2:SC2,sc3:SC3,sc4:SC4,sc5:SC5};
|
|
const META = {
|
|
sc1:{label:'Healthy',cls:'sc-n'},
|
|
sc2:{label:'Moderate',cls:'sc-m'},
|
|
sc3:{label:'Severe',cls:'sc-s'},
|
|
sc4:{label:'Edge Cases',cls:'sc-e'},
|
|
sc5:{label:'Missing Teeth',cls:'sc-k'},
|
|
};
|
|
|
|
// Exam notes (Q8 — shown per OD convention)
|
|
const NOTES = {
|
|
sc1: 'Full mouth probing. Patient is healthy with good home care. No significant findings.',
|
|
sc2: 'Moderate generalized periodontitis. Plaque and calculus moderate. Bleeding on probing moderate.',
|
|
sc3: 'Severe generalized periodontitis. Immediate referral to periodontist recommended. Full probing complete.',
|
|
sc4: 'Edge case data — boundary values and partial surface recordings for QA testing purposes.',
|
|
sc5: 'Teeth #1, #16, #17, #32 extracted. Remaining dentition charted in full.',
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// RENDER
|
|
// ═══════════════════════════════════════════════════
|
|
const UPPER = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16];
|
|
const LOWER = [32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17];
|
|
|
|
function vSpan(v, cls, tip) {
|
|
if (v === null) return `<span class="v gray" data-tip="–">–</span>`;
|
|
return `<span class="v ${cls}" data-tip="${tip}">${v}</span>`;
|
|
}
|
|
|
|
function tripleVals(arr, colorFn, surfs) {
|
|
return arr.map((v,i) => vSpan(v, colorFn(v), `${surfs[i]}: ${v??'–'}`)).join('');
|
|
}
|
|
|
|
function dotRow(boolArr, skip, type) {
|
|
const colors = {bleed:'on-bleed',plaque:'on-plaq',calculus:'on-calc',suppuration:'on-supp'};
|
|
const cls = colors[type] || 'on-bleed';
|
|
if (skip) return [0,1,2].map(()=>`<span class="dot skip"></span>`).join('');
|
|
return boolArr.map(v=>`<span class="dot ${v?cls:'off'}"></span>`).join('');
|
|
}
|
|
|
|
function graphBars(pdArr, skip, inverted) {
|
|
const maxH = 72, scale = 10;
|
|
if (skip) return pdArr.map(()=>`<div class="bar skip" style="height:4px"></div>`).join('');
|
|
return pdArr.map((v,i) => {
|
|
if (v === null) return `<div class="bar skip" style="height:2px"></div>`;
|
|
const h = Math.min(maxH, Math.max(3, v * scale));
|
|
const cls = v >= 4 ? 'red' : 'black';
|
|
return `<div class="bar ${cls}" style="height:${h}px" data-tip="PD ${v}mm"></div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function furcCell(v, skip) {
|
|
if (skip) return `<div class="scell skipped">✕</div>`;
|
|
const sym = FURC_SYM[v??0];
|
|
const cls = furcC(v??0);
|
|
return `<div class="scell"><span class="${cls}" data-tip="${['None','Incipient','Moderate','Advanced','Severe'][v??0]}">${sym}</span></div>`;
|
|
}
|
|
function mobCell(v, skip) {
|
|
if (skip) return `<div class="scell skipped">✕</div>`;
|
|
const sym = MOB_SYM[v??0];
|
|
const cls = mobC(v??0);
|
|
return `<div class="scell"><span class="${cls}" data-tip="${['None','Slight','Moderate','Severe'][v??0]}">${sym}</span></div>`;
|
|
}
|
|
|
|
function buildJaw(teeth, T, isUpper) {
|
|
// Derive calculated values per tooth
|
|
function der(td, side) {
|
|
const d = td[side];
|
|
const ag = d.pd.map((pd,i) => pd===null ? null : calcAG(d.mgj[i], pd));
|
|
const cal = d.pd.map((pd,i) => pd===null ? null : calcCAL(pd, d.rec[i], d.gm[i]));
|
|
return {...d, ag, cal};
|
|
}
|
|
|
|
function bleedDotsRow(teeth, T, sliceStart) {
|
|
// Combined all 4 types in one row as colored dots (OD style)
|
|
return `<div class="grow">
|
|
<div class="fl-marker blank"></div>
|
|
<div class="rlabel">Bleed/Plaq/Calc/Supp</div>
|
|
${teeth.map(t => {
|
|
const td = T[t];
|
|
const sk = td.skip;
|
|
const bl = td.bleeding.slice(sliceStart, sliceStart+3);
|
|
const pl = td.plaque.slice(sliceStart, sliceStart+3);
|
|
const ca = td.calculus.slice(sliceStart, sliceStart+3);
|
|
const su = td.suppuration.slice(sliceStart, sliceStart+3);
|
|
return `<div class="dots-cell${sk?' style=background:#f3f4f6':''}" ${sk?'':''}><div class="dots-triple">
|
|
${[0,1,2].map(i => `
|
|
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;padding:1px 0">
|
|
${sk ? '<span class="dot skip"></span><span class="dot skip"></span><span class="dot skip"></span><span class="dot skip"></span>'
|
|
: `<span class="dot ${bl[i]?'on-bleed':'off'}" data-tip="${bl[i]?'Bleeding':'No bleeding'}"></span>
|
|
<span class="dot ${pl[i]?'on-plaq':'off'}" data-tip="${pl[i]?'Plaque':'No plaque'}"></span>
|
|
<span class="dot ${ca[i]?'on-calc':'off'}" data-tip="${ca[i]?'Calculus':'No calculus'}"></span>
|
|
<span class="dot ${su[i]?'on-supp':'off'}" data-tip="${su[i]?'Suppuration':'No suppuration'}"></span>`
|
|
}
|
|
</div>`).join('')}
|
|
</div></div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function dataRow(flCls, flLabel, rlCls, rlLabel, valFn) {
|
|
return `<div class="grow">
|
|
<div class="fl-marker ${flCls}">${flLabel}</div>
|
|
<div class="rlabel ${rlCls}">${rlLabel}</div>
|
|
${teeth.map(t => {
|
|
const td = T[t];
|
|
return `<div class="dcell${td.skip?' skipped':''}"><div class="triple">${td.skip?'<span class="v skip">✕</span><span class="v skip">✕</span><span class="v skip">✕</span>':valFn(t,td)}</div></div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function singleRow(flCls, flLabel, rlCls, rlLabel, valFn) {
|
|
return `<div class="grow">
|
|
<div class="fl-marker ${flCls}">${flLabel}</div>
|
|
<div class="rlabel ${rlCls}">${rlLabel}</div>
|
|
${teeth.map(t => valFn(t, T[t])).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function toothNumRow() {
|
|
return `<div class="grow">
|
|
<div class="fl-marker blank"></div>
|
|
<div class="rlabel section ${isUpper?'upper-lbl':'lower-lbl'}"></div>
|
|
${teeth.map(t => `<div class="tnum-cell${T[t].skip?' skipped':''}">${t}</div>`).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function graphRow(isInverted, side) {
|
|
const surfs = side==='facial' ? SURF_F : SURF_L;
|
|
return `<div class="grow graph-row">
|
|
<div class="fl-marker blank"></div>
|
|
<div class="rlabel" style="font-style:italic;color:var(--muted);font-size:9px">PD graph</div>
|
|
${teeth.map(t => {
|
|
const td = T[t]; const sk = td.skip;
|
|
const pd = td[side].pd;
|
|
return `<div class="dcell"><div class="graph-outer${isInverted?' inverted':''}${sk?' skipped':''}">${graphBars(pd,sk,isInverted)}</div></div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
const acc = isUpper ? 'f' : 'l';
|
|
const alt = isUpper ? 'l' : 'f';
|
|
|
|
if (isUpper) {
|
|
// OD UPPER layout:
|
|
// [F] PD facial (with exam date)
|
|
// [ ] Bleed/Plaq/Calc/Supp dots (facial)
|
|
// [ ] MGJ
|
|
// [ ] auto Att Ging
|
|
// [ ] Recession
|
|
// [ ] auto CAL
|
|
// [ ] Furc
|
|
// [ ] Mobility
|
|
// [tooth numbers 1-16]
|
|
// [graph — bars DOWN ▼]
|
|
// [L] Furc (lingual)
|
|
// [ ] auto Att Ging (lingual)
|
|
// [ ] Recession (lingual)
|
|
// [L] PD lingual (with exam date)
|
|
// [ ] Bleed/Plaq/Calc/Supp dots (lingual)
|
|
return `
|
|
${dataRow('f','F','','Probing Depth', (t,td) => tripleVals(der(td,'facial').pd, pdC, SURF_F))}
|
|
${bleedDotsRow(teeth, T, 0)}
|
|
${dataRow('blank','','','MGJ', (t,td) => tripleVals(der(td,'facial').mgj, mgjC, SURF_F))}
|
|
${dataRow('blank','','auto','auto Att Ging', (t,td) => { const ag=der(td,'facial').ag; return ag.map((v,i)=>vSpan(v,agC(v),`${SURF_F[i]}: AG ${v??'–'}`)).join(''); })}
|
|
${dataRow('blank','','','Recession', (t,td) => tripleVals(der(td,'facial').rec, recC, SURF_F))}
|
|
${dataRow('blank','','auto','auto CAL', (t,td) => { const cal=der(td,'facial').cal; return cal.map((v,i)=>vSpan(v,calC(v),`${SURF_F[i]}: CAL ${v??'–'}`)).join(''); })}
|
|
${singleRow('blank','','','Furc', (t,td) => furcCell(td.furcation, td.skip))}
|
|
${singleRow('blank','','','Mobility', (t,td) => mobCell(td.mobility, td.skip))}
|
|
${toothNumRow()}
|
|
${graphRow(true, 'facial')}
|
|
${singleRow('blank','','','Furc (L)', (t,td) => furcCell(td.furcation, td.skip))}
|
|
${dataRow('blank','','auto','auto Att Ging (L)', (t,td) => { const ag=der(td,'lingual').ag; return ag.map((v,i)=>vSpan(v,agC(v),`${SURF_L[i]}: AG ${v??'–'}`)).join(''); })}
|
|
${dataRow('blank','','','Recession (L)', (t,td) => tripleVals(der(td,'lingual').rec, recC, SURF_L))}
|
|
${dataRow('l','L','','Probing Depth (L)', (t,td) => tripleVals(der(td,'lingual').pd, pdC, SURF_L))}
|
|
${bleedDotsRow(teeth, T, 3)}
|
|
`;
|
|
} else {
|
|
// OD LOWER layout:
|
|
// [L] PD lingual (exam date)
|
|
// [ ] Bleed/Plaq/Calc/Supp (lingual)
|
|
// [ ] MGJ
|
|
// [ ] auto Att Ging
|
|
// [ ] Recession
|
|
// [ ] auto CAL
|
|
// [ ] Furc
|
|
// [tooth numbers 32-17]
|
|
// [graph — bars UP ▲]
|
|
// [ ] Mobility
|
|
// [ ] auto CAL (facial)
|
|
// [ ] auto Att Ging (facial)
|
|
// [ ] Recession (facial)
|
|
// [ ] MGJ (facial)
|
|
// [F] PD facial (exam date)
|
|
// [ ] Bleed/Plaq/Calc/Supp (facial)
|
|
return `
|
|
${dataRow('l','L','','Probing Depth (L)', (t,td) => tripleVals(der(td,'lingual').pd, pdC, SURF_L))}
|
|
${bleedDotsRow(teeth, T, 3)}
|
|
${dataRow('blank','','','MGJ (L)', (t,td) => tripleVals(der(td,'lingual').mgj, mgjC, SURF_L))}
|
|
${dataRow('blank','','auto','auto Att Ging (L)', (t,td) => { const ag=der(td,'lingual').ag; return ag.map((v,i)=>vSpan(v,agC(v),`${SURF_L[i]}: AG ${v??'–'}`)).join(''); })}
|
|
${dataRow('blank','','','Recession (L)', (t,td) => tripleVals(der(td,'lingual').rec, recC, SURF_L))}
|
|
${dataRow('blank','','auto','auto CAL (L)', (t,td) => { const cal=der(td,'lingual').cal; return cal.map((v,i)=>vSpan(v,calC(v),`${SURF_L[i]}: CAL ${v??'–'}`)).join(''); })}
|
|
${singleRow('blank','','','Furc (L)', (t,td) => furcCell(td.furcation, td.skip))}
|
|
${toothNumRow()}
|
|
${graphRow(false, 'lingual')}
|
|
${singleRow('blank','','','Mobility', (t,td) => mobCell(td.mobility, td.skip))}
|
|
${dataRow('blank','','auto','auto CAL', (t,td) => { const cal=der(td,'facial').cal; return cal.map((v,i)=>vSpan(v,calC(v),`${SURF_F[i]}: CAL ${v??'–'}`)).join(''); })}
|
|
${dataRow('blank','','auto','auto Att Ging', (t,td) => { const ag=der(td,'facial').ag; return ag.map((v,i)=>vSpan(v,agC(v),`${SURF_F[i]}: AG ${v??'–'}`)).join(''); })}
|
|
${dataRow('blank','','','Recession', (t,td) => tripleVals(der(td,'facial').rec, recC, SURF_F))}
|
|
${dataRow('blank','','','MGJ', (t,td) => tripleVals(der(td,'facial').mgj, mgjC, SURF_F))}
|
|
${dataRow('f','F','','Probing Depth', (t,td) => tripleVals(der(td,'facial').pd, pdC, SURF_F))}
|
|
${bleedDotsRow(teeth, T, 0)}
|
|
`;
|
|
}
|
|
}
|
|
|
|
function buildStats(T) {
|
|
let pd4=0,pdN=0,mgj2=0,mgjN=0,rec2=0,recN=0,cal6=0,calN=0,ag3=0,agN=0,furc1=0,mob1=0,skip=0;
|
|
let bleed=0,plaq=0,calc_=0,supp=0,boolN=0;
|
|
let pdSum=0;
|
|
|
|
for (let t=1;t<=32;t++) {
|
|
const td=T[t]; if(td.skip){skip++;continue;}
|
|
['facial','lingual'].forEach(side=>{
|
|
const d=td[side];
|
|
d.pd.forEach((v,i)=>{
|
|
if(v===null)return;
|
|
pdN++;pdSum+=v;if(v>=4)pd4++;
|
|
const ag=calcAG(d.mgj[i],v); if(ag!==null){agN++;if(ag<=3)ag3++;}
|
|
const cal=calcCAL(v,d.rec[i],d.gm[i]); if(cal!==null){calN++;if(cal>=6)cal6++;}
|
|
});
|
|
d.mgj.forEach(v=>{if(v===null)return;mgjN++;if(v<=2)mgj2++;});
|
|
d.rec.forEach(v=>{if(v===null)return;recN++;if(v>=2)rec2++;});
|
|
});
|
|
td.bleeding.forEach(v=>{boolN++;if(v)bleed++;});
|
|
td.plaque.forEach(v=>{if(v)plaq++;});
|
|
td.calculus.forEach(v=>{if(v)calc_++;});
|
|
td.suppuration.forEach(v=>{if(v)supp++;});
|
|
if(td.furcation>=1)furc1++;
|
|
if(td.mobility>=1)mob1++;
|
|
}
|
|
const bop=boolN?Math.round(bleed/boolN*100):0;
|
|
const meanPD=pdN?(pdSum/pdN).toFixed(1):'—';
|
|
|
|
return {pd4,pdN,mgj2,mgjN,rec2,recN,cal6,calN,ag3,agN,furc1,mob1,skip,bleed,plaq,calc_,supp,boolN,bop,meanPD};
|
|
}
|
|
|
|
function renderStats(s) {
|
|
const nr = (label, thresh, count) =>
|
|
`<tr><td class="nr-label">${label}</td><td class="nr-thresh">${thresh}</td><td class="nr-count">${count}</td></tr>`;
|
|
|
|
document.getElementById('statsPanel').innerHTML = `
|
|
<div>
|
|
<div class="sp-title">Index %</div>
|
|
<div class="sp-row"><span class="sp-label">Plaque</span><span class="sp-dot" style="background:var(--plaq-dot)"></span><span class="sp-val ${s.plaq>10?'red':'ok'}">${s.plaq}</span></div>
|
|
<div class="sp-row"><span class="sp-label">Calculus</span><span class="sp-dot" style="background:var(--calc-dot)"></span><span class="sp-val ${s.calc_>10?'red':'ok'}">${s.calc_}</span></div>
|
|
<div class="sp-row"><span class="sp-label">Bleeding</span><span class="sp-dot" style="background:var(--bleed-dot)"></span><span class="sp-val ${s.bop>20?'red':'ok'}">${s.bop}%</span></div>
|
|
<div class="sp-row"><span class="sp-label">Suppuration</span><span class="sp-dot" style="background:var(--supp-dot)"></span><span class="sp-val ${s.supp>0?'warn':'ok'}">${s.supp}</span></div>
|
|
</div>
|
|
<div>
|
|
<div class="sp-title">Mean PD</div>
|
|
<div class="sp-row"><span class="sp-label">Average</span><span class="sp-val ${parseFloat(s.meanPD)>=3?'warn':'ok'}">${s.meanPD}mm</span></div>
|
|
</div>
|
|
<div>
|
|
<div class="sp-title">Numbers in red</div>
|
|
<table class="numbers-red-table">
|
|
<tr style="background:var(--surface2)"><td class="nr-label" style="font-weight:700">Metric</td><td class="nr-thresh" style="font-weight:700">Red if</td><td class="nr-count" style="font-weight:700"># Teeth</td></tr>
|
|
${nr('Probing','≥ 4',s.pd4)}
|
|
${nr('MGJ','≤ 2',s.mgj2)}
|
|
${nr('Recession','≥ 2',s.rec2)}
|
|
${nr('CAL','≥ 6',s.cal6)}
|
|
${nr('Att Ging','≤ 3',s.ag3)}
|
|
${nr('Furc','≥ 1',s.furc1)}
|
|
${nr('Mobility','≥ 1',s.mob1)}
|
|
</table>
|
|
</div>
|
|
${s.skip>0?`<div><div class="sp-title">Missing</div><div class="sp-row"><span class="sp-label">Skipped teeth</span><span class="sp-val" style="color:var(--muted)">${s.skip}</span></div></div>`:''}
|
|
`;
|
|
}
|
|
|
|
function loadExam(key) {
|
|
const T = EXAMS[key];
|
|
const meta = META[key];
|
|
document.getElementById('scBadge').textContent = meta.label;
|
|
document.getElementById('scBadge').className = `sc-badge ${meta.cls}`;
|
|
|
|
const note = NOTES[key];
|
|
const noteHtml = note ? `
|
|
<div style="padding:6px 16px;background:#fffbeb;border-bottom:1px solid #fde68a;font-size:11px;display:flex;gap:8px;align-items:flex-start">
|
|
<span style="font-weight:700;color:#92400e;font-size:10px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;margin-top:1px">Exam Note</span>
|
|
<span style="color:#78350f">${note}</span>
|
|
</div>` : '';
|
|
|
|
document.getElementById('chartArea').innerHTML = `
|
|
${noteHtml}
|
|
<div style="padding:0 0 0 0">
|
|
<!-- UPPER JAW -->
|
|
<div class="jaw-block">
|
|
<div class="jaw-label">
|
|
<span class="jaw-label-text upper">Maxillary — Teeth 1–16</span>
|
|
<div class="jaw-divider"></div>
|
|
</div>
|
|
<div style="overflow-x:auto">
|
|
<div class="grid">${buildJaw(UPPER, T, true)}</div>
|
|
</div>
|
|
</div>
|
|
<!-- separator -->
|
|
<div style="height:10px;background:var(--bg);margin:8px 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border)"></div>
|
|
<!-- LOWER JAW -->
|
|
<div class="jaw-block">
|
|
<div class="jaw-label">
|
|
<span class="jaw-label-text lower">Mandibular — Teeth 17–32</span>
|
|
<div class="jaw-divider"></div>
|
|
</div>
|
|
<div style="overflow-x:auto">
|
|
<div class="grid">${buildJaw(LOWER, T, false)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
renderStats(buildStats(T));
|
|
}
|
|
|
|
loadExam('sc1');
|
|
</script>
|
|
</body>
|
|
</html>
|