Reading the Sky: A Hertzsprung-Russell Diagram from the dslabs stars Dataset

Author

Dev Narang

Published

Invalid Date

The Dataset

This graph uses the stars dataset from the dslabs package (Rafael Irizarry, Data Science Labs). It records 96 of the nearest and brightest stars in the sky with four variables:

  • star — the star’s name
  • temp — surface temperature in Kelvin, from about 2,600 K to 28,000 K
  • magnitude — absolute magnitude, a measure of intrinsic brightness. The scale runs backwards: smaller and negative numbers mean brighter stars, so the Sun sits at 4.8 while the supergiant Rigel is at -7.2.
  • type — spectral class, the letter astronomers assign a star based on its temperature. From hottest to coolest the sequence is O, B, A, F, G, K, M. Classes beginning with D (DA, DB, DF) are white dwarfs, the collapsed remnants of dead stars.

I chose stars because it is not one of the datasets used in the course notes. The Unit 4 notes work with murders, gapminder, us_contagious_diseases, and polls_us_election_2016, so everything here is built from scratch rather than modified from a tutorial example.

Setup

# dslabs supplies the stars dataset; tidyverse gives dplyr for wrangling and
# ggplot2 for the graph; ggrepel places star name labels without overlap;
# scales lets me build a combined log-and-reversed axis transformation.
library(dslabs)
library(tidyverse)
library(ggrepel)
library(scales)

# Load the stars dataset into the environment and confirm its structure.
data(stars)
glimpse(stars)
Rows: 96
Columns: 4
$ star      <fct> Sun, SiriusA, Canopus, Arcturus, AlphaCentauriA, Vega, Capel…
$ magnitude <dbl> 4.8, 1.4, -3.1, -0.4, 4.3, 0.5, -0.6, -7.2, 2.6, -5.7, -2.4,…
$ temp      <int> 5840, 9620, 7400, 4590, 5840, 9900, 5150, 12140, 6580, 3200,…
$ type      <chr> "G", "A", "F", "K", "G", "A", "G", "B", "F", "M", "B", "B", …

Preparing the Data

# Two changes are needed before plotting.
# 1. The three white dwarf classes (DA, DB, DF) are only four stars combined and
#    are physically one population, so str_starts() collapses them into a single
#    "White dwarf" group. This keeps the legend readable.
# 2. Spectral class is stored as plain text, which ggplot2 would sort
#    alphabetically. Converting it to a factor in temperature order (O hottest
#    through M coolest) makes the colour legend run hot to cool instead.
hr <- stars |>
  mutate(
    spectral = if_else(str_starts(type, "D"), "White dwarf", type),
    spectral = factor(spectral,
                      levels = c("O", "B", "A", "F", "G", "K", "M", "White dwarf"))
  )

# Check how many stars fall in each spectral class.
table(hr$spectral)

          O           B           A           F           G           K 
          1          19          13           7           4          16 
          M White dwarf 
         32           4 
# Select a handful of stars most readers will recognise, so the graph has
# reference points instead of 96 anonymous dots.
label_stars <- hr |>
  filter(star %in% c("Sun", "Rigel", "Betelgeuse", "SiriusA",
                     "Antares", "Arcturus", "Deneb"))

label_stars
        star magnitude  temp type spectral
1        Sun       4.8  5840    G        G
2    SiriusA       1.4  9620    A        A
3   Arcturus      -0.4  4590    K        K
4      Rigel      -7.2 12140    B        B
5 Betelgeuse      -5.7  3200    M        M
6    Antares      -5.2  3340    M        M
7      Deneb      -7.2  9340    A        A

The Graph

# A custom palette replacing ggplot2's default hues. Each colour approximates the
# real visible colour of that spectral class, so the legend runs blue (hot)
# through white and yellow to orange-red (cool), with teal marking white dwarfs.
star_cols <- c("O"           = "#6D8DFF",
               "B"           = "#9BB0FF",
               "A"           = "#CAD7FF",
               "F"           = "#FFFFFF",
               "G"           = "#FFE87A",
               "K"           = "#FFB56B",
               "M"           = "#FF6B47",
               "White dwarf" = "#4FD8C8")

# Astronomers plot temperature decreasing to the right AND on a log scale.
# transform_compose() chains the two transformations into one axis.
log_reversed <- transform_compose(transform_log10(), transform_reverse())

ggplot(hr, aes(x = temp, y = magnitude)) +
  # Text annotations naming the three stellar populations the diagram reveals.
  # These are placed before the points so labels sit underneath them.
  annotate("text", x = 4300, y = -7.6, label = "Giants and supergiants",
           color = "grey60", size = 3.4, fontface = "italic") +
  annotate("text", x = 17000, y = 5.5, label = "Main sequence",
           color = "grey60", size = 3.4, fontface = "italic", angle = -30) +
  annotate("text", x = 13000, y = 12.6, label = "White dwarfs",
           color = "grey60", size = 3.4, fontface = "italic") +
  # Colour maps to spectral class, the third variable in the graph.
  geom_point(aes(color = spectral), size = 3.4, alpha = 0.95) +
  # Repelled labels for the recognisable stars only.
  geom_text_repel(data = label_stars, aes(label = star),
                  color = "grey85", size = 3, seed = 7, box.padding = 0.6,
                  min.segment.length = 0, segment.color = "grey55") +
  # Reversed log temperature axis with readable comma-formatted breaks.
  scale_x_continuous(transform = log_reversed,
                     breaks = c(3000, 5000, 10000, 20000, 30000),
                     labels = label_comma()) +
  # Reversed magnitude axis, because smaller magnitudes are brighter stars.
  scale_y_reverse(breaks = seq(-10, 15, 5)) +
  # Apply the custom colours and give the legend a meaningful title.
  scale_color_manual(name = "Spectral Class", values = star_cols) +
  labs(
    title    = "Hotter Stars Are Brighter, Except for the Ones That Break the Rule",
    subtitle = "Temperature is reversed and logged and magnitude is reversed, following astronomical convention:\nhotter stars sit to the left, brighter stars sit higher",
    x        = "Surface Temperature (Kelvin, log scale)",
    y        = "Absolute Magnitude (smaller values are brighter)",
    caption  = "Source: stars dataset, dslabs R package (Rafael Irizarry)"
  ) +
  # theme_minimal() replaces ggplot2's default grey theme, then a dark panel and
  # light text are applied so the star colours read the way they would in a
  # night sky rather than washing out against white.
  theme_minimal(base_size = 12) +
  theme(
    plot.background  = element_rect(fill = "#11141C", color = NA),
    panel.background = element_rect(fill = "#11141C", color = NA),
    panel.grid.major = element_line(color = "#2A2F3D", linewidth = 0.3),
    panel.grid.minor = element_blank(),
    text             = element_text(color = "grey85"),
    plot.title       = element_text(color = "white", face = "bold", size = 14),
    plot.subtitle    = element_text(color = "grey70", size = 9.5),
    plot.caption     = element_text(color = "grey55", hjust = 0, size = 8),
    axis.text        = element_text(color = "grey75"),
    legend.key       = element_blank()
  )
Scatterplot of stellar surface temperature against absolute magnitude for 96 stars. Temperature runs on a reversed logarithmic x-axis so hotter stars appear on the left, and magnitude is reversed so brighter stars appear higher. Points are coloured by spectral class from blue for hot O and B stars through white and yellow to orange for cool M stars, with white dwarfs in teal. Most stars form a diagonal band called the main sequence running from hot and bright at upper left to cool and dim at lower right. A separate group of cool but very bright giants and supergiants sits at the upper right, including Betelgeuse and Antares, and four faint white dwarfs sit at the lower left.
Figure 1: Hertzsprung-Russell diagram of 96 nearby and bright stars, coloured by spectral class

How I Built It, and What It Shows

The graph is a scatterplot of two continuous variables — surface temperature and absolute magnitude — with spectral class carried by colour as the third variable. Building it took three deliberate choices. First, both axes are reversed, because astronomers draw temperature decreasing to the right and magnitude is a backwards scale where smaller numbers mean brighter stars; I chained a log and a reverse transformation with transform_compose() so the x-axis does both at once. Without the log scale the cool M stars crush against the right edge and the main sequence stops looking like a line. Second, I replaced ggplot2’s default hues with a hand-built palette whose colours approximate what each spectral class actually looks like, running blue through white and yellow to orange-red, and put it on a dark panel so those colours read as starlight instead of washing out on white. Third, I collapsed the three white dwarf classes into one group and labelled seven familiar stars, so the plot has anchors rather than 96 anonymous points.

What emerges is the Hertzsprung-Russell diagram, and the payoff is that a single scatterplot separates three distinct populations. Most stars fall along the diagonal main sequence, where hotter means brighter. The interesting part is what disobeys it: Betelgeuse and Antares sit at the top right, cool enough to be dim by the main-sequence rule yet among the brightest things here, because they are enormous red supergiants whose surface area more than compensates for their low temperature. Down at the lower left are four white dwarfs, hot but so tiny they are nearly invisible. The Sun lands unremarkably in the middle of the main sequence. The colour variable is what makes the exceptions legible — the giants and the white dwarfs are identifiable as anomalies precisely because their colours put them where their positions do not.