{"id":339,"date":"2025-08-23T21:24:41","date_gmt":"2025-08-23T21:24:41","guid":{"rendered":"https:\/\/tomchan.hk\/?page_id=339"},"modified":"2025-08-25T17:20:05","modified_gmt":"2025-08-25T17:20:05","slug":"test-nnj","status":"publish","type":"page","link":"https:\/\/tomchan.hk\/?page_id=339","title":{"rendered":"Phototherapy &amp; Exchange Transfusion Threshold"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Phototherapy &amp; Exchange Transfusion Threshold<\/h2>\n\n\n<div id=\"nnj-app\" class=\"nnj-wrap\">\r\n  <style>\r\n    .nnj-wrap { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color:#213547; }\r\n    .nnj-card { background:#fff; border:1px solid #e5e7eb; border-radius:16px; padding:16px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }\r\n    .nnj-grid { display:grid; grid-template-columns: 1fr; gap:12px; }\r\n    .nnj-row { display:grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap:12px; }\r\n    .nnj-label { font-size:12px; color:#5b6470; margin-bottom:4px; display:block; }\r\n    .nnj-input, .nnj-select { width:100%; padding:10px 12px; border:1px solid #d1d5db; border-radius:12px; background:#f9fafb; color:#111827; }\r\n    .nnj-hint { font-size:12px; color:#6b7280; }\r\n    .nnj-pills { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }\r\n    .nnj-pill { padding:6px 10px; border-radius:999px; background:#f3f4f6; font-size:12px; border:1px solid #e5e7eb; }\r\n    .nnj-legend { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }\r\n    .nnj-legend span { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:#374151; }\r\n    .swatch { width:12px; height:12px; border-radius:3px; display:inline-block; border:1px solid rgba(0,0,0,0.08); }\r\n    .nnj-note { font-size:12px; color:#4b5563; margin-top:8px; }\r\n    .nnj-alert { background:#fff7ed; border:1px solid #fed7aa; padding:8px 10px; border-radius:10px; font-size:12px; color:#7c2d12; }\r\n    .muted { opacity:0.35; }\r\n\r\n    \/* NICE badge *\/\r\n    .nnj-badge { display:none; align-items:center; gap:6px; padding:3px 8px; border-radius:999px; font-size:11px; border:1px solid #e5e7eb; background:#f3f4f6; color:#374151; }\r\n    .nnj-badge.ok { background:#ecfdf5; border-color:#a7f3d0; color:#065f46; }\r\n    .nnj-badge.warn { background:#fef2f2; border-color:#fecaca; color:#991b1b; }\r\n\r\n    \/* Big toggle *\/\r\n    .nnj-toggle-wrap { display:flex; align-items:center; gap:10px; }\r\n    .nnj-toggle { position:relative; width:56px; height:30px; border-radius:999px; border:1px solid #d1d5db; background:#e5e7eb; cursor:pointer; transition:background .2s, border-color .2s; }\r\n    .nnj-toggle .knob { position:absolute; top:3px; left:3px; width:24px; height:24px; border-radius:999px; background:#fff; box-shadow:0 1px 2px rgba(0,0,0,0.15); transition:transform .2s; }\r\n    .nnj-toggle[aria-checked=\"true\"] { background:#22c55e22; border-color:#86efac; }\r\n    .nnj-toggle[aria-checked=\"true\"] .knob { transform:translateX(26px); }\r\n    .nnj-toggle-label { font-size:12px; color:#374151; }\r\n\r\n    @media (max-width: 520px) { .nnj-row { grid-template-columns: 1fr; } }\r\n    #nnjChart { width:100% !important; display:block; } \/* Chart.js controls height via aspectRatio *\/\r\n  <\/style>\r\n\r\n  <div class=\"nnj-grid\">\r\n    <!-- INPUTS -->\r\n    <div class=\"nnj-card\">\r\n      <div class=\"nnj-row\">\r\n        <div>\r\n          <label class=\"nnj-label\">Date & time of birth<\/label>\r\n          <input id=\"dob\" class=\"nnj-input\" type=\"datetime-local\" \/>\r\n        <\/div>\r\n        <div>\r\n          <label class=\"nnj-label\">Date & time of test<\/label>\r\n          <input id=\"dot\" class=\"nnj-input\" type=\"datetime-local\" \/>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- HOL toggle + input -->\r\n      <div class=\"nnj-row\" style=\"margin-top:8px; align-items:end;\">\r\n        <div>\r\n          <label class=\"nnj-label\">Hours of life mode<\/label>\r\n          <div class=\"nnj-toggle-wrap\">\r\n            <button id=\"holToggle\" class=\"nnj-toggle\" role=\"switch\" aria-checked=\"false\" aria-label=\"Enter hours of life directly\">\r\n              <span class=\"knob\"><\/span>\r\n            <\/button>\r\n            <span class=\"nnj-toggle-label\">Enter hours of life directly (disables date\/time fields)<\/span>\r\n          <\/div>\r\n          <!-- Hidden state carrier for logic -->\r\n          <input id=\"holMode\" type=\"checkbox\" style=\"display:none\" \/>\r\n        <\/div>\r\n        <div>\r\n          <label class=\"nnj-label\">Hours of life (0\u2013336)<\/label>\r\n          <input id=\"hol\" class=\"nnj-input\" type=\"number\" inputmode=\"decimal\" step=\"0.1\" min=\"0\" max=\"336\" placeholder=\"e.g., 36.5\" disabled \/>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"nnj-row\" style=\"margin-top:8px;\">\r\n        <div>\r\n          <label class=\"nnj-label\">Gestational age<\/label>\r\n          <select id=\"ga\" class=\"nnj-select\">\r\n            <option value=\"\">\u2014 show all PT curves \u2014<\/option>\r\n            <!-- NICE preterm -->\r\n            <option value=\"24\">24 weeks<\/option>\r\n            <option value=\"25\">25 weeks<\/option>\r\n            <option value=\"26\">26 weeks<\/option>\r\n            <option value=\"27\">27 weeks<\/option>\r\n            <option value=\"28\">28 weeks<\/option>\r\n            <option value=\"29\">29 weeks<\/option>\r\n            <option value=\"30\">30 weeks<\/option>\r\n            <option value=\"31\">31 weeks<\/option>\r\n            <option value=\"32\">32 weeks<\/option>\r\n            <option value=\"33\">33 weeks<\/option>\r\n            <option value=\"34\">34 weeks<\/option>\r\n            <!-- AAP -->\r\n            <option value=\"35\">35 weeks<\/option>\r\n            <option value=\"36\">36 weeks<\/option>\r\n            <option value=\"37\">37 weeks<\/option>\r\n            <option value=\"38\">\u226538 weeks<\/option>\r\n          <\/select>\r\n          <div style=\"margin-top:6px;\">\r\n            <span id=\"niceBadge\" class=\"nnj-badge\">NICE not loaded<\/span>\r\n          <\/div>\r\n        <\/div>\r\n        <div>\r\n          <label class=\"nnj-label\">Neurotoxicity risk factors (for ET selection)<\/label>\r\n          <select id=\"risk\" class=\"nnj-select\">\r\n            <option value=\"no\">No (none\/only GA)<\/option>\r\n            <option value=\"yes\">Yes (any risk factor)<\/option>\r\n          <\/select>\r\n          <div class=\"nnj-hint\">Default shows AAP term\/late-preterm. GA 24\u201334 uses NICE preterm PT\/ET.<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"nnj-row\" style=\"margin-top:8px;\">\r\n        <div>\r\n          <label class=\"nnj-label\">Serum bilirubin (patient)<\/label>\r\n          <input id=\"tsb\" class=\"nnj-input\" type=\"number\" inputmode=\"decimal\" placeholder=\"e.g., 220\" \/>\r\n        <\/div>\r\n        <div>\r\n          <label class=\"nnj-label\">Units<\/label>\r\n          <select id=\"unit\" class=\"nnj-select\">\r\n            <option value=\"umol\">\u00b5mol\/L (SI)<\/option>\r\n            <option value=\"mgdl\">mg\/dL<\/option>\r\n          <\/select>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"nnj-pills\" style=\"margin-top:10px;\">\r\n        <div id=\"agePill\" class=\"nnj-pill\">Hours of life: \u2014<\/div>\r\n        <div id=\"ptPill\" class=\"nnj-pill\">PT threshold: \u2014 \u00b5mol\/L<\/div>\r\n        <div id=\"etNoPill\" class=\"nnj-pill\">ET (no RF): \u2014 \u00b5mol\/L<\/div>\r\n        <div id=\"etYesPill\" class=\"nnj-pill\">ET (+RF): \u2014 \u00b5mol\/L<\/div>\r\n      <\/div>\r\n\r\n      <div id=\"fallbackMsg\" class=\"nnj-alert\" style=\"display:none; margin-top:8px;\">\r\n        Couldn\u2019t load hourly JSON; using built-ins (less precise). Upload\r\n        <code>https:\/\/tomchan.hk\/tomwebapp\/nnj.json<\/code> (AAP) and\r\n        <code>https:\/\/tomchan.hk\/tomwebapp\/nicennj.json<\/code> (NICE preterm).\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- CHART (16:9 height) -->\r\n    <div class=\"nnj-card\">\r\n      <canvas id=\"nnjChart\"><\/canvas>\r\n      <div class=\"nnj-legend\" id=\"legendRow\"><\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Chart.js + annotation -->\r\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js@4.4.3\/dist\/chart.umd.min.js\"><\/script>\r\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-plugin-annotation@3.1.0\/dist\/chartjs-plugin-annotation.min.js\"><\/script>\r\n\r\n  <script>\r\n    (function () {\r\n      const COLORS = {\r\n        pt38: \"#ed6d85\",  \/\/ \u226538w PT (AAP)\r\n        pt37: \"#f4a261\",\r\n        pt36: \"#f6d55c\",\r\n        pt35: \"#56a0e5\",\r\n        etNo: \"#b5dede\",  \/\/ AAP ET (no RF)\r\n        etYes: \"#c8b3fb\", \/\/ AAP ET (+RF)\r\n        nicePT: \"#56a0e5\", \/\/ NICE PT\r\n        niceET: \"#c8b3fb\", \/\/ NICE ET\r\n        grey: \"#c9cbce\"\r\n      };\r\n      const MGDL_TO_UMOL = 17.104;\r\n      const JSON_URL_AAP  = \"https:\/\/tomchan.hk\/tomwebapp\/nnj.json\";\r\n      const JSON_URL_NICE = \"https:\/\/tomchan.hk\/tomwebapp\/nicennj.json\";\r\n      const BASE_Y_MIN = 4 * MGDL_TO_UMOL; \/\/ ~68.4 \u00b5mol\/L default baseline\r\n\r\n      \/\/ --- Fallback AAP anchors (mg\/dL) ---\r\n      const PT_ANCHORS = {\r\n        \"38\": [6.4,10.5,14.0,16.6,18.2,18.2,18.2,18.2,18.2,18.2,18.2,18.2,18.2,18.2,18.2],\r\n        \"37\": [5.9,10.0,13.5,16.1,17.9,18.0,18.1,18.2,18.2,18.2,18.2,18.2,18.2,18.2,18.2],\r\n        \"36\": [5.4, 9.4,12.8,15.4,17.0,17.1,17.3,17.4,17.5,17.6,17.7,17.9,18.0,18.1,18.2],\r\n        \"35\": [4.9, 8.9,12.2,14.6,16.1,16.3,16.4,16.5,16.6,16.8,16.9,17.0,17.1,17.3,17.4]\r\n      };\r\n      const ET_NO_ANCHORS = {\r\n        \"38\": [18.0,21.4,24.0,25.9,27.0,27.0,27.0,27.0,27.0,27.0,27.0,27.0,27.0,27.0,27.0],\r\n        \"37\": [17.0,20.3,23.1,25.2,26.6,26.7,26.9,27.0,27.0,27.0,27.0,27.0,27.0,27.0,27.0],\r\n        \"36\": [15.9,19.1,21.9,24.1,25.5,25.7,25.9,26.1,26.2,26.4,26.5,26.7,26.8,26.9,27.0],\r\n        \"35\": [14.9,17.9,20.7,22.9,24.5,24.7,24.9,25.1,25.3,25.5,25.7,25.9,26.0,26.2,26.3]\r\n      };\r\n      const ET_YES_ANCHORS = {\r\n        \"38\": [14.8,17.7,20.1,22.1,23.5,23.5,23.5,23.5,23.5,23.5,23.5,23.5,23.5,23.5,23.5],\r\n        \"37\": [14.3,17.2,19.7,21.7,23.1,23.2,23.4,23.5,23.5,23.5,23.5,23.5,23.5,23.5,23.5],\r\n        \"36\": [13.7,16.6,19.1,20.9,22.1,22.3,22.5,22.6,22.8,22.9,23.1,23.2,23.3,23.4,23.5],\r\n        \"35\": [13.1,16.1,18.5,20.1,21.1,21.3,21.5,21.7,21.9,22.1,22.3,22.5,22.6,22.8,22.9]\r\n      };\r\n\r\n      \/\/ ---- Helpers ----\r\n      function expandAnchorsToHourly(mgdlAnchors) {\r\n        const hourly = [];\r\n        for (let d = 0; d < mgdlAnchors.length - 1; d++) {\r\n          const y0 = mgdlAnchors[d], y1 = mgdlAnchors[d + 1];\r\n          for (let hr = 0; hr < 24; hr++) hourly.push(y0 + (y1 - y0) * (hr \/ 24));\r\n        }\r\n        while (hourly.length < 337) hourly.push(mgdlAnchors[mgdlAnchors.length - 1]);\r\n        return hourly.slice(0, 337);\r\n      }\r\n      function thrAtHourHourly(arr_mgdl, h) {\r\n        const hour = Math.max(0, Math.min(336, Math.round(h || 0)));\r\n        return (arr_mgdl && arr_mgdl.length) ? arr_mgdl[hour] * MGDL_TO_UMOL : null;\r\n      }\r\n\r\n      \/\/ Smooth display series: resample every 0.25 h using linear interp (JSON untouched)\r\n      function resampleHourlyToStep(arr_mgdl, step) {\r\n        if (!arr_mgdl || !arr_mgdl.length) return [];\r\n        const out = [];\r\n        const maxH = Math.min(336, arr_mgdl.length - 1);\r\n        for (let t = 0; t <= maxH + 1e-9; t += step) {\r\n          const t0 = Math.floor(t);\r\n          const t1 = Math.min(maxH, t0 + 1);\r\n          const frac = t - t0;\r\n          const y0 = arr_mgdl[t0], y1 = arr_mgdl[t1];\r\n          const y = (1 - frac) * y0 + frac * y1; \/\/ linear between hours\r\n          out.push({ x: t, y: y * MGDL_TO_UMOL });\r\n        }\r\n        return out;\r\n      }\r\n      function seriesSmoothFromHourly(arr_mgdl, step=0.25) {\r\n        return resampleHourlyToStep(arr_mgdl, step);\r\n      }\r\n\r\n      function round1(x){ return Math.round(x*10)\/10; }\r\n      function ceilToStep(x, step){ return Math.ceil(x\/step)*step; }\r\n      const h = (id) => document.getElementById(id);\r\n\r\n      \/\/ --- 150ms debounce helper ---\r\n      function debounce(fn, wait=150){\r\n        let t;\r\n        return (...args) => {\r\n          clearTimeout(t);\r\n          t = setTimeout(() => fn.apply(null, args), wait);\r\n        };\r\n      }\r\n\r\n      \/\/ ---- Data holders ----\r\n      let AAP = {\r\n        unit: \"mg\/dL\",\r\n        pt: { \"35\": expandAnchorsToHourly(PT_ANCHORS[\"35\"]),\r\n              \"36\": expandAnchorsToHourly(PT_ANCHORS[\"36\"]),\r\n              \"37\": expandAnchorsToHourly(PT_ANCHORS[\"37\"]),\r\n              \"38\": expandAnchorsToHourly(PT_ANCHORS[\"38\"]) },\r\n        et_norf: { \"35\": expandAnchorsToHourly(ET_NO_ANCHORS[\"35\"]),\r\n                   \"36\": expandAnchorsToHourly(ET_NO_ANCHORS[\"36\"]),\r\n                   \"37\": expandAnchorsToHourly(ET_NO_ANCHORS[\"37\"]),\r\n                   \"38\": expandAnchorsToHourly(ET_NO_ANCHORS[\"38\"]) },\r\n        et_rf: { \"35\": expandAnchorsToHourly(ET_YES_ANCHORS[\"35\"]),\r\n                 \"36\": expandAnchorsToHourly(ET_YES_ANCHORS[\"36\"]),\r\n                 \"37\": expandAnchorsToHourly(ET_YES_ANCHORS[\"37\"]),\r\n                 \"38\": expandAnchorsToHourly(ET_YES_ANCHORS[\"38\"]) },\r\n        usingFallback: true\r\n      };\r\n\r\n      let NICE = { unit:\"mg\/dL\", pt:{}, et:{}, usingFallback:true, loaded:false };\r\n\r\n      \/\/ Normalize NICE JSON (accept mg\/dL or \u00b5mol\/L; ensure 337 points)\r\n      function normalizeNice(json){\r\n        if(!json || !json.pt || !json.et) return null;\r\n        const isUmol = String(json.unit || \"mg\/dL\").toLowerCase().includes(\"umol\");\r\n        const toMgdl = (obj)=>{\r\n          const out={};\r\n          for(const k of Object.keys(obj)){\r\n            const arr=obj[k];\r\n            if(!Array.isArray(arr) || arr.length!==337) return null;\r\n            out[k] = isUmol ? arr.map(v => v \/ MGDL_TO_UMOL) : arr.slice();\r\n          }\r\n          return out;\r\n        };\r\n        const pt = toMgdl(json.pt); if(!pt) return null;\r\n        const et = toMgdl(json.et); if(!et) return null;\r\n        return { unit:\"mg\/dL\", pt, et };\r\n      }\r\n\r\n      \/\/ Load both JSONs (cache-busted)\r\n      Promise.allSettled([\r\n        fetch(JSON_URL_AAP  + \"?v=\" + Date.now(), {cache:\"no-store\"}).then(r => r.ok ? r.json() : Promise.reject(`AAP HTTP ${r.status}`)),\r\n        fetch(JSON_URL_NICE + \"?v=\" + Date.now(), {cache:\"no-store\"}).then(r => r.ok ? r.json() : Promise.reject(`NICE HTTP ${r.status}`))\r\n      ]).then(([aapRes, niceRes]) => {\r\n        if (aapRes.status === \"fulfilled\" && aapRes.value?.pt && aapRes.value?.et_norf && aapRes.value?.et_rf) {\r\n          AAP = { unit:aapRes.value.unit || \"mg\/dL\", pt:aapRes.value.pt, et_norf:aapRes.value.et_norf, et_rf:aapRes.value.et_rf, usingFallback:false };\r\n        }\r\n        if (niceRes.status === \"fulfilled\") {\r\n          const norm = normalizeNice(niceRes.value);\r\n          if (norm){ NICE = { ...norm, usingFallback:false, loaded:true }; }\r\n        }\r\n        h(\"fallbackMsg\").style.display = (AAP.usingFallback && !NICE.loaded) ? \"block\" : \"none\";\r\n        initChart();\r\n      }).catch(() => { h(\"fallbackMsg\").style.display = \"block\"; initChart(); });\r\n\r\n      let chart;\r\n\r\n      function mkLine(label, data, color, width, hidden=false) {\r\n        return {\r\n          type:\"line\", label, data,\r\n          borderColor: color, backgroundColor: color + \"33\",\r\n          borderWidth: width, pointRadius: 0,\r\n          cubicInterpolationMode: \"monotone\",\r\n          tension: 0.4,\r\n          borderJoinStyle: \"round\",\r\n          borderCapStyle: \"round\",\r\n          hidden\r\n        };\r\n      }\r\n\r\n      function initChart() {\r\n        const ctx = h(\"nnjChart\").getContext(\"2d\");\r\n\r\n        \/\/ AAP PT datasets (default view)\r\n        const hourlyPT = {\r\n          \"38\": seriesSmoothFromHourly(AAP.pt[\"38\"]),\r\n          \"37\": seriesSmoothFromHourly(AAP.pt[\"37\"]),\r\n          \"36\": seriesSmoothFromHourly(AAP.pt[\"36\"]),\r\n          \"35\": seriesSmoothFromHourly(AAP.pt[\"35\"])\r\n        };\r\n\r\n        const datasets = [\r\n          mkLine(\"PT \u226538 w (AAP)\", hourlyPT[\"38\"], COLORS.pt38, 2),\r\n          mkLine(\"PT 37 w (AAP)\",  hourlyPT[\"37\"], COLORS.pt37, 2),\r\n          mkLine(\"PT 36 w (AAP)\",  hourlyPT[\"36\"], COLORS.pt36, 2),\r\n          mkLine(\"PT 35 w (AAP)\",  hourlyPT[\"35\"], COLORS.pt35, 2),\r\n          mkLine(\"ET (AAP no RF)\", [], COLORS.etNo, 2, true),\r\n          mkLine(\"ET (AAP +RF)\",   [], COLORS.etYes, 2, true),\r\n          mkLine(\"PT (NICE 24\u201334w)\", [], COLORS.nicePT, 2, true),\r\n          mkLine(\"ET (NICE)\",        [], COLORS.niceET, 2, true),\r\n          { type: \"scatter\", label: \"Patient\", data: [], backgroundColor: \"#ed6d85\",\r\n            pointRadius: 5, pointHoverRadius: 6, showLine: false, hidden: true }\r\n        ];\r\n\r\n        chart = new Chart(ctx, {\r\n          type: \"line\",\r\n          data: { datasets },\r\n          options: {\r\n            responsive: true,\r\n            maintainAspectRatio: true,\r\n            aspectRatio: 16\/9,\r\n            interaction: { mode: \"nearest\", intersect: false },\r\n            layout: { padding: { top: 42 } }, \/* extra headroom for the label *\/\r\n            scales: {\r\n              x: { type: \"linear\", min: 0, max: 336,\r\n                   title: { display: true, text: \"Hours of life\" },\r\n                   grid: { color: \"rgba(0,0,0,0.06)\" },\r\n                   ticks: { stepSize: 24, callback: (v)=>`${v}`, maxTicksLimit: 15 } },\r\n              y: { type: \"linear\", min: BASE_Y_MIN, suggestedMax: 500,\r\n                   title: { display: true, text: \"Total serum bilirubin (\u00b5mol\/L)\" },\r\n                   ticks: { stepSize: MGDL_TO_UMOL, callback: (val)=> Math.round(val) },\r\n                   grid: { color: \"rgba(0,0,0,0.06)\" } }\r\n            },\r\n            plugins: {\r\n              legend: { display: false },\r\n              annotation: {\r\n                annotations: {\r\n                  xline: {\r\n                    type: \"line\", xMin: -1, xMax: -1,\r\n                    borderColor: \"rgba(0,0,0,0.35)\", borderWidth: 1, borderDash: [4,4],\r\n                    label: {\r\n                      display: false,\r\n                      position: \"end\",     \/* <-- place at TOP of the chart *\/\r\n                      yAdjust: 10,         \/* <-- nudge down a bit inside chart *\/\r\n                      xAdjust: 6,          \/* <-- nudge right off the line *\/\r\n                      textAlign: \"left\",\r\n                      backgroundColor: \"rgba(255,255,255,0.92)\",\r\n                      color: \"#111827\",\r\n                      padding: 6,\r\n                      borderRadius: 8,\r\n                      borderWidth: 1,\r\n                      borderColor: \"rgba(0,0,0,0.12)\"\r\n                    }\r\n                  }\r\n                }\r\n              },\r\n              tooltip: { enabled: true }\r\n            }\r\n          }\r\n        });\r\n\r\n        \/\/ Legend (AAP default)\r\n        const legend = h(\"legendRow\");\r\n        legend.innerHTML = \"\";\r\n        [\r\n          { c: COLORS.pt38, t: \"PT \u226538 w (AAP)\" },\r\n          { c: COLORS.pt37, t: \"PT 37 w (AAP)\" },\r\n          { c: COLORS.pt36, t: \"PT 36 w (AAP)\" },\r\n          { c: COLORS.pt35, t: \"PT 35 w (AAP)\" },\r\n          { c: COLORS.etNo, t: \"ET (AAP no RF)\" },\r\n          { c: COLORS.etYes, t: \"ET (AAP +RF)\" }\r\n        ].forEach(it => {\r\n          const s = document.createElement(\"span\");\r\n          s.innerHTML = `<i class=\"swatch\" style=\"background:${it.c}\"><\/i>${it.t}`;\r\n          legend.appendChild(s);\r\n        });\r\n\r\n        \/\/ 150ms debounced update function\r\n        const upd = debounce(updatePlot, 150);\r\n\r\n        \/\/ Wire listeners with debounce\r\n        [\"dob\",\"dot\",\"ga\",\"risk\",\"tsb\",\"unit\",\"hol\"].forEach(id => {\r\n          h(id).addEventListener(\"input\", upd);\r\n          h(id).addEventListener(\"change\", upd);\r\n        });\r\n        \/\/ Toggle behavior (also debounced refresh)\r\n        h(\"holToggle\").addEventListener(\"click\", () => {\r\n          const cb = h(\"holMode\");\r\n          cb.checked = !cb.checked;\r\n          h(\"holToggle\").setAttribute(\"aria-checked\", cb.checked ? \"true\" : \"false\");\r\n          syncHolModeUI();\r\n          upd();\r\n        });\r\n\r\n        \/\/ Initial UI sync\r\n        h(\"holToggle\").setAttribute(\"aria-checked\",\"false\");\r\n        syncHolModeUI();\r\n        upd();\r\n      }\r\n\r\n      function syncHolModeUI(){\r\n        const usingManual = h(\"holMode\").checked;\r\n        h(\"hol\").disabled = !usingManual;\r\n        h(\"dob\").disabled = usingManual;\r\n        h(\"dot\").disabled = usingManual;\r\n        h(\"dob\").classList.toggle(\"muted\", usingManual);\r\n        h(\"dot\").classList.toggle(\"muted\", usingManual);\r\n        h(\"hol\").classList.toggle(\"muted\", !usingManual);\r\n      }\r\n\r\n      function updateNiceBadge(ga) {\r\n        const badge = h(\"niceBadge\");\r\n        if (!badge) return;\r\n        if (!ga) { badge.style.display = \"none\"; return; }\r\n        const n = parseInt(ga, 10);\r\n        if (n >= 24 && n <= 34) {\r\n          const okPT = !!(NICE && NICE.pt && NICE.pt[ga] && NICE.pt[ga].length === 337);\r\n          const okET = !!(NICE && NICE.et && NICE.et[ga] && NICE.et[ga].length === 337);\r\n          badge.style.display = \"inline-flex\";\r\n          badge.textContent = (okPT && okET) ? \"NICE Guideline\" : \"NICE missing\";\r\n          badge.classList.remove(\"ok\",\"warn\");\r\n          badge.classList.add((okPT && okET) ? \"ok\" : \"warn\");\r\n        } else {\r\n          badge.style.display = \"none\";\r\n        }\r\n      }\r\n\r\n      function updatePlot() {\r\n        if (!chart) return;\r\n\r\n        const ga = h(\"ga\").value || \"\";\r\n        const risk = h(\"risk\").value === \"yes\";\r\n        const unit = h(\"unit\").value;\r\n        const tsbRaw = parseFloat(h(\"tsb\").value);\r\n        const tsbUmol = isFinite(tsbRaw) ? (unit === \"mgdl\" ? tsbRaw * MGDL_TO_UMOL : tsbRaw) : null;\r\n\r\n        \/\/ Hours of life (manual toggle)\r\n        const manualMode = h(\"holMode\").checked;\r\n        let hol = null;\r\n        if (manualMode) {\r\n          const hv = parseFloat(h(\"hol\").value);\r\n          hol = isFinite(hv) ? Math.max(0, Math.min(336, hv)) : null;\r\n        } else {\r\n          const dob = h(\"dob\").value ? new Date(h(\"dob\").value) : null;\r\n          const dot = h(\"dot\").value ? new Date(h(\"dot\").value) : null;\r\n          if (dob && dot) {\r\n            const ms = (dot - dob);\r\n            hol = ms \/ 3_600_000;\r\n            if (hol < 0) hol = null;\r\n          }\r\n        }\r\n\r\n        \/\/ NICE badge\r\n        updateNiceBadge(ga);\r\n\r\n        \/\/ Dataset indices\r\n        const dsAAP_PT = [0,1,2,3];\r\n        const dsAAP_ET_NO = 4;\r\n        const dsAAP_ET_RF = 5;\r\n        const dsNICE_PT = 6;\r\n        const dsNICE_ET = 7;\r\n        const dsPATIENT = 8;\r\n\r\n        \/\/ Reset visibilities\r\n        dsAAP_PT.forEach(i => { chart.data.datasets[i].hidden = false; });\r\n        chart.data.datasets[dsAAP_ET_NO].hidden = true; chart.data.datasets[dsAAP_ET_NO].data = [];\r\n        chart.data.datasets[dsAAP_ET_RF].hidden = true; chart.data.datasets[dsAAP_ET_RF].data = [];\r\n        chart.data.datasets[dsNICE_PT].hidden  = true;  chart.data.datasets[dsNICE_PT].data = [];\r\n        chart.data.datasets[dsNICE_ET].hidden  = true;  chart.data.datasets[dsNICE_ET].data = [];\r\n\r\n        \/\/ Default coloring for AAP PTs (when no GA)\r\n        const origColors = [COLORS.pt38, COLORS.pt37, COLORS.pt36, COLORS.pt35];\r\n        dsAAP_PT.forEach((i, idx) => {\r\n          const ds = chart.data.datasets[i];\r\n          ds.borderWidth = 2;\r\n          if (!ga) {\r\n            ds.borderColor = origColors[idx];\r\n            ds.backgroundColor = ds.borderColor + \"26\";\r\n          }\r\n        });\r\n\r\n        \/\/ Decide mode\r\n        const gaNum = parseInt(ga || \"0\", 10);\r\n        const NICE_MODE = (ga && gaNum >= 24 && gaNum <= 34 && NICE.loaded && NICE.pt[ga]);\r\n\r\n        \/\/ Y-axis start: 0 in NICE mode; raised baseline otherwise\r\n        chart.options.scales.y.min = NICE_MODE ? 0 : BASE_Y_MIN;\r\n\r\n        let ptThr = null, etNoThr = null, etYesThr = null;\r\n\r\n        if (NICE_MODE) {\r\n          \/\/ Hide all AAP curves\r\n          dsAAP_PT.forEach(i => { chart.data.datasets[i].hidden = true; });\r\n          chart.data.datasets[dsAAP_ET_NO].hidden = true;\r\n          chart.data.datasets[dsAAP_ET_RF].hidden = true;\r\n\r\n          \/\/ Show NICE PT always (smoothed)\r\n          const nicePTArr = NICE.pt[ga];\r\n          chart.data.datasets[dsNICE_PT].data = seriesSmoothFromHourly(nicePTArr);\r\n          chart.data.datasets[dsNICE_PT].hidden = false;\r\n\r\n          \/\/ Show NICE ET always if present (smoothed)\r\n          const niceETArr = NICE.et[ga] || [];\r\n          if (niceETArr.length === 337) {\r\n            chart.data.datasets[dsNICE_ET].data = seriesSmoothFromHourly(niceETArr);\r\n            chart.data.datasets[dsNICE_ET].hidden = false;\r\n          }\r\n\r\n          \/\/ Thresholds only if HOL known\r\n          if (hol !== null) {\r\n            ptThr = thrAtHourHourly(nicePTArr, hol);\r\n            const niceETthr = thrAtHourHourly(niceETArr, hol);\r\n            h(\"ptPill\").textContent   = `PT threshold: ${ptThr ? Math.round(ptThr) : \"\u2014\"} \u00b5mol\/L`;\r\n            h(\"etNoPill\").textContent = `ET (NICE): ${niceETthr ? Math.round(niceETthr) : \"\u2014\"} \u00b5mol\/L`;\r\n            h(\"etYesPill\").textContent= `ET (+RF): \u2014 \u00b5mol\/L`;\r\n          } else {\r\n            h(\"ptPill\").textContent   = `PT threshold: \u2014 \u00b5mol\/L`;\r\n            h(\"etNoPill\").textContent = `ET (NICE): \u2014 \u00b5mol\/L`;\r\n            h(\"etYesPill\").textContent= `ET (+RF): \u2014 \u00b5mol\/L`;\r\n          }\r\n\r\n        } else if (ga) {\r\n          \/\/ AAP mode\r\n          const labels = [\"PT \u226538 w (AAP)\",\"PT 37 w (AAP)\",\"PT 36 w (AAP)\",\"PT 35 w (AAP)\"];\r\n          dsAAP_PT.forEach((i, idx) => {\r\n            const ds = chart.data.datasets[i];\r\n            const match = (labels[idx].includes(\"\u226538\") && ga===\"38\") || labels[idx].includes(` ${ga} w`);\r\n            ds.borderWidth = match ? 3 : 2;\r\n            ds.borderColor = match ? origColors[idx] : COLORS.grey;\r\n            ds.backgroundColor = match ? ds.borderColor + \"22\" : COLORS.grey + \"33\";\r\n          });\r\n\r\n          if (hol !== null) {\r\n            ptThr   = thrAtHourHourly(AAP.pt[ga], hol);\r\n            etNoThr = thrAtHourHourly(AAP.et_norf[ga] || [], hol);\r\n            etYesThr= thrAtHourHourly(AAP.et_rf[ga]   || [], hol);\r\n\r\n            if (tsbUmol !== null && ptThr !== null && tsbUmol > ptThr) {\r\n              if (AAP.et_norf[ga]?.length) {\r\n                chart.data.datasets[dsAAP_ET_NO].data = seriesSmoothFromHourly(AAP.et_norf[ga]);\r\n                chart.data.datasets[dsAAP_ET_NO].hidden = false;\r\n              }\r\n              if (AAP.et_rf[ga]?.length) {\r\n                chart.data.datasets[dsAAP_ET_RF].data = seriesSmoothFromHourly(AAP.et_rf[ga]);\r\n                chart.data.datasets[dsAAP_ET_RF].hidden = false;\r\n              }\r\n            }\r\n\r\n            h(\"ptPill\").textContent   = `PT threshold: ${ptThr ? Math.round(ptThr) : \"\u2014\"} \u00b5mol\/L`;\r\n            h(\"etNoPill\").textContent = `ET (no RF): ${etNoThr ? Math.round(etNoThr) : \"\u2014\"} \u00b5mol\/L`;\r\n            h(\"etYesPill\").textContent= `ET (+RF): ${etYesThr ? Math.round(etYesThr) : \"\u2014\"} \u00b5mol\/L`;\r\n          } else {\r\n            h(\"ptPill\").textContent   = `PT threshold: \u2014 \u00b5mol\/L`;\r\n            h(\"etNoPill\").textContent = `ET (no RF): \u2014 \u00b5mol\/L`;\r\n            h(\"etYesPill\").textContent= `ET (+RF): \u2014 \u00b5mol\/L`;\r\n          }\r\n\r\n        } else {\r\n          \/\/ No GA \u2014 default AAP view\r\n          h(\"ptPill\").textContent   = `PT threshold: \u2014 \u00b5mol\/L`;\r\n          h(\"etNoPill\").textContent = `ET (no RF): \u2014 \u00b5mol\/L`;\r\n          h(\"etYesPill\").textContent= `ET (+RF): \u2014 \u00b5mol\/L`;\r\n        }\r\n\r\n        \/\/ Patient point & vertical annotation\r\n        const patientDS = chart.data.datasets[dsPATIENT];\r\n        if (tsbUmol !== null && hol !== null) {\r\n          patientDS.hidden = false;\r\n          patientDS.data = [{ x: Math.max(0, Math.min(336, hol)), y: tsbUmol }];\r\n        } else {\r\n          patientDS.hidden = true;\r\n          patientDS.data = [];\r\n        }\r\n\r\n        \/\/ Annotation (now at TOP)\r\n        const ann = chart.options.plugins.annotation.annotations.xline;\r\n        if (tsbUmol !== null && hol !== null) {\r\n          ann.xMin = ann.xMax = hol;\r\n          ann.label.display = true;\r\n          const isNICE = (ga && parseInt(ga,10) >= 24 && parseInt(ga,10) <= 34 && NICE.loaded && NICE.pt[ga]);\r\n          const etNiceVal = (isNICE && NICE.et[ga]) ? thrAtHourHourly(NICE.et[ga],hol) : null;\r\n          const lines = [\r\n            `HOL: ${round1(hol)} h`,\r\n            `PT thr: ${ptThr ? Math.round(ptThr) : \"\u2014\"} \u00b5mol\/L`,\r\n            isNICE\r\n              ? `ET (NICE): ${etNiceVal ? Math.round(etNiceVal) : \"\u2014\"} \u00b5mol\/L`\r\n              : `ET (no RF): ${etNoThr ? Math.round(etNoThr) : \"\u2014\"} \u00b5mol\/L`,\r\n            isNICE ? `Guideline: NICE (preterm)` : `ET (+RF): ${etYesThr ? Math.round(etYesThr) : \"\u2014\"} \u00b5mol\/L`\r\n          ];\r\n          ann.label.content = lines.join(\"\\n\");\r\n        } else {\r\n          ann.xMin = ann.xMax = -1;\r\n          ann.label.display = false;\r\n        }\r\n\r\n        \/\/ Hours-of-life pill\r\n        h(\"agePill\").textContent = `Hours of life: ${hol !== null ? round1(hol) : \"\u2014\"}`;\r\n\r\n        \/\/ Y max \u2192 snap to 1 mg\/dL steps\r\n        const dm = Math.max(\r\n          ...chart.data.datasets.filter(d=>!d.hidden).flatMap(d => d.data.map(p => p.y)),\r\n          tsbUmol || 0, 400\r\n        );\r\n        chart.options.scales.y.max = ceilToStep(Math.max(300, dm * 1.08), MGDL_TO_UMOL);\r\n\r\n        chart.update();\r\n      }\r\n    })();\r\n  <\/script>\r\n<\/div>\r\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Phototherapy &amp; Exchange Transfusion Threshold<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-full.php","meta":{"footnotes":""},"tags":[],"class_list":["post-339","page","type-page","status-publish","article"],"_links":{"self":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/339","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=339"}],"version-history":[{"count":17,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/339\/revisions"}],"predecessor-version":[{"id":393,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/339\/revisions\/393"}],"wp:attachment":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=339"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=339"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}