Gaia Million Star Atlas#
Explore the sky, stellar color–magnitude diagram, and Galactic latitude fingerprint together. Try an approximate stellar population or brush either map to follow the same sources through all three views. Run with panel serve gaia_million_star_atlas.ipynb.
Data: ESA Gaia Archive, Gaia DR3, credited to ESA/Gaia/DPAC; CC BY-NC 3.0 IGO. This extract is a quality-selected, non-representative subset, not a complete Milky Way census.
import holoviews as hv
import numpy as np
import pandas as pd
import panel as pn
import param
from bokeh.models import ColumnDataSource, NumeralTickFormatter
from bokeh.plotting import figure
from holoviews.operation.datashader import rasterize
hv.extension("bokeh")
pn.extension(sizing_mode="stretch_width")
Data#
The Parquet extract was built with prepare_gaia.py, which sits next to this notebook. It queries gaiadr3.gaia_source on the Gaia TAP server in chunks of random_index, keeps sources with parallax_over_error > 5 and a measured bp_rp, and derives Galactic coordinates and a naive absolute G magnitude. pn.cache keeps the million rows in memory across sessions.
DATA_URL = "https://datasets.holoviz.org/gaia/v1/gaia_dr3_random12m_snr5.parquet"
@pn.cache
def load_stars():
try:
return pd.read_parquet(DATA_URL)
except Exception as exc:
raise RuntimeError(f"Could not load the Gaia DR3 extract from {DATA_URL}. Check network access and retry.") from exc
load_stars().head()
Selection statistics#
The population presets are HoloViews dim expressions that link_selections applies across every view. The statistics and the latitude histogram are plain functions of a selected frame, so they can be tested without a running app.
INK = "#f3f3ec"
MUTED = "#aabbd0"
TEAL = "#65e0db"
CORAL = "#ff987e"
PANEL = "#101d32"
BORDER = "#30445e"
DENSITY = ["#061125", "#12488e", "#198fe0", "#57e2da", "#f2b76c", "#fff5dd"]
BINS = np.arange(0, 95, 5)
LATITUDE = (BINS[:-1] + BINS[1:]) / 2
COLOR = hv.dim("bp_rp")
MAGNITUDE = hv.dim("absolute_g")
PRESETS = {
"All stars": "all", "Blue + luminous": "blue", "Red giants": "giants", "Cool dwarfs": "dwarfs",
"Brushed": "brush",
}
REGIONS = {
"all": None,
"blue": (COLOR < 0.9) & (MAGNITUDE < 3),
"giants": (COLOR > 1.35) & (MAGNITUDE < 2.5),
"dwarfs": (COLOR > 1.7) & (MAGNITUDE > 7),
}
def latitude_share(frame):
counts, _ = np.histogram(np.abs(frame["galactic_latitude"]), bins=BINS)
return 100 * counts / len(frame) if len(frame) else np.zeros(len(LATITUDE))
def disk_share(frame):
return 100 * (np.abs(frame["galactic_latitude"]) < 10).mean()
def selection_stats(selected, total, comparison):
count = len(selected)
return {
"count": count,
"percent": 100 * count / total,
"median_parallax": selected["parallax"].median() if count else None,
"disk": disk_share(selected) if count else None,
"comparison": comparison,
}
def readout_text(stats):
if not stats["count"]:
return "No sources in this selection · 0.00% of extract · median parallax —"
return (
f"{stats['count']:,} sources · {stats['percent']:.2f}% of extract · "
f"median measured parallax {stats['median_parallax']:.2f} mas"
)
def insight_text(stats, baseline):
if not stats["count"]:
return "Try a wider box, another population, or clear the brush."
if stats["comparison"]:
return (
f"Within |b| < 10°: {stats['disk']:.1f}% of selection vs {baseline:.1f}% of full extract. "
"A sky brush constrains latitude directly; CMD regions compare populations within this quality-selected sample."
)
return (
f"{baseline:.1f}% of the full extract lies within 10° of the Galactic plane. "
"Choose a population or brush a plot to compare its sky distribution."
)
Plots and cards#
Both maps are datashaded rasters of the same frame, so link_selections can brush either one. The latitude profile is a plain Bokeh figure whose ColumnDataSource is patched in place on every selection. Each view sits in the same styled card.
RASTER_OPTS = dict(
cmap=DENSITY, cnorm="log", responsive="width", show_grid=False, bgcolor="#061125",
colorbar=False, default_tools=["pan", "wheel_zoom", "reset"],
)
def sky_raster(frame):
points = hv.Points(frame, ["galactic_longitude", "galactic_latitude"], ["bp_rp", "absolute_g"])
return rasterize(points, width=1100, height=340).opts(
clim=(1, 80), aspect=2.7, xlim=(-180, 180), ylim=(-90, 90),
xlabel="Galactic longitude (°)", ylabel="Galactic latitude (°)", **RASTER_OPTS,
)
def diagram_raster(frame):
points = hv.Points(frame, ["bp_rp", "absolute_g"], ["galactic_longitude", "galactic_latitude"])
return rasterize(points, width=700, height=340).opts(
clim=(1, 160), aspect=1.8, xlim=(-0.5, 4), ylim=(-6, 16), invert_yaxis=True,
xlabel="Color · BP − RP (mag)", ylabel="Absolute G magnitude (mag)", **RASTER_OPTS,
)
def profile_figure(full, selected, y_end):
fig = figure(
height=350, sizing_mode="stretch_width", x_range=(0, 90), y_range=(0, y_end),
x_axis_label="Absolute Galactic latitude |b| (deg)",
y_axis_label="Share of sources per 5° bin (%)",
toolbar_location=None, background_fill_color="#061125",
border_fill_color=PANEL, outline_line_color=BORDER,
)
fig.varea(x="latitude", y1=0, y2="share", source=selected, fill_color=CORAL, fill_alpha=0.22)
fig.line("latitude", "share", source=full, line_color=TEAL, line_width=2.5, legend_label="Full extract")
fig.line("latitude", "share", source=selected, line_color=CORAL, line_width=3, legend_label="Selection")
fig.xaxis.axis_label_text_color = INK
fig.yaxis.axis_label_text_color = INK
for axis in (fig.xaxis[0], fig.yaxis[0]):
axis.major_label_text_color = MUTED
axis.axis_line_color = "#55708f"
axis.major_tick_line_color = "#55708f"
fig.yaxis.formatter = NumeralTickFormatter(format="0.0")
fig.grid.grid_line_color = BORDER
fig.grid.grid_line_alpha = 0.4
fig.legend.background_fill_color = PANEL
fig.legend.background_fill_alpha = 0.7
fig.legend.label_text_color = INK
fig.legend.border_line_color = None
return fig
def card(kicker, accent, title, view, caption):
return pn.ui.Paper(
pn.ui.Typography(kicker, sx={"color": accent, "fontSize": "0.72rem", "letterSpacing": "0.18em"}),
pn.ui.Typography(title, sx={"color": INK, "fontFamily": "Georgia, serif", "fontSize": "1.35rem"}),
view,
pn.ui.Typography(caption, sx={"color": MUTED, "fontSize": "0.84rem"}),
sx={"p": 2, "backgroundColor": PANEL, "border": f"1px solid {BORDER}"},
)
Atlas state#
GaiaAtlas owns the selection. The preset radio group writes a region expression into link_selections, and a manual brush switches it to Brushed so the buttons never claim a selection they did not make. Each selection change schedules one callback that writes only the profile data and the stats dict; the readout and insight text are bound to stats.
class GaiaAtlas(pn.viewable.Viewer):
preset = param.Selector(default="all", objects=PRESETS)
reset = param.Event()
stats = param.Dict()
def __init__(self, **params):
self.stars = load_stars()
super().__init__(stats=selection_stats(self.stars, len(self.stars), False), **params)
self._applying = False
self._all_share = latitude_share(self.stars)
self._disk_baseline = disk_share(self.stars)
self.link = hv.link_selections.instance(
selected_color=None, unselected_color="#314c71", unselected_alpha=0.13,
)
self._sky = pn.ui.HoloViews(self.link(sky_raster(self.stars)), linked_axes=False)
self._diagram = pn.ui.HoloViews(self.link(diagram_raster(self.stars)), linked_axes=False)
self._selected_source = ColumnDataSource(dict(latitude=LATITUDE, share=np.zeros_like(self._all_share)))
self._profile = profile_figure(
ColumnDataSource(dict(latitude=LATITUDE, share=self._all_share)),
self._selected_source, self._all_share.max() * 1.15,
)
self._readout = pn.ui.Typography(
pn.bind(readout_text, self.param.stats), sx={"color": INK, "fontSize": "0.97rem"},
)
self._insight = pn.ui.Typography(
pn.bind(insight_text, self.param.stats, self._disk_baseline),
sx={"color": MUTED, "fontSize": "0.86rem"},
)
self._presets = pn.ui.RadioButtonGroup.from_param(
self.param.preset, label="", color="primary", min_width=300, max_width=760,
# Five labelled buttons do not fit a phone width; wrap at natural width instead of overflowing.
sx={"flexWrap": "wrap", "& .MuiToggleButton-root": {"flex": "1 0 auto", "width": "auto"}},
)
self._reset = pn.ui.Button.from_param(
self.param.reset, label="Clear brush", icon="restart_alt", variant="outlined", width=145,
)
pn.bind(self._update_profile, self.link.selection_param(self.stars), watch=True)
self.link.param.watch(self._sync_preset, "selection_expr")
@param.depends("preset", watch=True)
def _apply_preset(self):
if self.preset == "brush":
# Only a manual brush can produce this state; clicking it without one means no selection.
if self.link.selection_expr is None:
self.preset = "all"
return
self._applying = True
try:
# Replacing an active selection directly can leave stale HoloViews stream state.
with pn.io.hold():
self.link.selection_expr = None
self.link.selection_expr = REGIONS[self.preset]
finally:
self._applying = False
def _sync_preset(self, event):
if not self._applying:
self.preset = "all" if event.new is None else "brush"
@param.depends("reset", watch=True)
def _clear_selection(self):
self.preset = "all"
self.link.selection_expr = None
def _update_profile(self, selected):
selection = repr(self.link.selection_expr)
comparison = self.link.selection_expr is not None
stats = selection_stats(selected, len(self.stars), comparison)
share = latitude_share(selected) if comparison else np.zeros_like(self._all_share)
def update():
# A newer selection may have arrived before this callback ran.
if selection != repr(self.link.selection_expr):
return
self._selected_source.data = dict(latitude=LATITUDE, share=share)
self._profile.y_range.end = max(5, max(self._all_share.max(), share.max()) * 1.15)
self.stats = stats
pn.state.execute(update, schedule=True)
def __panel__(self):
return pn.ui.Container(pn.ui.Column(
pn.ui.Typography("GAIA DR3 / AN INTERACTIVE FIELD ATLAS", sx={"color": TEAL, "letterSpacing": "0.2em", "fontSize": "0.72rem"}),
pn.ui.Typography(
"The Milky Way in a million measurements",
sx={"color": INK, "fontFamily": "Georgia, serif",
"fontSize": {"xs": "1.9rem", "md": "2.5rem"}, "lineHeight": 1.1},
),
pn.ui.Typography(
"One selection, three views: trace a population from the sky to the "
"color–magnitude diagram and its latitude distribution.",
sx={"color": MUTED, "fontSize": "0.94rem"},
),
pn.ui.Row(self._presets, self._reset, align="center", sx={"gap": "8px", "flexWrap": "wrap"}),
pn.ui.Typography(
"Approximate CMD regions (mag): blue + luminous (BP−RP < 0.9, MG < 3); "
"red giants (BP−RP > 1.35, MG < 2.5); cool dwarfs (BP−RP > 1.7, MG > 7). "
"Or box/lasso either raster.", sx={"color": MUTED, "fontSize": "0.81rem"},
),
card(
"01 / GALACTIC COORDINATES", TEAL, "The sky, unfolded", self._sky,
"Density is shown on a logarithmic scale; the bright belt follows the Galactic plane.",
),
pn.ui.Row(self._readout, sx={"borderLeft": f"3px solid {CORAL}", "paddingLeft": "14px"}),
self._insight,
pn.ui.Grid(
pn.ui.Grid(card(
"02 / COLOR × ABSOLUTE G", CORAL, "A diagram of light and color", self._diagram,
"Brighter sources are higher; bluer sources are farther left. "
"Approximate regions are not spectral classifications.",
), size={"xs": 12, "md": 8}),
pn.ui.Grid(card(
"03 / GALACTIC LATITUDE FINGERPRINT", TEAL, "Where do they gather?",
pn.ui.Bokeh(self._profile),
"After selecting, each curve sums to 100%. Compare shapes, not raw population sizes.",
), size={"xs": 12, "md": 4}),
container=True, spacing=2,
),
pn.ui.Divider(),
pn.ui.Markdown(
f"**About these measurements** · {len(self.stars):,} observed Gaia DR3 sources from "
"`gaiadr3.gaia_source`, selected with `random_index < 12000000`, "
"`parallax_over_error > 5`, and a measured `bp_rp`. This is a "
"quality-selected, non-representative subset. Absolute G uses naive "
"inverse parallax; no extinction correction is applied. "
f"[Data extract]({DATA_URL}) · [ESA Gaia Archive](https://gea.esac.esa.int/archive/) "
"· ESA/Gaia/DPAC · [CC BY-NC 3.0 IGO](https://creativecommons.org/licenses/by-nc/3.0/igo/).",
styles={"color": MUTED},
),
sx={"gap": "10px"}, margin=(12, 0),
), width_option="xl")
Page#
The dark Page shell wraps a single atlas instance, so the notebook preview and the served app share one set of linked plots.
atlas = GaiaAtlas()
app = pn.ui.Page(
title="GAIA / FIELD ATLAS",
dark_theme=True,
theme_toggle=False,
theme_config={
"dark": {"palette": {
"primary": {"main": TEAL},
"background": {"default": "#071222", "paper": PANEL},
"text": {"primary": INK, "secondary": MUTED},
}},
},
sx={"& .header": {"backgroundColor": "#0b1a30", "backgroundImage": "none", "boxShadow": "none"}},
main=[atlas],
)
app
if pn.state.served:
app.servable()