{"spec_id":"star-chart-constellation","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nstar-chart-constellation: Star Chart with Constellations\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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\"\nGRID = INK  # used with low alpha for the RA/Dec coordinate grid\n\n# Imprint palette position 1 — brand green, the single primary star series.\n# A constant green reads on both the cream and near-black surfaces, unlike the\n# pale-yellow of a literal night sky which would vanish on the light theme.\nBRAND = \"#009E73\"\n\n# Data — notable stars with real approximate coordinates (RA in hours, Dec in degrees)\nnp.random.seed(42)\n\nstars_data = [\n    # Orion\n    (\"Betelgeuse\", 5.92, 7.41, 0.42, \"Ori\"),\n    (\"Rigel\", 5.24, -8.20, 0.13, \"Ori\"),\n    (\"Bellatrix\", 5.42, 6.35, 1.64, \"Ori\"),\n    (\"Mintaka\", 5.53, -0.30, 2.23, \"Ori\"),\n    (\"Alnilam\", 5.60, -1.20, 1.69, \"Ori\"),\n    (\"Alnitak\", 5.68, -1.94, 1.77, \"Ori\"),\n    (\"Saiph\", 5.80, -9.67, 2.09, \"Ori\"),\n    # Ursa Major (Big Dipper)\n    (\"Dubhe\", 11.06, 61.75, 1.79, \"UMa\"),\n    (\"Merak\", 11.03, 56.38, 2.37, \"UMa\"),\n    (\"Phecda\", 11.90, 53.69, 2.44, \"UMa\"),\n    (\"Megrez\", 12.26, 57.03, 3.31, \"UMa\"),\n    (\"Alioth\", 12.90, 55.96, 1.77, \"UMa\"),\n    (\"Mizar\", 13.40, 54.93, 2.27, \"UMa\"),\n    (\"Alkaid\", 13.79, 49.31, 1.86, \"UMa\"),\n    # Cassiopeia\n    (\"Schedar\", 0.68, 56.54, 2.23, \"Cas\"),\n    (\"Caph\", 0.15, 59.15, 2.27, \"Cas\"),\n    (\"Gamma Cas\", 0.95, 60.72, 2.47, \"Cas\"),\n    (\"Ruchbah\", 1.43, 60.24, 2.68, \"Cas\"),\n    (\"Segin\", 1.91, 63.67, 3.37, \"Cas\"),\n    # Leo\n    (\"Regulus\", 10.14, 11.97, 1.35, \"Leo\"),\n    (\"Denebola\", 11.82, 14.57, 2.14, \"Leo\"),\n    (\"Algieba\", 10.33, 19.84, 2.28, \"Leo\"),\n    (\"Zosma\", 11.24, 20.52, 2.56, \"Leo\"),\n    (\"Chertan\", 11.24, 15.43, 3.33, \"Leo\"),\n    # Cygnus\n    (\"Deneb\", 20.69, 45.28, 1.25, \"Cyg\"),\n    (\"Sadr\", 20.37, 40.26, 2.23, \"Cyg\"),\n    (\"Gienah Cyg\", 20.77, 33.97, 2.48, \"Cyg\"),\n    (\"Albireo\", 19.51, 27.96, 3.08, \"Cyg\"),\n    (\"Fawaris\", 19.75, 45.13, 2.87, \"Cyg\"),\n    # Scorpius\n    (\"Antares\", 16.49, -26.43, 1.09, \"Sco\"),\n    (\"Shaula\", 17.56, -37.10, 1.63, \"Sco\"),\n    (\"Sargas\", 17.62, -42.99, 1.87, \"Sco\"),\n    (\"Dschubba\", 16.01, -22.62, 2.32, \"Sco\"),\n    (\"Graffias\", 16.09, -19.81, 2.62, \"Sco\"),\n    (\"Lesath\", 17.53, -37.29, 2.69, \"Sco\"),\n    # Gemini\n    (\"Pollux\", 7.76, 28.03, 1.14, \"Gem\"),\n    (\"Castor\", 7.58, 31.89, 1.58, \"Gem\"),\n    (\"Alhena\", 6.63, 16.40, 1.93, \"Gem\"),\n    (\"Tejat\", 6.38, 22.51, 2.88, \"Gem\"),\n    (\"Mebsuta\", 6.73, 25.13, 3.06, \"Gem\"),\n    # Lyra\n    (\"Vega\", 18.62, 38.78, 0.03, \"Lyr\"),\n    (\"Sheliak\", 18.83, 33.36, 3.45, \"Lyr\"),\n    (\"Sulafat\", 18.98, 32.69, 3.24, \"Lyr\"),\n    # Aquila\n    (\"Altair\", 19.85, 8.87, 0.77, \"Aql\"),\n    (\"Tarazed\", 19.77, 10.61, 2.72, \"Aql\"),\n    (\"Alshain\", 19.92, 6.41, 3.71, \"Aql\"),\n    # Canis Major\n    (\"Sirius\", 6.75, -16.72, -1.46, \"CMa\"),\n    (\"Adhara\", 6.98, -28.97, 1.50, \"CMa\"),\n    (\"Wezen\", 7.14, -26.39, 1.84, \"CMa\"),\n    (\"Mirzam\", 6.38, -17.96, 1.98, \"CMa\"),\n    (\"Aludra\", 7.40, -29.30, 2.45, \"CMa\"),\n    # Taurus\n    (\"Aldebaran\", 4.60, 16.51, 0.85, \"Tau\"),\n    (\"Elnath\", 5.44, 28.61, 1.65, \"Tau\"),\n    (\"Alcyone\", 3.79, 24.11, 2.87, \"Tau\"),\n    (\"Tianguan\", 5.63, 21.14, 3.00, \"Tau\"),\n    # Bootes\n    (\"Arcturus\", 14.26, 19.18, -0.05, \"Boo\"),\n    (\"Izar\", 14.75, 27.07, 2.37, \"Boo\"),\n    (\"Muphrid\", 13.91, 18.40, 2.68, \"Boo\"),\n    # Perseus\n    (\"Mirfak\", 3.41, 49.86, 1.79, \"Per\"),\n    (\"Algol\", 3.14, 40.96, 2.12, \"Per\"),\n    (\"Atik\", 3.96, 31.88, 2.85, \"Per\"),\n]\n\n# Fainter background stars to fill out the sky (mag <= 5.0 threshold per spec)\nn_bg = 150\nbg_ra = np.random.uniform(0, 24, n_bg)\nbg_dec = np.random.uniform(-45, 70, n_bg)\nbg_mag = np.random.uniform(3.5, 5.0, n_bg)\n\nstar_id = [s[0] for s in stars_data] + [f\"BG{i}\" for i in range(n_bg)]\nra_hours = np.array([s[1] for s in stars_data] + list(bg_ra))\ndec = np.array([s[2] for s in stars_data] + list(bg_dec))\nmagnitude = np.array([s[3] for s in stars_data] + list(bg_mag))\nconstellation = [s[4] for s in stars_data] + [\"\" for _ in range(n_bg)]\n\n# Azimuthal equidistant projection centred on the North Celestial Pole:\n# angular distance from the pole becomes the radius, RA becomes the bearing.\n# RA meridians map to straight radials, Dec parallels to concentric circles —\n# the natural circular sky boundary the spec asks for.\nrho = 90.0 - dec\nphi = np.radians(ra_hours * 15.0)\nx = rho * np.sin(phi)\ny = rho * np.cos(phi)\n\n# Invert magnitude for point sizing: brighter (lower magnitude) = larger\nmax_mag = 5.5\nsize = (max_mag - magnitude + 0.6) * 1.5\n\ndf = pd.DataFrame(\n    {\n        \"star_id\": star_id,\n        \"x\": x,\n        \"y\": y,\n        \"ra_h\": ra_hours,\n        \"dec\": dec,\n        \"magnitude\": magnitude,\n        \"constellation\": constellation,\n        \"size\": size,\n    }\n)\n\nnamed = df[df[\"constellation\"] != \"\"]\nbackground = df[df[\"constellation\"] == \"\"]\nbright_1 = named[named[\"magnitude\"] < 0.6]\nbright_2 = named[named[\"magnitude\"] < 1.2]\n\n# Constellation stick-figure edges (pairs of star names)\nedges = [\n    (\"Betelgeuse\", \"Bellatrix\"),\n    (\"Bellatrix\", \"Mintaka\"),\n    (\"Mintaka\", \"Alnilam\"),\n    (\"Alnilam\", \"Alnitak\"),\n    (\"Betelgeuse\", \"Alnitak\"),\n    (\"Bellatrix\", \"Rigel\"),\n    (\"Betelgeuse\", \"Saiph\"),\n    (\"Rigel\", \"Saiph\"),\n    (\"Dubhe\", \"Merak\"),\n    (\"Merak\", \"Phecda\"),\n    (\"Phecda\", \"Megrez\"),\n    (\"Megrez\", \"Alioth\"),\n    (\"Alioth\", \"Mizar\"),\n    (\"Mizar\", \"Alkaid\"),\n    (\"Megrez\", \"Dubhe\"),\n    (\"Caph\", \"Schedar\"),\n    (\"Schedar\", \"Gamma Cas\"),\n    (\"Gamma Cas\", \"Ruchbah\"),\n    (\"Ruchbah\", \"Segin\"),\n    (\"Regulus\", \"Chertan\"),\n    (\"Chertan\", \"Zosma\"),\n    (\"Zosma\", \"Denebola\"),\n    (\"Regulus\", \"Algieba\"),\n    (\"Algieba\", \"Zosma\"),\n    (\"Deneb\", \"Sadr\"),\n    (\"Sadr\", \"Albireo\"),\n    (\"Sadr\", \"Gienah Cyg\"),\n    (\"Sadr\", \"Fawaris\"),\n    (\"Graffias\", \"Dschubba\"),\n    (\"Dschubba\", \"Antares\"),\n    (\"Antares\", \"Shaula\"),\n    (\"Shaula\", \"Lesath\"),\n    (\"Shaula\", \"Sargas\"),\n    (\"Castor\", \"Pollux\"),\n    (\"Castor\", \"Tejat\"),\n    (\"Pollux\", \"Alhena\"),\n    (\"Tejat\", \"Mebsuta\"),\n    (\"Mebsuta\", \"Castor\"),\n    (\"Vega\", \"Sheliak\"),\n    (\"Sheliak\", \"Sulafat\"),\n    (\"Sulafat\", \"Vega\"),\n    (\"Altair\", \"Tarazed\"),\n    (\"Altair\", \"Alshain\"),\n    (\"Sirius\", \"Mirzam\"),\n    (\"Sirius\", \"Adhara\"),\n    (\"Adhara\", \"Wezen\"),\n    (\"Wezen\", \"Aludra\"),\n    (\"Aldebaran\", \"Elnath\"),\n    (\"Aldebaran\", \"Alcyone\"),\n    (\"Elnath\", \"Tianguan\"),\n    (\"Arcturus\", \"Izar\"),\n    (\"Arcturus\", \"Muphrid\"),\n    (\"Mirfak\", \"Algol\"),\n    (\"Algol\", \"Atik\"),\n]\n\npos = {row.star_id: (row.x, row.y) for row in df.itertuples()}\ndf_edges = pd.DataFrame(\n    [{\"x\": pos[a][0], \"y\": pos[a][1], \"xend\": pos[b][0], \"yend\": pos[b][1]} for a, b in edges if a in pos and b in pos]\n)\n\n# RA/Dec coordinate grid — concentric Dec circles + radial RA meridians\nangle = np.linspace(0, 2 * np.pi, 240)\ncircle_rows = []\nfor dec_ring in (60, 30, 0, -30):\n    r = 90.0 - dec_ring\n    for a in angle:\n        circle_rows.append({\"x\": r * np.sin(a), \"y\": r * np.cos(a), \"ring\": f\"d{dec_ring}\"})\ndf_circles = pd.DataFrame(circle_rows)\n\nr_bound = 138.0\ndf_boundary = pd.DataFrame({\"x\": r_bound * np.sin(angle), \"y\": r_bound * np.cos(angle)})\n\nmerid_rows = []\nfor h in range(0, 24, 2):\n    p = np.radians(h * 15.0)\n    merid_rows.append(\n        {\"x\": 22 * np.sin(p), \"y\": 22 * np.cos(p), \"xend\": r_bound * np.sin(p), \"yend\": r_bound * np.cos(p)}\n    )\ndf_merid = pd.DataFrame(merid_rows)\n\n# Grid tick labels: Dec rings along the RA=0 meridian, RA hours around the rim\ndf_declab = pd.DataFrame(\n    {\"x\": [7] * 4, \"y\": [90.0 - d for d in (60, 30, 0, -30)], \"label\": [f\"{d}°\" for d in (60, 30, 0, -30)]}\n)\nra_label_rows = []\nfor h in range(0, 24, 2):\n    p = np.radians(h * 15.0)\n    ra_label_rows.append({\"x\": 149 * np.sin(p), \"y\": 149 * np.cos(p), \"label\": f\"{h}h\"})\ndf_ralab = pd.DataFrame(ra_label_rows)\n\n# Constellation labels — projected centroid pushed radially outward to clear stars\ncentroids = named.groupby(\"constellation\")[[\"x\", \"y\"]].mean().reset_index()\nnorm = np.hypot(centroids[\"x\"], centroids[\"y\"]).replace(0, 1)\ncentroids[\"x\"] = centroids[\"x\"] + centroids[\"x\"] / norm * 15.0\ncentroids[\"y\"] = centroids[\"y\"] + centroids[\"y\"] / norm * 15.0\nconst_full_names = {\n    \"Ori\": \"Orion\",\n    \"UMa\": \"Ursa Major\",\n    \"Cas\": \"Cassiopeia\",\n    \"Leo\": \"Leo\",\n    \"Cyg\": \"Cygnus\",\n    \"Sco\": \"Scorpius\",\n    \"Gem\": \"Gemini\",\n    \"Lyr\": \"Lyra\",\n    \"Aql\": \"Aquila\",\n    \"CMa\": \"Canis Major\",\n    \"Tau\": \"Taurus\",\n    \"Boo\": \"Bootes\",\n    \"Per\": \"Perseus\",\n}\ncentroids[\"name\"] = centroids[\"constellation\"].map(const_full_names)\n\n# Magnitude legend — compact panel tucked into the empty top-left corner (outside the sky disc)\nlegend_mags = [0, 1, 2, 3, 4, 5]\nlegend_x = -128\ndf_legend = pd.DataFrame(\n    {\n        \"x\": [legend_x] * len(legend_mags),\n        \"y\": [92 + i * 9 for i in range(len(legend_mags))],\n        \"size\": [(max_mag - m + 0.6) * 1.5 for m in legend_mags],\n        \"label\": [f\"mag {m}\" for m in legend_mags],\n    }\n)\ndf_legend_title = pd.DataFrame({\"x\": [legend_x], \"y\": [92 + len(legend_mags) * 9]})\ndf_legend_bg = pd.DataFrame({\"xmin\": [-145], \"xmax\": [-97], \"ymin\": [85], \"ymax\": [152]})\n\n# Plot\nplot = (\n    ggplot()\n    # Coordinate grid\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"ring\"), data=df_circles, color=GRID, size=0.4, alpha=0.13)\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_merid, color=GRID, size=0.4, alpha=0.13)\n    + geom_path(aes(x=\"x\", y=\"y\"), data=df_boundary, color=GRID, size=0.8, alpha=0.30)\n    # Constellation stick-figure lines\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_edges, color=BRAND, size=0.7, alpha=0.45)\n    # Faint background stars\n    + geom_point(aes(x=\"x\", y=\"y\", size=\"size\"), data=background, color=INK_MUTED, alpha=0.55, shape=16)\n    # Glow halo beneath the brightest stars\n    + geom_point(aes(x=\"x\", y=\"y\"), data=bright_2, color=BRAND, alpha=0.07, size=13, shape=16)\n    + geom_point(aes(x=\"x\", y=\"y\"), data=bright_1, color=BRAND, alpha=0.10, size=19, shape=16)\n    # Named constellation stars (with interactive tooltips — a lets-plot feature)\n    + geom_point(\n        aes(x=\"x\", y=\"y\", size=\"size\"),\n        data=named,\n        color=BRAND,\n        alpha=0.95,\n        shape=16,\n        tooltips=layer_tooltips()\n        .title(\"@star_id\")\n        .line(\"Constellation|@constellation\")\n        .line(\"Magnitude|@magnitude\")\n        .line(\"RA|@{ra_h}h\")\n        .line(\"Dec|@{dec}°\")\n        .format(\"@magnitude\", \".2f\")\n        .format(\"@ra_h\", \".2f\")\n        .format(\"@dec\", \".1f\"),\n    )\n    + scale_size_identity()\n    # Constellation name labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"name\"), data=centroids, color=INK_SOFT, size=7, fontface=\"italic\")\n    # Grid tick labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_declab, color=INK_MUTED, size=5.5)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_ralab, color=INK_MUTED, size=5.5)\n    # Magnitude legend panel\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_legend_bg,\n        fill=ELEVATED_BG,\n        color=INK_SOFT,\n        alpha=0.9,\n        size=0.4,\n    )\n    + geom_point(aes(x=\"x\", y=\"y\", size=\"size\"), data=df_legend, color=BRAND, alpha=0.95, shape=16)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_legend, color=INK_SOFT, size=5, nudge_x=10, hjust=0)\n    + geom_text(aes(x=\"x\", y=\"y\"), data=df_legend_title, label=\"Magnitude\", color=INK, size=6, fontface=\"bold\")\n    + labs(\n        title=\"star-chart-constellation · python · letsplot · anyplot.ai\",\n        caption=\"Azimuthal equidistant projection · point size ∝ brightness (lower magnitude = brighter)\",\n    )\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=[-160, 160])\n    + scale_y_continuous(limits=[-160, 160])\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid=element_blank(),\n        axis_line=element_blank(),\n        axis_ticks=element_blank(),\n        axis_text=element_blank(),\n        axis_title=element_blank(),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_caption=element_text(size=11, color=INK_MUTED),\n        legend_position=\"none\",\n    )\n    + ggsize(600, 600)\n)\n\n# Save (square → 2400 × 2400 px at scale 4)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}