{"spec_id":"spectrum-nmr","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nspectrum-nmr: NMR Spectrum (Nuclear Magnetic Resonance)\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive chrome (Imprint palette)\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# Imprint palette — first series is always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]\n\n# Synthetic 1H NMR spectrum of ethanol (CH3-CH2-OH)\nnp.random.seed(42)\nchemical_shift = np.linspace(-0.5, 5.0, 5000)\nw = 0.012  # Lorentzian half-width\n\nintensity = np.zeros_like(chemical_shift)\n\n# TMS reference peak at 0 ppm (singlet)\nintensity += 0.3 / (1 + ((chemical_shift - 0.0) / 0.015) ** 2)\n\n# CH3 triplet near 1.18 ppm (3 peaks, 1:2:1 ratio, J ~ 0.07 ppm)\nj_ch3 = 0.07\nintensity += 0.50 / (1 + ((chemical_shift - (1.18 - j_ch3)) / w) ** 2)\nintensity += 1.00 / (1 + ((chemical_shift - 1.18) / w) ** 2)\nintensity += 0.50 / (1 + ((chemical_shift - (1.18 + j_ch3)) / w) ** 2)\n\n# OH singlet near 2.61 ppm\nintensity += 0.35 / (1 + ((chemical_shift - 2.61) / 0.02) ** 2)\n\n# CH2 quartet near 3.69 ppm (4 peaks, 1:3:3:1 ratio, J ~ 0.07 ppm)\nj_ch2 = 0.07\nintensity += 0.25 / (1 + ((chemical_shift - (3.69 - 1.5 * j_ch2)) / w) ** 2)\nintensity += 0.75 / (1 + ((chemical_shift - (3.69 - 0.5 * j_ch2)) / w) ** 2)\nintensity += 0.75 / (1 + ((chemical_shift - (3.69 + 0.5 * j_ch2)) / w) ** 2)\nintensity += 0.25 / (1 + ((chemical_shift - (3.69 + 1.5 * j_ch2)) / w) ** 2)\n\nintensity += np.random.normal(0, 0.003, len(chemical_shift))\nintensity = np.clip(intensity, 0, None)\n\ndf = pd.DataFrame({\"Chemical Shift (ppm)\": chemical_shift, \"Intensity\": intensity})\n\nlabels_df = pd.DataFrame(\n    {\n        \"Chemical Shift (ppm)\": [0.0, 1.18, 2.61, 3.69],\n        \"Intensity\": [0.33, 1.08, 0.39, 0.80],\n        \"label\": [\"TMS\\n0.00 ppm\", \"CH₃ (triplet)\\n1.18 ppm\", \"OH (singlet)\\n2.61 ppm\", \"CH₂ (quartet)\\n3.69 ppm\"],\n    }\n)\n\nregions_data = pd.DataFrame(\n    {\n        \"x_start\": [-0.08, 1.02, 2.50, 3.52],\n        \"x_end\": [0.08, 1.34, 2.72, 3.86],\n        \"y_start\": [0.0, 0.0, 0.0, 0.0],\n        \"y_end\": [0.38, 1.12, 0.43, 0.84],\n    }\n)\n\ndroplines_df = pd.DataFrame(\n    {\"Chemical Shift (ppm)\": [0.0, 1.18, 2.61, 3.69], \"y_base\": [0.0, 0.0, 0.0, 0.0], \"y_top\": [0.30, 1.00, 0.35, 0.75]}\n)\n\n# NMR convention: high ppm on left (reversed x-axis)\nx_scale = alt.Scale(domain=[5.0, -0.5])\ny_scale = alt.Scale(domain=[0, 1.35])\n\n# Nearest-point selection for interactive highlight\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"Chemical Shift (ppm)\"], empty=False)\n\n# Subtle background region shading for peak groups — theme-adaptive via BRAND\nregion_shading = (\n    alt.Chart(regions_data)\n    .mark_rect(opacity=0.08, color=BRAND, cornerRadius=3)\n    .encode(x=alt.X(\"x_start:Q\", scale=x_scale), x2=\"x_end:Q\", y=alt.Y(\"y_start:Q\", scale=y_scale), y2=\"y_end:Q\")\n)\n\n# Dashed vertical drop-lines at peak centers\ndrop_rules = (\n    alt.Chart(droplines_df)\n    .mark_rule(color=BRAND, opacity=0.25, strokeDash=[4, 4], strokeWidth=1)\n    .encode(x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale), y=alt.Y(\"y_base:Q\", scale=y_scale), y2=\"y_top:Q\")\n)\n\n# Spectrum line — Imprint brand green (#009E73) as first (only) series\nspectrum = (\n    alt.Chart(df)\n    .mark_line(color=BRAND, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale, title=\"Chemical Shift (ppm)\"),\n        y=alt.Y(\"Intensity:Q\", title=\"Intensity (a.u.)\", scale=y_scale),\n        tooltip=[alt.Tooltip(\"Chemical Shift (ppm):Q\", format=\".2f\"), alt.Tooltip(\"Intensity:Q\", format=\".3f\")],\n    )\n)\n\n# Interactive crosshair rule on hover — theme-adaptive muted ink\ncrosshair = (\n    alt.Chart(df)\n    .mark_rule(color=INK_MUTED, strokeWidth=0.8, strokeDash=[3, 3])\n    .encode(x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale))\n    .transform_filter(nearest)\n)\n\n# Hover point indicator\nhover_point = (\n    alt.Chart(df)\n    .mark_circle(size=50, color=BRAND, opacity=0.8)\n    .encode(x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale), y=alt.Y(\"Intensity:Q\", scale=y_scale))\n    .transform_filter(nearest)\n)\n\n# Invisible voronoi layer for nearest-point selection\nselectors = (\n    alt.Chart(df)\n    .mark_point(size=1, opacity=0)\n    .encode(x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale))\n    .add_params(nearest)\n)\n\n# Peak annotation labels — theme-adaptive ink color\npeak_labels = (\n    alt.Chart(labels_df)\n    .mark_text(\n        fontSize=11, fontWeight=\"bold\", lineBreak=\"\\n\", align=\"center\", dy=-18, font=\"Helvetica Neue, Arial, sans-serif\"\n    )\n    .encode(\n        x=alt.X(\"Chemical Shift (ppm):Q\", scale=x_scale),\n        y=alt.Y(\"Intensity:Q\", scale=y_scale),\n        text=\"label:N\",\n        color=alt.value(INK),\n    )\n)\n\nchart = (\n    alt.layer(region_shading, drop_rules, spectrum, selectors, crosshair, hover_point, peak_labels)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"spectrum-nmr · altair · anyplot.ai\",\n            fontSize=16,\n            fontWeight=\"bold\",\n            anchor=\"middle\",\n            font=\"Helvetica Neue, Arial, sans-serif\",\n            color=INK,\n            subtitle=\"Ethanol ¹H NMR — Synthetic 300 MHz Spectrum\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=4,\n            subtitleFont=\"Helvetica Neue, Arial, sans-serif\",\n            subtitleFontStyle=\"italic\",\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleColor=INK,\n        titleFont=\"Helvetica Neue, Arial, sans-serif\",\n        titleFontWeight=\"normal\",\n        labelColor=INK_SOFT,\n        labelFont=\"Helvetica Neue, Arial, sans-serif\",\n        grid=False,\n        domainColor=INK_SOFT,\n        domainWidth=0.8,\n        tickColor=INK_SOFT,\n        tickSize=5,\n        tickWidth=0.8,\n    )\n    .configure_title(font=\"Helvetica Neue, Arial, sans-serif\", color=INK)\n    .interactive()\n)\n\n# Save PNG with vl-convert, then pad to exact 3200×1800 target\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\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    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\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"}