A celestial map that plots stars on a sky projection (stereographic or azimuthal equidistant) with constellation stick-figure lines connecting notable stars. Star apparent magnitudes are represented by point size (brighter stars appear larger), and a coordinate grid overlays the chart for orientation. The dark background mimics a night sky, making this an intuitive tool for identifying constellations and planning observations.

""" anyplot.ai
star-chart-constellation: Star Chart with Constellations
Library: matplotlib 3.11.0 | Python 3.13.13
Quality: 88/100 | Updated: 2026-06-17
"""
import os
import matplotlib.patheffects as pe
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome")
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
BRAND = "#009E73" # Imprint palette position 1 — brightest stars
AMBER = "#DDCC77" # semantic anchor — ecliptic reference line
np.random.seed(42)
# Star catalog: name -> (RA hours, Dec degrees, magnitude, constellation abbrev)
stars = {
# Orion
"Betelgeuse": (5.92, 7.41, 0.42, "Ori"),
"Rigel": (5.24, -8.20, 0.13, "Ori"),
"Bellatrix": (5.42, 6.35, 1.64, "Ori"),
"Mintaka": (5.53, -0.30, 2.23, "Ori"),
"Alnilam": (5.60, -1.20, 1.69, "Ori"),
"Alnitak": (5.68, -1.94, 1.77, "Ori"),
"Saiph": (5.80, -9.67, 2.09, "Ori"),
# Ursa Major
"Dubhe": (11.06, 61.75, 1.79, "UMa"),
"Merak": (11.03, 56.38, 2.37, "UMa"),
"Phecda": (11.90, 53.69, 2.44, "UMa"),
"Megrez": (12.26, 57.03, 3.31, "UMa"),
"Alioth": (12.90, 55.96, 1.77, "UMa"),
"Mizar": (13.40, 54.93, 2.27, "UMa"),
"Alkaid": (13.79, 49.31, 1.86, "UMa"),
# Cassiopeia
"Schedar": (0.68, 56.54, 2.23, "Cas"),
"Caph": (0.15, 59.15, 2.27, "Cas"),
"Gamma Cas": (0.95, 60.72, 2.47, "Cas"),
"Ruchbah": (1.36, 60.24, 2.68, "Cas"),
"Segin": (1.91, 63.67, 3.37, "Cas"),
# Leo
"Regulus": (10.14, 11.97, 1.35, "Leo"),
"Denebola": (11.82, 14.57, 2.13, "Leo"),
"Algieba": (10.33, 19.84, 2.08, "Leo"),
"Zosma": (11.24, 20.52, 2.56, "Leo"),
"Chertan": (11.24, 15.43, 3.33, "Leo"),
"Eta Leo": (10.12, 16.76, 3.52, "Leo"),
# Scorpius
"Antares": (16.49, -26.43, 1.09, "Sco"),
"Shaula": (17.56, -37.10, 1.63, "Sco"),
"Sargas": (17.62, -42.99, 1.87, "Sco"),
"Dschubba": (16.01, -22.62, 2.32, "Sco"),
"Acrab": (16.09, -19.81, 2.62, "Sco"),
"Wei": (16.84, -34.29, 2.29, "Sco"),
"Lesath": (17.53, -37.29, 2.69, "Sco"),
# Cygnus
"Deneb": (20.69, 45.28, 1.25, "Cyg"),
"Sadr": (20.37, 40.26, 2.20, "Cyg"),
"Gienah Cyg": (20.77, 33.97, 2.46, "Cyg"),
"Delta Cyg": (19.75, 45.13, 2.87, "Cyg"),
"Albireo": (19.51, 27.96, 3.08, "Cyg"),
# Lyra
"Vega": (18.62, 38.78, 0.03, "Lyr"),
"Sheliak": (18.83, 33.36, 3.45, "Lyr"),
"Sulafat": (18.98, 32.69, 3.24, "Lyr"),
"Epsilon1 Lyr": (18.74, 39.67, 4.67, "Lyr"),
"Epsilon2 Lyr": (18.75, 39.61, 4.59, "Lyr"),
# Gemini
"Pollux": (7.76, 28.03, 1.14, "Gem"),
"Castor": (7.58, 31.89, 1.58, "Gem"),
"Alhena": (6.63, 16.40, 1.93, "Gem"),
"Wasat": (7.07, 21.98, 3.53, "Gem"),
"Mebsuta": (6.73, 25.13, 2.98, "Gem"),
"Tejat": (6.38, 22.51, 2.88, "Gem"),
# Taurus
"Aldebaran": (4.60, 16.51, 0.85, "Tau"),
"Elnath": (5.44, 28.61, 1.68, "Tau"),
"Alcyone": (3.79, 24.11, 2.87, "Tau"),
"Tianguan": (5.63, 21.14, 3.00, "Tau"),
"Prima Hyadum": (4.33, 15.63, 3.65, "Tau"),
"Ain": (4.48, 19.18, 3.54, "Tau"),
# Canis Major
"Sirius": (6.75, -16.72, -1.46, "CMa"),
"Adhara": (6.98, -28.97, 1.50, "CMa"),
"Wezen": (7.14, -26.39, 1.84, "CMa"),
"Mirzam": (6.38, -17.96, 1.98, "CMa"),
"Aludra": (7.40, -29.30, 2.45, "CMa"),
"Furud": (6.34, -30.06, 3.02, "CMa"),
# Aquila
"Altair": (19.85, 8.87, 0.76, "Aql"),
"Tarazed": (19.77, 10.61, 2.72, "Aql"),
"Alshain": (19.92, 6.41, 3.71, "Aql"),
"Theta Aql": (20.19, -0.82, 3.23, "Aql"),
"Delta Aql": (19.42, 3.11, 3.36, "Aql"),
# Bootes
"Arcturus": (14.26, 19.18, -0.05, "Boo"),
"Izar": (14.75, 27.07, 2.37, "Boo"),
"Muphrid": (13.91, 18.40, 2.68, "Boo"),
"Nekkar": (15.03, 40.39, 3.58, "Boo"),
"Seginus": (14.53, 38.31, 3.03, "Boo"),
# Perseus
"Mirfak": (3.41, 49.86, 1.80, "Per"),
"Algol": (3.14, 40.96, 2.12, "Per"),
"Zeta Per": (3.90, 31.88, 2.85, "Per"),
"Epsilon Per": (3.96, 40.01, 2.89, "Per"),
"Delta Per": (3.72, 47.79, 3.01, "Per"),
# Auriga
"Capella": (5.27, 45.99, 0.08, "Aur"),
"Menkalinan": (5.99, 44.95, 1.90, "Aur"),
"Theta Aur": (5.99, 37.21, 2.62, "Aur"),
"Hassaleh": (4.95, 33.17, 2.69, "Aur"),
"Almaaz": (5.03, 43.82, 2.99, "Aur"),
# Virgo
"Spica": (13.42, -11.16, 0.97, "Vir"),
"Porrima": (12.69, -1.45, 2.74, "Vir"),
"Vindemiatrix": (13.04, 10.96, 2.83, "Vir"),
"Heze": (13.58, -0.60, 3.37, "Vir"),
"Zaniah": (12.33, -0.67, 3.89, "Vir"),
# Corona Borealis
"Alphecca": (15.58, 26.71, 2.23, "CrB"),
"Nusakan": (15.46, 29.11, 3.68, "CrB"),
"Gamma CrB": (15.71, 26.30, 3.84, "CrB"),
"Delta CrB": (15.83, 26.07, 4.59, "CrB"),
# Pegasus
"Enif": (21.74, 9.88, 2.39, "Peg"),
"Markab": (23.08, 15.21, 2.49, "Peg"),
"Scheat": (23.06, 28.08, 2.42, "Peg"),
"Algenib": (0.22, 15.18, 2.83, "Peg"),
"Matar": (22.72, 30.22, 2.94, "Peg"),
# Andromeda
"Alpheratz": (0.14, 29.09, 2.06, "And"),
"Mirach": (1.16, 35.62, 2.05, "And"),
"Almach": (2.07, 42.33, 2.17, "And"),
# Draco
"Eltanin": (17.94, 51.49, 2.24, "Dra"),
"Rastaban": (17.51, 52.30, 2.79, "Dra"),
"Thuban": (14.07, 64.38, 3.65, "Dra"),
"Grumium": (17.89, 56.87, 3.75, "Dra"),
"Kuma": (17.53, 55.17, 4.87, "Dra"),
# Hercules
"Kornephoros": (16.50, 21.49, 2.77, "Her"),
"Zeta Her": (16.69, 31.60, 2.81, "Her"),
"Rasalgethi": (17.24, 14.39, 3.37, "Her"),
"Sarin": (17.25, 24.84, 3.14, "Her"),
"Pi Her": (17.25, 36.81, 3.16, "Her"),
"Eta Her": (16.71, 38.92, 3.53, "Her"),
# Sagittarius
"Kaus Australis": (18.40, -34.38, 1.85, "Sgr"),
"Nunki": (18.92, -26.30, 2.02, "Sgr"),
"Ascella": (19.04, -29.88, 2.59, "Sgr"),
"Kaus Media": (18.35, -29.83, 2.70, "Sgr"),
"Kaus Borealis": (18.47, -25.42, 2.81, "Sgr"),
"Nash": (18.10, -30.42, 3.11, "Sgr"),
}
# Constellation stick-figure edges
edges = [
# Orion
("Betelgeuse", "Bellatrix"),
("Betelgeuse", "Mintaka"),
("Bellatrix", "Mintaka"),
("Mintaka", "Alnilam"),
("Alnilam", "Alnitak"),
("Rigel", "Alnitak"),
("Rigel", "Saiph"),
("Saiph", "Alnitak"),
# Ursa Major (Big Dipper)
("Dubhe", "Merak"),
("Merak", "Phecda"),
("Phecda", "Megrez"),
("Megrez", "Alioth"),
("Alioth", "Mizar"),
("Mizar", "Alkaid"),
("Megrez", "Dubhe"),
# Cassiopeia
("Caph", "Schedar"),
("Schedar", "Gamma Cas"),
("Gamma Cas", "Ruchbah"),
("Ruchbah", "Segin"),
# Leo
("Regulus", "Eta Leo"),
("Eta Leo", "Algieba"),
("Algieba", "Zosma"),
("Zosma", "Denebola"),
("Regulus", "Chertan"),
("Chertan", "Denebola"),
# Scorpius
("Acrab", "Dschubba"),
("Dschubba", "Antares"),
("Antares", "Wei"),
("Wei", "Shaula"),
("Shaula", "Lesath"),
("Wei", "Sargas"),
# Cygnus (Northern Cross)
("Deneb", "Sadr"),
("Sadr", "Albireo"),
("Sadr", "Gienah Cyg"),
("Sadr", "Delta Cyg"),
# Lyra
("Vega", "Sheliak"),
("Vega", "Sulafat"),
("Sheliak", "Sulafat"),
# Gemini
("Castor", "Pollux"),
("Castor", "Mebsuta"),
("Mebsuta", "Tejat"),
("Pollux", "Wasat"),
("Wasat", "Alhena"),
# Taurus
("Aldebaran", "Ain"),
("Ain", "Prima Hyadum"),
("Aldebaran", "Elnath"),
("Ain", "Alcyone"),
("Elnath", "Tianguan"),
# Canis Major
("Sirius", "Mirzam"),
("Sirius", "Adhara"),
("Adhara", "Wezen"),
("Wezen", "Aludra"),
("Adhara", "Furud"),
# Aquila
("Altair", "Tarazed"),
("Altair", "Alshain"),
("Altair", "Theta Aql"),
("Altair", "Delta Aql"),
# Bootes
("Arcturus", "Muphrid"),
("Arcturus", "Izar"),
("Izar", "Seginus"),
("Seginus", "Nekkar"),
# Perseus
("Mirfak", "Delta Per"),
("Mirfak", "Epsilon Per"),
("Epsilon Per", "Algol"),
("Algol", "Zeta Per"),
# Auriga
("Capella", "Menkalinan"),
("Menkalinan", "Theta Aur"),
("Theta Aur", "Hassaleh"),
("Hassaleh", "Almaaz"),
("Almaaz", "Capella"),
# Virgo
("Spica", "Heze"),
("Heze", "Porrima"),
("Porrima", "Zaniah"),
("Porrima", "Vindemiatrix"),
# Corona Borealis
("Alphecca", "Nusakan"),
("Alphecca", "Gamma CrB"),
("Gamma CrB", "Delta CrB"),
# Pegasus (Great Square)
("Markab", "Scheat"),
("Scheat", "Algenib"),
("Markab", "Enif"),
("Markab", "Algenib"),
# Andromeda
("Alpheratz", "Mirach"),
("Mirach", "Almach"),
("Alpheratz", "Scheat"),
# Draco
("Eltanin", "Rastaban"),
("Rastaban", "Kuma"),
("Kuma", "Grumium"),
("Eltanin", "Grumium"),
# Hercules
("Kornephoros", "Zeta Her"),
("Zeta Her", "Eta Her"),
("Eta Her", "Pi Her"),
("Kornephoros", "Rasalgethi"),
("Rasalgethi", "Sarin"),
("Sarin", "Pi Her"),
# Sagittarius (Teapot)
("Kaus Australis", "Kaus Media"),
("Kaus Media", "Kaus Borealis"),
("Kaus Borealis", "Nunki"),
("Nunki", "Ascella"),
("Ascella", "Kaus Australis"),
("Nash", "Kaus Media"),
]
# Limit of the circular sky chart: declination floor (stars below this are off-map)
DEC_FLOOR = -48.0
R_MAX = 90.0 - DEC_FLOOR # radial distance of the chart boundary
# North-polar azimuthal-equidistant projection: equally spaced declination
# circles give a natural *circular* sky boundary (the spec's preferred style).
# theta = RA (radians) r = colatitude = 90deg - Dec
def project(ra_h, dec_d):
theta = np.asarray(ra_h, dtype=float) * (np.pi / 12.0)
r = 90.0 - np.asarray(dec_d, dtype=float)
return theta, r
# Background field stars (faint, for celestial-sphere context)
n_bg = 320
bg_ra_hours = np.random.uniform(0, 24, n_bg)
bg_dec_deg = np.random.uniform(DEC_FLOOR, 72, n_bg)
bg_mag = np.random.uniform(4.0, 6.0, n_bg)
bg_theta, bg_r = project(bg_ra_hours, bg_dec_deg)
# Milky Way band — soft cloud of faint points along the galactic plane.
# Convert galactic (l, b) -> equatorial (RA, Dec) via the standard NGP frame.
ra_ngp, dec_ngp, l_ncp = np.radians(192.85948), np.radians(27.12825), np.radians(122.93192)
n_mw = 2600
mw_l = np.random.uniform(0, 2 * np.pi, n_mw)
mw_b = np.random.normal(0.0, np.radians(8.5), n_mw) # band thickness ~ +/-8.5deg
mw_dec = np.arcsin(np.sin(dec_ngp) * np.sin(mw_b) + np.cos(dec_ngp) * np.cos(mw_b) * np.cos(l_ncp - mw_l))
mw_ra = ra_ngp + np.arctan2(
np.cos(mw_b) * np.sin(l_ncp - mw_l),
np.sin(mw_b) * np.cos(dec_ngp) - np.cos(mw_b) * np.sin(dec_ngp) * np.cos(l_ncp - mw_l),
)
mw_theta, mw_r = project(np.degrees(mw_ra) / 15.0, np.degrees(mw_dec))
# Extract named-star arrays
star_names = list(stars.keys())
star_ra_h = np.array([stars[s][0] for s in star_names])
star_dec_d = np.array([stars[s][1] for s in star_names])
star_mag = np.array([stars[s][2] for s in star_names])
star_theta, star_r = project(star_ra_h, star_dec_d)
# Size mapping: brighter stars (lower magnitude) map to larger points
max_mag, min_mag = 6.5, -1.5
min_size, max_size = 5, 175
star_sizes = max_size * ((max_mag - star_mag) / (max_mag - min_mag)) ** 1.5 + min_size
# Color mapping by magnitude — Imprint sequential cmap (brand green -> blue).
# Brightest stars (low magnitude) get brand green; faintest fade to blue.
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", [BRAND, "#4467A3"])
star_color_norm = np.clip((star_mag - min_mag) / (max_mag - min_mag), 0, 1)
star_colors = imprint_seq(star_color_norm)
# Plot — square axes centered in the 3200x1800 canvas so the polar plot reads
# as a true circle, leaving the side margins for the legends.
fig = plt.figure(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax = fig.add_axes([0.305, 0.085, 0.39, 0.83], projection="polar", facecolor=PAGE_BG)
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1) # RA increases clockwise from the top
ax.set_rlim(0, R_MAX)
ax.set_rorigin(0)
# Milky Way band (deepest layer) — faint diffuse glow
ax.scatter(mw_theta, mw_r, s=7, c=INK_MUTED, alpha=0.045, edgecolors="none", zorder=0)
# Faint background field stars (context layer, muted)
bg_sizes = max_size * ((max_mag - bg_mag) / (max_mag - min_mag)) ** 1.5 + min_size
ax.scatter(bg_theta, bg_r, s=bg_sizes, c=INK_MUTED, alpha=0.30, edgecolors="none", zorder=1)
# Constellation stick-figure lines (thin, semi-transparent structural ink)
for s1, s2 in edges:
t1, r1 = project(stars[s1][0], stars[s1][1])
t2, r2 = project(stars[s2][0], stars[s2][1])
# Unwrap so the segment takes the short way around the RA circle
if t2 - t1 > np.pi:
t2 -= 2 * np.pi
elif t1 - t2 > np.pi:
t2 += 2 * np.pi
ax.plot([t1, t2], [r1, r2], color=INK_SOFT, linewidth=0.8, alpha=0.5, zorder=2)
# Soft halo behind the brightest stars (mag < 1.0) for a gentle glow
bright_mask = star_mag < 1.0
ax.scatter(
star_theta[bright_mask],
star_r[bright_mask],
s=star_sizes[bright_mask] * 2.6,
c=BRAND,
alpha=0.10,
edgecolors="none",
zorder=2,
)
# Named stars — sized by magnitude, colored along the Imprint sequential ramp
ax.scatter(star_theta, star_r, s=star_sizes, c=star_colors, alpha=0.95, edgecolors=PAGE_BG, linewidth=0.4, zorder=3)
# Ecliptic — dashed amber reference curve. The ecliptic is the great circle
# 23.44deg from the ecliptic pole; trace it in RA/Dec then project.
eps = np.radians(23.44)
ecl_lon = np.linspace(0, 2 * np.pi, 720)
ecl_dec = np.degrees(np.arcsin(np.sin(eps) * np.sin(ecl_lon)))
ecl_ra_h = (np.degrees(np.arctan2(np.cos(eps) * np.sin(ecl_lon), np.cos(ecl_lon))) / 15.0) % 24
ecl_theta, ecl_r = project(ecl_ra_h, ecl_dec)
# Break the curve where it wraps past the RA seam to avoid a chord across the chart
seam = np.abs(np.diff(ecl_theta)) > np.pi
ecl_theta[:-1][seam] = np.nan
ax.plot(ecl_theta, ecl_r, color=AMBER, linewidth=1.4, alpha=0.8, linestyle=(0, (7, 4)), zorder=2)
# Label the brightest stars (mag < 1.0)
bright_offsets = {
"Sirius": (7, -10),
"Vega": (8, 6),
"Arcturus": (8, -4),
"Capella": (-6, 9),
"Rigel": (7, -10),
"Betelgeuse": (8, 6),
"Altair": (8, -8),
"Aldebaran": (-10, 8),
"Spica": (7, -10),
"Pollux": (8, 7),
}
for name, (ra, dec, mag, _) in stars.items():
if mag < 1.0:
t, r = project(ra, dec)
ax.annotate(
name,
(t, r),
fontsize=7.5,
color=INK,
alpha=0.9,
xytext=bright_offsets.get(name, (7, 6)),
textcoords="offset points",
zorder=5,
path_effects=[pe.withStroke(linewidth=2, foreground=PAGE_BG, alpha=0.8)],
)
# Constellation names placed near each group's centroid (circular mean in RA)
constellation_names = {
"Ori": "Orion",
"UMa": "Ursa Major",
"Cas": "Cassiopeia",
"Leo": "Leo",
"Sco": "Scorpius",
"Cyg": "Cygnus",
"Lyr": "Lyra",
"Gem": "Gemini",
"Tau": "Taurus",
"CMa": "Canis Major",
"Aql": "Aquila",
"Boo": "Boötes",
"Per": "Perseus",
"Aur": "Auriga",
"Vir": "Virgo",
"CrB": "Corona Bor.",
"Peg": "Pegasus",
"And": "Andromeda",
"Dra": "Draco",
"Her": "Hercules",
"Sgr": "Sagittarius",
}
# Seed each label at its constellation centroid (circular mean handles the
# RA=0h seam), then relax overlaps with a force-based pass in projected
# Cartesian space so the dense northern groups spread out legibly.
label_seeds = []
for abbr, full_name in constellation_names.items():
member_t = np.array([project(stars[s][0], stars[s][1])[0] for s in star_names if stars[s][3] == abbr])
member_r = np.array([project(stars[s][0], stars[s][1])[1] for s in star_names if stars[s][3] == abbr])
ct = np.arctan2(np.mean(np.sin(member_t)), np.mean(np.cos(member_t)))
cr = np.mean(member_r)
label_seeds.append((ct, cr, full_name))
xy = np.array([[r * np.cos(t), r * np.sin(t)] for t, r, _ in label_seeds])
# Width-aware half-extents (data units) so long names reserve more room.
half_w = np.array([len(full) * 0.95 + 2.0 for _, _, full in label_seeds])
# Fixed obstacles: bright-star labels keep their place, so push names off them.
fixed_xy = np.array(
[
[project(ra, dec)[1] * np.cos(project(ra, dec)[0]), project(ra, dec)[1] * np.sin(project(ra, dec)[0])]
for ra, dec, mag, _ in stars.values()
if mag < 1.0
]
)
MIN_D_FIXED = 12.0
for _ in range(200):
moved = False
for i in range(len(xy)):
for j in range(i + 1, len(xy)):
d = xy[i] - xy[j]
dist = np.hypot(d[0], d[1])
need = half_w[i] + half_w[j]
if 1e-6 < dist < need:
shift = (d / dist) * (need - dist) / 2.0
xy[i] += shift
xy[j] -= shift
moved = True
for fx in fixed_xy:
d = xy[i] - fx
dist = np.hypot(d[0], d[1])
if 1e-6 < dist < MIN_D_FIXED:
xy[i] += (d / dist) * (MIN_D_FIXED - dist)
moved = True
if not moved:
break
for (x, y), (_, _, full_name) in zip(xy, label_seeds, strict=True):
nr = min(np.hypot(x, y), R_MAX - 4.0)
nt = np.arctan2(y, x)
ax.text(
nt,
nr,
full_name,
fontsize=8,
color=INK_SOFT,
alpha=0.9,
ha="center",
va="center",
fontstyle="italic",
fontweight="medium",
zorder=4,
path_effects=[pe.withStroke(linewidth=2.5, foreground=PAGE_BG, alpha=0.7)],
)
# Style the circular coordinate grid (RA spokes + Dec circles)
ax.grid(True, color=INK, linewidth=0.5, alpha=0.16, linestyle=(0, (4, 6)))
ax.set_axisbelow(True)
ax.spines["polar"].set_color(INK_SOFT)
ax.spines["polar"].set_alpha(0.5)
ax.spines["polar"].set_linewidth(1.0)
# RA spokes every 2 hours, labelled in hours
ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 6))
ax.set_xticklabels([f"{(2 * k) % 24}h" for k in range(12)])
# Dec circles: r = 90 - Dec -> +60, +30, 0 (equator), -30
ax.set_rticks([30, 60, 90, 120])
ax.set_yticklabels(["+60°", "+30°", "0°", "−30°"])
ax.set_rlabel_position(99)
ax.tick_params(axis="x", colors=INK_SOFT, labelsize=8, pad=2)
ax.tick_params(axis="y", colors=INK_MUTED, labelsize=7)
for lbl in ax.get_yticklabels():
lbl.set_path_effects([pe.withStroke(linewidth=2, foreground=PAGE_BG, alpha=0.7)])
# Feature legend (ecliptic + Milky Way) — upper-right margin
feature_handles = [
Line2D([0], [0], color=AMBER, lw=1.4, linestyle=(0, (5, 3)), label="Ecliptic"),
Patch(facecolor=INK_MUTED, alpha=0.35, edgecolor="none", label="Milky Way"),
]
leg1 = ax.legend(
handles=feature_handles,
loc="upper left",
bbox_to_anchor=(1.04, 1.02),
fontsize=8,
facecolor=ELEVATED_BG,
edgecolor=INK_SOFT,
framealpha=0.9,
handlelength=1.8,
borderpad=0.7,
)
plt.setp(leg1.get_texts(), color=INK_SOFT)
ax.add_artist(leg1)
# Magnitude size legend — lower-right margin (size encodes brightness)
mag_legend = [0.0, 2.0, 4.0]
mag_labels = ["0 (bright)", "2", "4 (faint)"]
size_handles = [
Line2D(
[0],
[0],
marker="o",
color="none",
markerfacecolor=imprint_seq((m - min_mag) / (max_mag - min_mag)),
markeredgecolor=PAGE_BG,
markeredgewidth=0.4,
markersize=np.sqrt(max_size * ((max_mag - m) / (max_mag - min_mag)) ** 1.5 + min_size),
label=lab,
)
for m, lab in zip(mag_legend, mag_labels, strict=True)
]
leg2 = ax.legend(
handles=size_handles,
loc="lower left",
bbox_to_anchor=(1.04, -0.02),
fontsize=8,
title="Apparent magnitude",
facecolor=ELEVATED_BG,
edgecolor=INK_SOFT,
framealpha=0.9,
labelspacing=1.1,
borderpad=0.8,
handletextpad=1.0,
)
plt.setp(leg2.get_texts(), color=INK_SOFT)
leg2.get_title().set_color(INK)
leg2.get_title().set_fontsize(8)
# Title + footer subtitle
fig.text(
0.5,
0.95,
"star-chart-constellation · python · matplotlib · anyplot.ai",
ha="center",
fontsize=12,
fontweight="medium",
color=INK,
)
fig.text(
0.5,
0.035,
"Azimuthal-equidistant sky chart | Right Ascension (hours) · Declination (°) | point size ∝ brightness",
ha="center",
fontsize=9,
color=INK_SOFT,
)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=fig.get_facecolor())
Part of Star Chart with Constellations on anyplot.ai.