{"spec_id":"audiogram-clinical","library":"d3","language":"javascript","code":"// anyplot.ai\n// audiogram-clinical: Clinical Audiogram\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-15\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 165, bottom: 88, left: 88 };\nconst iw = width  - margin.left - margin.right;\nconst ih = height - margin.top  - margin.bottom;\n\n// --- Data: noise-induced high-frequency sensorineural hearing loss -----------\nconst frequencies = [125, 250, 500, 1000, 2000, 4000, 8000];\nconst freqLabel   = { 125: \"125\", 250: \"250\", 500: \"500\", 1000: \"1k\",\n                      2000: \"2k\", 4000: \"4k\", 8000: \"8k\" };\n\n// Right ear: gradual sensorineural notch peaking at 4 kHz (O markers, red)\nconst rightEar = [\n  { freq: 125, db: 10 }, { freq: 250, db: 15 }, { freq: 500, db: 15 },\n  { freq: 1000, db: 20 }, { freq: 2000, db: 35 }, { freq: 4000, db: 65 },\n  { freq: 8000, db: 70 },\n];\n\n// Left ear: similar sensorineural pattern, slightly worse (X markers, blue)\nconst leftEar = [\n  { freq: 125, db: 15 }, { freq: 250, db: 15 }, { freq: 500, db: 20 },\n  { freq: 1000, db: 25 }, { freq: 2000, db: 40 }, { freq: 4000, db: 70 },\n  { freq: 8000, db: 75 },\n];\n\n// ISO 8253 severity bands — Imprint-derived fills, 1.5× opacity lift for dark theme\nconst isDark = window.ANYPLOT_THEME === \"dark\";\nconst bands = [\n  { label: \"Normal\",      lo: -10, hi: 25,\n    fill: isDark ? \"rgba(0,158,115,0.15)\"   : \"rgba(0,158,115,0.10)\"   },\n  { label: \"Mild\",        lo: 25,  hi: 40,\n    fill: isDark ? \"rgba(153,179,20,0.18)\"  : \"rgba(153,179,20,0.12)\"  },\n  { label: \"Moderate\",    lo: 40,  hi: 55,\n    fill: isDark ? \"rgba(189,130,51,0.21)\"  : \"rgba(189,130,51,0.14)\"  },\n  { label: \"Mod. Severe\", lo: 55,  hi: 70,\n    fill: isDark ? \"rgba(221,204,119,0.27)\" : \"rgba(221,204,119,0.18)\" },\n  { label: \"Severe\",      lo: 70,  hi: 90,\n    fill: isDark ? \"rgba(174,48,48,0.21)\"   : \"rgba(174,48,48,0.14)\"   },\n  { label: \"Profound\",    lo: 90,  hi: 120,\n    fill: isDark ? \"rgba(174,48,48,0.39)\"   : \"rgba(174,48,48,0.26)\"   },\n];\n\n// Semantic colors: ISO 8253 clinical convention (red = right, blue = left)\nconst RIGHT_COLOR = \"#AE3030\";  // Imprint matte red — right ear convention\nconst LEFT_COLOR  = \"#4467A3\";  // Imprint blue     — left ear convention\n\n// --- SVG mount --------------------------------------------------------------\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -----------------------------------------------------------------\nconst x = d3.scaleLog()\n  .domain([125, 8000])\n  .range([0, iw]);\n\n// Inverted Y: -10 dB at top (y = 0), 120 dB at bottom (y = ih)\nconst y = d3.scaleLinear()\n  .domain([-10, 120])\n  .range([0, ih]);\n\n// --- Severity bands ---------------------------------------------------------\nbands.forEach(band => {\n  g.append(\"rect\")\n    .attr(\"x\", 0)\n    .attr(\"y\", y(band.lo))\n    .attr(\"width\", iw)\n    .attr(\"height\", y(band.hi) - y(band.lo))\n    .attr(\"fill\", band.fill);\n});\n\n// --- Gridlines --------------------------------------------------------------\n// Horizontal: every 10 dB (full y-range)\nd3.range(-10, 121, 10).forEach(db => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y(db)).attr(\"y2\", y(db))\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 0.8);\n});\n\n// Vertical: at each standard audiometric frequency\nfrequencies.forEach(freq => {\n  g.append(\"line\")\n    .attr(\"x1\", x(freq)).attr(\"x2\", x(freq))\n    .attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 0.8);\n});\n\n// --- Plot border (all 4 sides — clinical audiogram standard) ----------------\ng.append(\"rect\")\n  .attr(\"x\", 0).attr(\"y\", 0)\n  .attr(\"width\", iw).attr(\"height\", ih)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3.axisBottom(x)\n      .tickValues(frequencies)\n      .tickFormat(f => freqLabel[f])\n      .tickSize(6)\n  );\nxAxis.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\");\nxAxis.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").remove();\n\nconst yAxis = g.append(\"g\")\n  .call(\n    d3.axisLeft(y)\n      .tickValues(d3.range(-10, 121, 10))\n      .tickFormat(d3.format(\"d\"))\n      .tickSize(6)\n  );\nyAxis.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\");\nyAxis.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nyAxis.select(\".domain\").remove();\n\n// --- Ear connecting lines ---------------------------------------------------\nconst lineGen = d3.line()\n  .x(d => x(d.freq))\n  .y(d => y(d.db));\n\n// Right ear: solid red line\ng.append(\"path\")\n  .datum(rightEar)\n  .attr(\"d\", lineGen)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", RIGHT_COLOR)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-linejoin\", \"round\");\n\n// Left ear: dashed blue line (accepted clinical convention to distinguish ears)\ng.append(\"path\")\n  .datum(leftEar)\n  .attr(\"d\", lineGen)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", LEFT_COLOR)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"7,4\")\n  .attr(\"stroke-linejoin\", \"round\");\n\n// --- Markers ----------------------------------------------------------------\n// Right ear: open circles (O) — elevatedBg fill avoids blending into dark severity bands\nconst circleFill = isDark ? t.elevatedBg : t.pageBg;\ng.selectAll(\".m-right\")\n  .data(rightEar)\n  .join(\"circle\")\n  .attr(\"class\", \"m-right\")\n  .attr(\"cx\", d => x(d.freq))\n  .attr(\"cy\", d => y(d.db))\n  .attr(\"r\", 9)\n  .attr(\"fill\", circleFill)\n  .attr(\"stroke\", RIGHT_COLOR)\n  .attr(\"stroke-width\", 2.5);\n\n// Left ear: X markers — d3.symbolCross rotated 45° gives the correct × shape\nconst xSymbol = d3.symbol().type(d3.symbolCross).size(320);\ng.selectAll(\".m-left\")\n  .data(leftEar)\n  .join(\"path\")\n  .attr(\"class\", \"m-left\")\n  .attr(\"transform\", d => `translate(${x(d.freq)},${y(d.db)}) rotate(45)`)\n  .attr(\"d\", xSymbol)\n  .attr(\"fill\", LEFT_COLOR)\n  .attr(\"stroke\", \"none\");\n\n// --- Axis labels ------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Frequency (Hz)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 24)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Hearing Level (dB HL)\");\n\n// --- Severity band labels (right margin) ------------------------------------\nbands.forEach(band => {\n  svg.append(\"text\")\n    .attr(\"x\", margin.left + iw + 10)\n    .attr(\"y\", margin.top + y(band.lo) + (y(band.hi) - y(band.lo)) / 2)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(band.label);\n});\n\n// --- Legend (below title, above plot area) ----------------------------------\nconst lgY  = 60;\nconst lgX1 = margin.left + iw / 2 - 140;\nconst lgX2 = lgX1 + 175;\n\n// Right ear legend entry\nsvg.append(\"line\")\n  .attr(\"x1\", lgX1).attr(\"y1\", lgY)\n  .attr(\"x2\", lgX1 + 34).attr(\"y2\", lgY)\n  .attr(\"stroke\", RIGHT_COLOR)\n  .attr(\"stroke-width\", 2.5);\nsvg.append(\"circle\")\n  .attr(\"cx\", lgX1 + 17).attr(\"cy\", lgY).attr(\"r\", 7)\n  .attr(\"fill\", circleFill)\n  .attr(\"stroke\", RIGHT_COLOR)\n  .attr(\"stroke-width\", 2.5);\nsvg.append(\"text\")\n  .attr(\"x\", lgX1 + 44).attr(\"y\", lgY).attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"Right Ear (O)\");\n\n// Left ear legend entry\nsvg.append(\"line\")\n  .attr(\"x1\", lgX2).attr(\"y1\", lgY)\n  .attr(\"x2\", lgX2 + 34).attr(\"y2\", lgY)\n  .attr(\"stroke\", LEFT_COLOR)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"7,4\");\nsvg.append(\"path\")\n  .attr(\"transform\", `translate(${lgX2 + 17},${lgY}) rotate(45)`)\n  .attr(\"d\", d3.symbol().type(d3.symbolCross).size(220))\n  .attr(\"fill\", LEFT_COLOR)\n  .attr(\"stroke\", \"none\");\nsvg.append(\"text\")\n  .attr(\"x\", lgX2 + 44).attr(\"y\", lgY).attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"Left Ear (X)\");\n\n// --- Title ------------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 36)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"audiogram-clinical · javascript · d3 · anyplot.ai\");\n"}