{"spec_id":"audiogram-clinical","library":"altair","language":"python","code":"\"\"\" anyplot.ai\naudiogram-clinical: Clinical Audiogram\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-15\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Clinical convention (semantic exception): red=right ear, blue=left ear —\n# universally recognised audiology symbols; not the default Imprint order.\nRIGHT_COLOR = \"#AE3030\"  # Imprint position 5 (matte red)\nLEFT_COLOR = \"#4467A3\"  # Imprint position 3 (blue)\n\n# Imprint palette — first 6 positions used for severity band fills at low opacity\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data — bilateral high-frequency sensorineural notch (classic noise-induced pattern)\n# Notch peaks at 4 kHz, partial recovery at 8 kHz; low frequencies largely intact\nfrequencies = [125, 250, 500, 1000, 2000, 4000, 8000]\nright_thresholds = [10, 10, 15, 20, 35, 65, 55]\nleft_thresholds = [15, 15, 20, 25, 40, 70, 60]\n\nright_df = pd.DataFrame({\"frequency\": frequencies, \"threshold\": right_thresholds, \"ear\": \"Right Ear (O)\"})\nleft_df = pd.DataFrame({\"frequency\": frequencies, \"threshold\": left_thresholds, \"ear\": \"Left Ear (X)\"})\ndf = pd.concat([right_df, left_df], ignore_index=True)\n\n# Severity band boundaries (dB HL)\nband_rows = [\n    {\"y\": -10, \"y2\": 25, \"label\": \"Normal\"},\n    {\"y\": 25, \"y2\": 40, \"label\": \"Mild\"},\n    {\"y\": 40, \"y2\": 55, \"label\": \"Moderate\"},\n    {\"y\": 55, \"y2\": 70, \"label\": \"Mod. Severe\"},\n    {\"y\": 70, \"y2\": 90, \"label\": \"Severe\"},\n    {\"y\": 90, \"y2\": 120, \"label\": \"Profound\"},\n]\nband_data = pd.DataFrame(band_rows)\nband_data[\"mid_y\"] = (band_data[\"y\"] + band_data[\"y2\"]) / 2\n# Labels at the low-frequency left margin where ear data is always in the Normal band;\n# all Mild-to-Profound labels sit well below (larger dB HL) the ear lines at 125-250 Hz\nband_data[\"x_label\"] = 160\n\nbands_domain = band_data[\"label\"].tolist()\n\n# Shared scales\nx_scale = alt.Scale(type=\"log\", domain=[125, 8000])\ny_scale = alt.Scale(domain=[-10, 120], reverse=True)\n\nx_axis = alt.Axis(\n    title=\"Frequency (Hz)\",\n    values=[125, 250, 500, 1000, 2000, 4000, 8000],\n    labelExpr=\"datum.value >= 1000 ? (datum.value / 1000) + 'k' : datum.value\",\n)\ny_axis = alt.Axis(title=\"Hearing Level (dB HL)\", values=list(range(-10, 121, 10)))\n\n# Severity bands — rect marks spanning the full view width\nbands = (\n    alt.Chart(band_data)\n    .mark_rect(opacity=0.15)\n    .encode(\n        y=alt.Y(\"y:Q\", scale=y_scale),\n        y2=\"y2:Q\",\n        color=alt.Color(\"label:N\", scale=alt.Scale(domain=bands_domain, range=IMPRINT_PALETTE), legend=None),\n    )\n)\n\n# Severity band labels — left-aligned from the low-frequency left margin\nband_labels = (\n    alt.Chart(band_data)\n    .mark_text(align=\"left\", fontSize=10, fontStyle=\"italic\")\n    .encode(\n        x=alt.X(\"x_label:Q\", scale=x_scale),\n        y=alt.Y(\"mid_y:Q\", scale=y_scale),\n        text=\"label:N\",\n        color=alt.value(INK_MUTED),\n    )\n)\n\n# Interactive selection — click a legend symbol to highlight that ear, dim the other\nselection = alt.selection_point(fields=[\"ear\"], bind=\"legend\")\n\n# Shared color / shape encoding for ears\near_domain = [\"Right Ear (O)\", \"Left Ear (X)\"]\ncolor_scale_ears = alt.Scale(domain=ear_domain, range=[RIGHT_COLOR, LEFT_COLOR])\nshape_scale_ears = alt.Scale(domain=ear_domain, range=[\"circle\", \"cross\"])\n\nlegend_cfg = alt.Legend(\n    title=\"\",\n    orient=\"top-right\",\n    fillColor=ELEVATED_BG,\n    strokeColor=INK_SOFT,\n    labelColor=INK_SOFT,\n    labelFontSize=11,\n    symbolSize=160,\n    symbolStrokeWidth=2.5,\n)\n\n# Connecting lines per ear — opacity driven by selection state\nlines_layer = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"frequency:Q\", scale=x_scale, axis=x_axis),\n        y=alt.Y(\"threshold:Q\", scale=y_scale, axis=y_axis),\n        color=alt.Color(\"ear:N\", scale=color_scale_ears, legend=None),\n        detail=\"ear:N\",\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.2)),\n    )\n)\n\n# Markers: circle (O) for right ear, cross (X) for left ear\npoints_layer = (\n    alt.Chart(df)\n    .mark_point(size=200, strokeWidth=2.5, filled=False)\n    .encode(\n        x=alt.X(\"frequency:Q\", scale=x_scale),\n        y=alt.Y(\"threshold:Q\", scale=y_scale),\n        color=alt.Color(\"ear:N\", scale=color_scale_ears, legend=legend_cfg),\n        shape=alt.Shape(\"ear:N\", scale=shape_scale_ears, legend=None),\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.2)),\n        tooltip=[\n            alt.Tooltip(\"frequency:Q\", title=\"Frequency (Hz)\"),\n            alt.Tooltip(\"threshold:Q\", title=\"Threshold (dB HL)\"),\n            alt.Tooltip(\"ear:N\", title=\"Ear\"),\n        ],\n    )\n    .add_params(selection)\n)\n\ntitle_text = \"audiogram-clinical · python · altair · anyplot.ai\"\n\n# Compose layered chart\nchart = (\n    alt.layer(bands, band_labels, lines_layer, points_layer)\n    .resolve_scale(color=\"independent\")\n    .properties(\n        width=500,\n        height=460,\n        background=PAGE_BG,\n        title=alt.Title(title_text, fontSize=16, color=INK, fontWeight=\"bold\"),\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=500, continuousHeight=460)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK, fontSize=16, fontWeight=\"bold\")\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=11,\n        titleFontSize=10,\n    )\n    .interactive()\n)\n\n# Save PNG and HTML\nTW, TH = 2400, 2400\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to exact canvas target (never crop — would trigger AR-09 edge-clip reject)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _bg_hex = PAGE_BG.lstrip(\"#\")\n    _bg_rgb = tuple(int(_bg_hex[i : i + 2], 16) for i in (0, 2, 4))\n    _canvas = Image.new(\"RGB\", (TW, TH), _bg_rgb)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}