{"spec_id":"star-chart-constellation","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' star-chart-constellation: Star Chart with Constellations\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-06-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\n# Imprint palette — continuous magnitude scale (imprint_seq: brand green -> blue)\nIMPRINT_SEQ_LOW  <- \"#009E73\"  # brightest stars (low magnitude) -> brand green\nIMPRINT_SEQ_HIGH <- \"#4467A3\"  # faintest stars  (high magnitude) -> blue\n\n# --- Data: notable stars across northern constellations ---------------------\n# ra in hours (0-24), dec in degrees, mag = apparent visual magnitude\nnamed <- tibble::tribble(\n    ~star_id,       ~ra,    ~dec,    ~mag,  ~constellation,\n    # Ursa Major (Big Dipper)\n    \"Dubhe\",        11.06,  61.75,   1.79,  \"Ursa Major\",\n    \"Merak\",        11.03,  56.38,   2.37,  \"Ursa Major\",\n    \"Phecda\",       11.90,  53.69,   2.44,  \"Ursa Major\",\n    \"Megrez\",       12.26,  57.03,   3.31,  \"Ursa Major\",\n    \"Alioth\",       12.90,  55.96,   1.77,  \"Ursa Major\",\n    \"Mizar\",        13.40,  54.93,   2.04,  \"Ursa Major\",\n    \"Alkaid\",       13.79,  49.31,   1.86,  \"Ursa Major\",\n    # Ursa Minor (Little Dipper)\n    \"Polaris\",       2.53,  89.26,   1.98,  \"Ursa Minor\",\n    \"Yildun\",       17.54,  86.59,   4.36,  \"Ursa Minor\",\n    \"Epsilon UMi\",  16.77,  82.04,   4.21,  \"Ursa Minor\",\n    \"Zeta UMi\",     15.73,  77.79,   4.32,  \"Ursa Minor\",\n    \"Eta UMi\",      16.29,  75.76,   4.95,  \"Ursa Minor\",\n    \"Kochab\",       14.85,  74.16,   2.08,  \"Ursa Minor\",\n    \"Pherkad\",      15.35,  71.83,   3.05,  \"Ursa Minor\",\n    # Cassiopeia (W)\n    \"Caph\",          0.15,  59.15,   2.27,  \"Cassiopeia\",\n    \"Schedar\",       0.68,  56.54,   2.24,  \"Cassiopeia\",\n    \"Gamma Cas\",     0.95,  60.72,   2.47,  \"Cassiopeia\",\n    \"Ruchbah\",       1.43,  60.24,   2.68,  \"Cassiopeia\",\n    \"Segin\",         1.91,  63.67,   3.38,  \"Cassiopeia\",\n    # Cygnus (Northern Cross)\n    \"Deneb\",        20.69,  45.28,   1.25,  \"Cygnus\",\n    \"Sadr\",         20.37,  40.26,   2.23,  \"Cygnus\",\n    \"Gienah Cyg\",   20.77,  33.97,   2.48,  \"Cygnus\",\n    \"Delta Cyg\",    19.75,  45.13,   2.87,  \"Cygnus\",\n    \"Albireo\",      19.51,  27.96,   3.18,  \"Cygnus\",\n    # Lyra\n    \"Vega\",         18.62,  38.78,   0.03,  \"Lyra\",\n    \"Sheliak\",      18.83,  33.36,   3.52,  \"Lyra\",\n    \"Sulafat\",      18.98,  32.69,   3.25,  \"Lyra\",\n    \"Zeta Lyr\",     18.75,  37.60,   4.36,  \"Lyra\",\n    \"Delta Lyr\",    18.90,  36.90,   4.30,  \"Lyra\",\n    # Bootes (the Kite)\n    \"Arcturus\",     14.26,  19.18,  -0.05,  \"Bootes\",\n    \"Izar\",         14.75,  27.07,   2.35,  \"Bootes\",\n    \"Seginus\",      14.53,  38.31,   3.03,  \"Bootes\",\n    \"Nekkar\",       15.03,  40.39,   3.49,  \"Bootes\",\n    \"Delta Boo\",    15.26,  33.31,   3.47,  \"Bootes\",\n    \"Rho Boo\",      14.53,  30.37,   3.58,  \"Bootes\",\n    # Leo\n    \"Regulus\",      10.14,  11.97,   1.36,  \"Leo\",\n    \"Eta Leo\",      10.12,  16.76,   3.48,  \"Leo\",\n    \"Algieba\",      10.33,  19.84,   2.08,  \"Leo\",\n    \"Adhafera\",     10.28,  23.42,   3.43,  \"Leo\",\n    \"Zosma\",        11.24,  20.52,   2.56,  \"Leo\",\n    \"Chort\",        11.24,  15.43,   3.33,  \"Leo\",\n    \"Denebola\",     11.82,  14.57,   2.14,  \"Leo\",\n    # Gemini\n    \"Pollux\",        7.76,  28.03,   1.14,  \"Gemini\",\n    \"Castor\",        7.58,  31.89,   1.58,  \"Gemini\",\n    \"Alhena\",        6.63,  16.40,   1.93,  \"Gemini\",\n    \"Mebsuta\",       6.73,  25.13,   3.06,  \"Gemini\",\n    \"Tejat\",         6.38,  22.51,   2.87,  \"Gemini\",\n    # Orion\n    \"Betelgeuse\",    5.92,   7.41,   0.42,  \"Orion\",\n    \"Bellatrix\",     5.42,   6.35,   1.64,  \"Orion\",\n    \"Rigel\",         5.24,  -8.20,   0.18,  \"Orion\",\n    \"Saiph\",         5.80,  -9.67,   2.07,  \"Orion\",\n    \"Alnitak\",       5.68,  -1.94,   1.77,  \"Orion\",\n    \"Alnilam\",       5.60,  -1.20,   1.69,  \"Orion\",\n    \"Mintaka\",       5.53,  -0.30,   2.23,  \"Orion\",\n    # Auriga (+ Taurus)\n    \"Capella\",       5.28,  46.00,   0.08,  \"Auriga\",\n    \"Menkalinan\",    5.99,  44.95,   1.90,  \"Auriga\",\n    \"Mahasim\",       5.99,  37.21,   2.62,  \"Auriga\",\n    \"Hassaleh\",      4.95,  33.17,   2.69,  \"Auriga\",\n    \"Elnath\",        5.44,  28.61,   1.65,  \"Auriga\",\n    \"Aldebaran\",     4.60,  16.51,   0.87,  \"Taurus\"\n) %>%\n    mutate(is_field = FALSE)\n\n# Faint background field stars (uniform on the sphere via sin(dec)) to give depth\nn_field <- 280\nfield <- tibble::tibble(\n    star_id       = paste0(\"field_\", seq_len(n_field)),\n    ra            = runif(n_field, 0, 24),\n    dec           = asin(runif(n_field, sin(-15 * pi / 180), sin(88 * pi / 180))) * 180 / pi,\n    mag           = runif(n_field, 4.0, 5.8),\n    constellation = NA_character_,\n    is_field      = TRUE\n)\n\n# Azimuthal-equidistant projection centred on the north celestial pole:\n#   radius = 90 - dec (degrees from pole), angle = RA. Brighter -> larger point.\nDEC_LIMIT  <- -15\nSKY_RADIUS <- 90 - DEC_LIMIT\nstars <- bind_rows(named, field) %>%\n    mutate(\n        ang        = ra * pi / 12,\n        radius     = 90 - dec,\n        x          = radius * sin(ang),\n        y          = radius * cos(ang),\n        point_size = scales::rescale(-mag, to = c(1.0, 7.0))\n    ) %>%\n    filter(radius <= SKY_RADIUS)\n\n# Constellation stick-figure edges (pairs of star_ids)\nedges <- tibble::tribble(\n    ~from,          ~to,\n    \"Dubhe\", \"Merak\", \"Merak\", \"Phecda\", \"Phecda\", \"Megrez\", \"Megrez\", \"Dubhe\",\n    \"Megrez\", \"Alioth\", \"Alioth\", \"Mizar\", \"Mizar\", \"Alkaid\",\n    \"Polaris\", \"Yildun\", \"Yildun\", \"Epsilon UMi\", \"Epsilon UMi\", \"Zeta UMi\",\n    \"Zeta UMi\", \"Eta UMi\", \"Eta UMi\", \"Kochab\", \"Kochab\", \"Pherkad\", \"Pherkad\", \"Zeta UMi\",\n    \"Caph\", \"Schedar\", \"Schedar\", \"Gamma Cas\", \"Gamma Cas\", \"Ruchbah\", \"Ruchbah\", \"Segin\",\n    \"Deneb\", \"Sadr\", \"Sadr\", \"Albireo\", \"Delta Cyg\", \"Sadr\", \"Sadr\", \"Gienah Cyg\",\n    \"Vega\", \"Zeta Lyr\", \"Zeta Lyr\", \"Delta Lyr\", \"Delta Lyr\", \"Sulafat\",\n    \"Sulafat\", \"Sheliak\", \"Sheliak\", \"Zeta Lyr\",\n    \"Arcturus\", \"Izar\", \"Izar\", \"Delta Boo\", \"Delta Boo\", \"Nekkar\",\n    \"Nekkar\", \"Seginus\", \"Seginus\", \"Rho Boo\", \"Rho Boo\", \"Arcturus\",\n    \"Regulus\", \"Eta Leo\", \"Eta Leo\", \"Algieba\", \"Algieba\", \"Adhafera\",\n    \"Algieba\", \"Zosma\", \"Zosma\", \"Denebola\", \"Denebola\", \"Chort\", \"Chort\", \"Regulus\",\n    \"Castor\", \"Pollux\", \"Castor\", \"Mebsuta\", \"Mebsuta\", \"Tejat\", \"Pollux\", \"Alhena\",\n    \"Betelgeuse\", \"Bellatrix\", \"Betelgeuse\", \"Alnitak\", \"Bellatrix\", \"Mintaka\",\n    \"Alnitak\", \"Alnilam\", \"Alnilam\", \"Mintaka\", \"Alnitak\", \"Saiph\",\n    \"Mintaka\", \"Rigel\", \"Saiph\", \"Rigel\",\n    \"Capella\", \"Menkalinan\", \"Menkalinan\", \"Mahasim\", \"Mahasim\", \"Elnath\",\n    \"Elnath\", \"Hassaleh\", \"Hassaleh\", \"Capella\", \"Elnath\", \"Aldebaran\"\n)\n\ncoords <- stars %>% select(star_id, x, y)\nedge_lines <- edges %>%\n    left_join(coords %>% rename(from = star_id), by = \"from\") %>%\n    left_join(coords %>% rename(to = star_id, xend = x, yend = y), by = \"to\")\n\n# Constellation name labels at the centroid of each constellation's bright stars\ncon_labels <- stars %>%\n    filter(!is_field) %>%\n    group_by(constellation) %>%\n    summarise(x = mean(x), y = mean(y), .groups = \"drop\")\n\n# --- Sky scaffolding: disc, declination circles, RA spokes, boundary --------\ndisc <- tibble::tibble(t = seq(0, 2 * pi, length.out = 240)) %>%\n    mutate(x = SKY_RADIUS * cos(t), y = SKY_RADIUS * sin(t))\n\ndec_circles <- tidyr::expand_grid(dec = c(0, 30, 60), t = seq(0, 2 * pi, length.out = 240)) %>%\n    mutate(r = 90 - dec, x = r * cos(t), y = r * sin(t))\n\nra_spokes <- tibble::tibble(ra = seq(0, 22, by = 2)) %>%\n    mutate(\n        ang  = ra * pi / 12,\n        xend = SKY_RADIUS * sin(ang),\n        yend = SKY_RADIUS * cos(ang),\n        lx   = (SKY_RADIUS + 6) * sin(ang),\n        ly   = (SKY_RADIUS + 6) * cos(ang),\n        lab  = paste0(ra, \"h\")\n    )\n\ndec_ticks <- tibble::tibble(dec = c(0, 30, 60)) %>%\n    mutate(x = 4, y = 90 - dec, lab = paste0(\"+\", dec, \"°\"))\n\n# Ecliptic: the Sun's apparent annual path (obliquity 23.44 degrees)\necliptic <- tibble::tibble(lambda = seq(0, 360, length.out = 720)) %>%\n    mutate(\n        lam = lambda * pi / 180,\n        eps = 23.44 * pi / 180,\n        dec = asin(sin(eps) * sin(lam)) * 180 / pi,\n        ra  = (atan2(cos(eps) * sin(lam), cos(lam)) * 12 / pi) %% 24,\n        r   = 90 - dec,\n        ang = ra * pi / 12,\n        # break the path (NA) where the ecliptic dips outside the sky boundary\n        x   = ifelse(r <= SKY_RADIUS, r * sin(ang), NA_real_),\n        y   = ifelse(r <= SKY_RADIUS, r * cos(ang), NA_real_)\n    )\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot() +\n    geom_polygon(data = disc, aes(x, y), fill = ELEVATED_BG, color = NA) +\n    geom_path(\n        data = dec_circles, aes(x, y, group = dec),\n        color = INK, alpha = 0.16, linewidth = 0.3\n    ) +\n    geom_segment(\n        data = ra_spokes, aes(x = 0, y = 0, xend = xend, yend = yend),\n        color = INK, alpha = 0.13, linewidth = 0.25\n    ) +\n    geom_path(\n        data = ecliptic, aes(x, y),\n        color = INK_MUTED, linewidth = 0.45, linetype = \"dashed\"\n    ) +\n    geom_path(data = disc, aes(x, y), color = INK_SOFT, linewidth = 0.7) +\n    geom_segment(\n        data = edge_lines, aes(x = x, y = y, xend = xend, yend = yend),\n        color = INK_SOFT, alpha = 0.7, linewidth = 0.6\n    ) +\n    geom_point(\n        data = stars, aes(x, y, size = point_size, color = mag)\n    ) +\n    geom_text(\n        data = con_labels, aes(x, y, label = constellation),\n        color = INK_SOFT, size = 3.4, fontface = \"italic\", alpha = 0.9\n    ) +\n    geom_text(\n        data = ra_spokes, aes(lx, ly, label = lab),\n        color = INK_SOFT, size = 3.0\n    ) +\n    geom_text(\n        data = dec_ticks, aes(x, y, label = lab),\n        color = INK_MUTED, size = 2.7, hjust = -0.1\n    ) +\n    scale_size_identity() +\n    scale_color_gradient(\n        low = IMPRINT_SEQ_LOW, high = IMPRINT_SEQ_HIGH,\n        name = \"Apparent\\nmagnitude\",\n        guide = guide_colorbar(reverse = TRUE)\n    ) +\n    coord_fixed(xlim = c(-118, 118), ylim = c(-118, 118), expand = FALSE) +\n    labs(title = \"star-chart-constellation · r · ggplot2 · anyplot.ai\") +\n    theme_void(base_size = 8) +\n    theme(\n        plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n        panel.background = element_rect(fill = PAGE_BG, color = NA),\n        plot.title       = element_text(\n            color = INK, size = 16, hjust = 0.5,\n            margin = margin(b = 6, t = 4)\n        ),\n        legend.title     = element_text(color = INK, size = 11),\n        legend.text      = element_text(color = INK_SOFT, size = 9),\n        legend.position  = \"right\",\n        plot.margin      = margin(14, 14, 14, 14)\n    ) +\n    guides(color = guide_colorbar(\n        reverse = TRUE,\n        barwidth = unit(0.45, \"cm\"),\n        barheight = unit(3.4, \"cm\")\n    ))\n\n# --- Save -------------------------------------------------------------------\nggsave(\n    filename = sprintf(\"plot-%s.png\", THEME),\n    plot     = p,\n    device   = ragg::agg_png,\n    width    = 6,\n    height   = 6,\n    units    = \"in\",\n    dpi      = 400\n)\n"}