{"spec_id":"scatter-hr-diagram","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-hr-diagram: Hertzsprung-Russell Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\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\nnp.random.seed(42)\n\n# Main sequence stars (diagonal band from hot/bright to cool/dim)\nn_main = 250\nmain_temp = 10 ** np.random.uniform(np.log10(2500), np.log10(35000), n_main)\nmain_log_lum = (np.log10(main_temp) - np.log10(5778)) * 5.5 + np.random.normal(0, 0.3, n_main)\nmain_lum = 10**main_log_lum\n\n# Red giants (cool but bright)\nn_giants = 50\ngiant_temp = 10 ** np.random.uniform(np.log10(3200), np.log10(5500), n_giants)\ngiant_lum = 10 ** np.random.uniform(1.2, 3.0, n_giants)\n\n# Supergiants (very bright, wide temperature range)\nn_super = 20\nsuper_temp = 10 ** np.random.uniform(np.log10(3500), np.log10(30000), n_super)\nsuper_lum = 10 ** np.random.uniform(3.5, 5.5, n_super)\n\n# White dwarfs (hot but very dim)\nn_wd = 40\nwd_temp = 10 ** np.random.uniform(np.log10(5000), np.log10(30000), n_wd)\nwd_lum = 10 ** np.random.uniform(-4, -1.5, n_wd)\n\ntemperatures = np.concatenate([main_temp, giant_temp, super_temp, wd_temp])\nluminosities = np.concatenate([main_lum, giant_lum, super_lum, wd_lum])\nregions = [\"Main Sequence\"] * n_main + [\"Red Giants\"] * n_giants + [\"Supergiants\"] * n_super + [\"White Dwarfs\"] * n_wd\n\nspectral_types = np.select(\n    [\n        temperatures >= 30000,\n        temperatures >= 10000,\n        temperatures >= 7500,\n        temperatures >= 6000,\n        temperatures >= 5200,\n        temperatures >= 3700,\n    ],\n    [\"O\", \"B\", \"A\", \"F\", \"G\", \"K\"],\n    default=\"M\",\n)\n\ndf = pd.DataFrame(\n    {\n        \"Temperature (K)\": temperatures,\n        \"Luminosity (Solar)\": luminosities,\n        \"Region\": regions,\n        \"Spectral Type\": spectral_types,\n    }\n)\n\n# Sun as a reference point\nsun = pd.DataFrame({\"Temperature (K)\": [5778], \"Luminosity (Solar)\": [1.0], \"label\": [\"Sun ☉\"]})\n\n# Region labels placed in clear areas away from dense data\nregion_labels = pd.DataFrame(\n    {\n        \"Temperature (K)\": [8000, 3200, 9000, 25000],\n        \"Luminosity (Solar)\": [0.015, 800, 200000, 0.0008],\n        \"text\": [\"Main Sequence\", \"Red Giants\", \"Supergiants\", \"White Dwarfs\"],\n    }\n)\n\n# Interactive selection: click legend to highlight spectral type\nselection = alt.selection_point(fields=[\"Spectral Type\"], bind=\"legend\")\n\n# Spectral colors mapped to nearest Imprint palette members while preserving\n# hot-blue-to-cool-red astrophysical temperature sequence.\nSPECTRAL_DOMAIN = [\"O\", \"B\", \"A\", \"F\", \"G\", \"K\", \"M\"]\nSPECTRAL_RANGE = [\"#4467A3\", \"#2ABCCD\", \"#C475FD\", \"#DDCC77\", \"#99B314\", \"#BD8233\", \"#AE3030\"]\n# Redundant shape encoding ensures CVD accessibility (deuteranopia/protanopia safe).\nSPECTRAL_SHAPES = [\"circle\", \"diamond\", \"square\", \"triangle-up\", \"triangle-down\", \"cross\", \"triangle-left\"]\n\nstars = (\n    alt.Chart(df)\n    .mark_point(strokeWidth=0, filled=True)\n    .encode(\n        x=alt.X(\n            \"Temperature (K):Q\",\n            scale=alt.Scale(type=\"log\", domain=[50000, 2000]),\n            axis=alt.Axis(\n                title=\"Surface Temperature (K)\", values=[2000, 3000, 5000, 7000, 10000, 20000, 40000], format=\"~s\"\n            ),\n        ),\n        y=alt.Y(\n            \"Luminosity (Solar):Q\",\n            scale=alt.Scale(type=\"log\", domain=[0.00005, 2000000]),\n            axis=alt.Axis(title=\"Luminosity (L/L☉)\", format=\".0e\"),\n        ),\n        color=alt.Color(\n            \"Spectral Type:N\",\n            scale=alt.Scale(domain=SPECTRAL_DOMAIN, range=SPECTRAL_RANGE),\n            sort=SPECTRAL_DOMAIN,\n            legend=alt.Legend(title=\"Spectral Type\", symbolSize=150, orient=\"right\"),\n        ),\n        shape=alt.Shape(\n            \"Spectral Type:N\",\n            scale=alt.Scale(domain=SPECTRAL_DOMAIN, range=SPECTRAL_SHAPES),\n            sort=SPECTRAL_DOMAIN,\n            legend=alt.Legend(title=\"Spectral Type\", symbolSize=150, orient=\"right\"),\n        ),\n        size=alt.value(60),\n        opacity=alt.condition(selection, alt.value(0.75), alt.value(0.08)),\n        tooltip=[\"Temperature (K):Q\", \"Luminosity (Solar):Q\", \"Spectral Type:N\", \"Region:N\"],\n    )\n    .add_params(selection)\n)\n\nsun_point = (\n    alt.Chart(sun)\n    .mark_point(shape=\"cross\", size=400, color=\"#FFD700\", strokeWidth=3, filled=True)\n    .encode(x=\"Temperature (K):Q\", y=\"Luminosity (Solar):Q\", tooltip=alt.value(\"Sun (G2V, 5778 K, 1.0 L☉)\"))\n)\n\nSUN_LABEL_COLOR = \"#b07c00\" if THEME == \"light\" else \"#FFD700\"\nsun_label = (\n    alt.Chart(sun)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=SUN_LABEL_COLOR, dx=22, dy=-14)\n    .encode(x=\"Temperature (K):Q\", y=\"Luminosity (Solar):Q\", text=\"label:N\")\n)\n\nlabels = (\n    alt.Chart(region_labels)\n    .mark_text(fontSize=13, fontStyle=\"italic\", color=INK_MUTED, fontWeight=\"bold\")\n    .encode(x=\"Temperature (K):Q\", y=\"Luminosity (Solar):Q\", text=\"text:N\")\n)\n\nTITLE = \"scatter-hr-diagram · python · altair · anyplot.ai\"\n\nchart = (\n    (stars + sun_point + sun_label + labels)\n    .properties(width=620, height=320, title=alt.Title(TITLE, fontSize=16, anchor=\"start\"), background=PAGE_BG)\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        tickSize=0,\n        gridColor=INK,\n        gridOpacity=0.12,\n        labelColor=INK_SOFT,\n        labelFontSize=10,\n        titleColor=INK,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 target (vl-convert output is smaller than the inner view * scale_factor)\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"}