{"spec_id":"scatter-hr-diagram","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-hr-diagram: Hertzsprung-Russell Diagram\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# bokeh.py is the script name — remove its directory from sys.path so that\n# `import bokeh` resolves to the installed package, not this file itself.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path[:] = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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# Spectral type colors — closest Imprint palette members (blue=hot, red=cool)\n# O/B hot blue → cyan → lavender transition → ochre warm → matte red/rose cool\nSPECTRAL_COLORS = {\n    \"O\": \"#4467A3\",  # Imprint blue — hot blue-type stars\n    \"B\": \"#2ABCCD\",  # Imprint cyan — slightly cooler blue\n    \"A\": \"#C475FD\",  # Imprint lavender — blue-white transition\n    \"F\": \"#99B314\",  # Imprint lime — warm-ish transition\n    \"G\": \"#BD8233\",  # Imprint ochre — warm yellow, Sun-like\n    \"K\": \"#AE3030\",  # Imprint matte red — orange-red\n    \"M\": \"#954477\",  # Imprint rose — cool dark red\n}\n\n# Label colors for light theme — #2ABCCD and #C475FD need darkening on cream bg\nSPECTRAL_LABEL_COLORS = (\n    {\"O\": \"#2A3D6B\", \"B\": \"#1A7A88\", \"A\": \"#7A40AA\", \"F\": \"#667800\", \"G\": \"#8A5A1A\", \"K\": \"#8A2020\", \"M\": \"#6B2A50\"}\n    if THEME == \"light\"\n    else SPECTRAL_COLORS\n)\n\n# Distinct marker shapes per spectral type — redundant encoding for colorblind accessibility\nSPECTRAL_MARKERS = {\n    \"O\": \"circle\",\n    \"B\": \"diamond\",\n    \"A\": \"hex\",\n    \"F\": \"inverted_triangle\",\n    \"G\": \"circle\",\n    \"K\": \"triangle\",\n    \"M\": \"square\",\n}\n\n# Dark edge on light theme keeps near-white A/F stars visible on cream background\nMARKER_EDGE = INK_SOFT if THEME == \"light\" else \"#FFFFFF\"\nMARKER_EDGE_WIDTH = 1.5 if THEME == \"light\" else 0.5\n\n# Data\nnp.random.seed(42)\n\nspectral_temp_ranges = {\n    \"O\": (30000, 50000),\n    \"B\": (10000, 30000),\n    \"A\": (7500, 10000),\n    \"F\": (6000, 7500),\n    \"G\": (5200, 6000),\n    \"K\": (3700, 5200),\n    \"M\": (2400, 3700),\n}\n\ntemperatures = []\nluminosities = []\nspectral_types = []\nregions = []\n\n# Main sequence (~210 stars — L proportional to T^4 with observational scatter)\nfor spec_type, (t_min, t_max) in spectral_temp_ranges.items():\n    temps = np.random.uniform(t_min, t_max, 30)\n    for t in temps:\n        temperatures.append(t)\n        log_lum = 4.0 * np.log10(t / 5778) + np.random.normal(0, 0.3)\n        luminosities.append(10**log_lum)\n        spectral_types.append(spec_type)\n        regions.append(\"Main Sequence\")\n\n# Red giants (~40 stars)\nfor _ in range(40):\n    t = np.random.uniform(3000, 5500)\n    lum = 10 ** np.random.uniform(1.5, 3.5)\n    temperatures.append(t)\n    luminosities.append(lum)\n    spec = \"M\" if t < 3700 else (\"K\" if t < 5200 else \"G\")\n    spectral_types.append(spec)\n    regions.append(\"Red Giants\")\n\n# Supergiants (~20 stars)\nfor _ in range(20):\n    t = np.random.uniform(3500, 30000)\n    lum = 10 ** np.random.uniform(3.5, 5.5)\n    temperatures.append(t)\n    luminosities.append(lum)\n    if t > 10000:\n        spec = \"B\"\n    elif t > 7500:\n        spec = \"A\"\n    elif t > 6000:\n        spec = \"F\"\n    elif t > 5200:\n        spec = \"G\"\n    elif t > 3700:\n        spec = \"K\"\n    else:\n        spec = \"M\"\n    spectral_types.append(spec)\n    regions.append(\"Supergiants\")\n\n# White dwarfs (~30 stars)\nfor _ in range(30):\n    t = np.random.uniform(5000, 40000)\n    lum = 10 ** np.random.uniform(-4, -1.5)\n    temperatures.append(t)\n    luminosities.append(lum)\n    if t > 30000:\n        spec = \"O\"\n    elif t > 10000:\n        spec = \"B\"\n    elif t > 7500:\n        spec = \"A\"\n    elif t > 6000:\n        spec = \"F\"\n    else:\n        spec = \"G\"\n    spectral_types.append(spec)\n    regions.append(\"White Dwarfs\")\n\ntemperatures = np.array(temperatures)\nluminosities = np.array(luminosities)\nspectral_types = np.array(spectral_types)\nregions = np.array(regions)\n\n# Plot\ntitle_str = \"scatter-hr-diagram · python · bokeh · anyplot.ai\"\n\nhover = HoverTool(\n    tooltips=[\n        (\"Temperature\", \"@temperature{0,0} K\"),\n        (\"Luminosity\", \"@luminosity{0.00e+0} L☉\"),\n        (\"Spectral Type\", \"@spectral_type\"),\n        (\"Region\", \"@region\"),\n    ]\n)\n\np = figure(\n    width=3200,\n    height=1800,\n    title=title_str,\n    x_axis_type=\"log\",\n    y_axis_type=\"log\",\n    x_range=Range1d(55000, 2000),\n    y_range=Range1d(1e-4, 2e6),\n    x_axis_label=\"Surface Temperature (K)\",\n    y_axis_label=\"Luminosity (L☉)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=80,\n    tools=[hover],\n)\n\n# One scatter series per spectral type for individual legend entries\nlegend_items = []\nspectral_order = [\"O\", \"B\", \"A\", \"F\", \"G\", \"K\", \"M\"]\n\nfor spec_type in spectral_order:\n    mask = spectral_types == spec_type\n    if not np.any(mask):\n        continue\n    src = ColumnDataSource(\n        data={\n            \"temperature\": temperatures[mask].tolist(),\n            \"luminosity\": luminosities[mask].tolist(),\n            \"spectral_type\": spectral_types[mask].tolist(),\n            \"region\": regions[mask].tolist(),\n        }\n    )\n    # A and F types use larger markers — previously near-invisible on cream bg\n    marker_size = 18 if spec_type in (\"A\", \"F\") else 14\n    renderer = p.scatter(\n        x=\"temperature\",\n        y=\"luminosity\",\n        source=src,\n        size=marker_size,\n        marker=SPECTRAL_MARKERS[spec_type],\n        fill_color=SPECTRAL_COLORS[spec_type],\n        line_color=MARKER_EDGE,\n        line_width=MARKER_EDGE_WIDTH,\n        fill_alpha=0.85,\n    )\n    legend_items.append(LegendItem(label=f\"Type {spec_type}\", renderers=[renderer]))\n\n# Legend — 34pt text is readable at 3200×1800\nlegend = Legend(\n    items=legend_items,\n    label_text_color=INK_SOFT,\n    label_text_font_size=\"34pt\",\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    border_line_width=1,\n    click_policy=\"hide\",\n    title=\"Spectral Type\",\n    title_text_color=INK,\n    title_text_font_size=\"36pt\",\n    title_text_font_style=\"bold\",\n    spacing=8,\n    padding=15,\n)\np.add_layout(legend, \"right\")\n\n# Sun reference marker (G-type, T=5778 K, L=1.0 L☉)\nsun_src = ColumnDataSource(data={\"temperature\": [5778], \"luminosity\": [1.0]})\np.scatter(\n    x=\"temperature\",\n    y=\"luminosity\",\n    source=sun_src,\n    size=32,\n    fill_color=\"#DDCC77\",  # amber anchor — distinctive golden reference for the Sun\n    line_color=INK,\n    line_width=2,\n    marker=\"star\",\n    fill_alpha=1.0,\n)\np.add_layout(\n    Label(\n        x=5778,\n        y=1.0,\n        text=\"☀ Sun\",\n        x_offset=25,\n        y_offset=10,\n        text_font_size=\"28pt\",\n        text_font_style=\"bold\",\n        text_color=INK,\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.85,\n        x_units=\"data\",\n        y_units=\"data\",\n    )\n)\n\n# Region labels — Main Sequence repositioned above dense data cloud\nregion_labels = [\n    (\"Main Sequence\", 12000, 60, INK_MUTED),\n    (\"Red Giants\", 3600, 800, SPECTRAL_LABEL_COLORS[\"K\"]),\n    (\"Supergiants\", 6000, 150000, SPECTRAL_LABEL_COLORS[\"B\"]),\n    (\"White Dwarfs\", 15000, 0.001, SPECTRAL_LABEL_COLORS[\"B\"]),\n]\nfor label_text, lx, ly, lcolor in region_labels:\n    p.add_layout(\n        Label(\n            x=lx,\n            y=ly,\n            text=label_text,\n            text_font_size=\"28pt\",\n            text_font_style=\"italic\",\n            text_color=lcolor,\n            text_alpha=0.9,\n            x_units=\"data\",\n            y_units=\"data\",\n        )\n    )\n\n# Spectral class letters along the top\nspectral_boundaries = [(\"O\", 40000), (\"B\", 20000), (\"A\", 8750), (\"F\", 6750), (\"G\", 5600), (\"K\", 4450), (\"M\", 3050)]\nfor spec_label, spec_temp in spectral_boundaries:\n    p.add_layout(\n        Label(\n            x=spec_temp,\n            y=1.2e6,\n            text=spec_label,\n            text_font_size=\"28pt\",\n            text_font_style=\"bold\",\n            text_color=SPECTRAL_LABEL_COLORS[spec_label],\n            text_align=\"center\",\n            x_units=\"data\",\n            y_units=\"data\",\n        )\n    )\n\n# Style — theme-adaptive chrome\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None  # remove full box outline — L-shaped spine (left+bottom) via axis lines\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.ygrid.grid_line_alpha = 0.12\n\n# Save HTML then screenshot via headless Chrome\noutput_file(f\"plot-{THEME}.html\", title=\"Hertzsprung-Russell Diagram\")\nsave(p)\n\n# Use CDP setDeviceMetricsOverride — --window-size alone gives 1661 instead of 1800\n# in headless=new Chrome because the browser chrome eats part of the requested height.\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# Pin PNG to exact dims so the post-render gate passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}