Two ETL bugs, both fixed at the source and then re-run against HYG. Star ids,
ordering and positions are all unchanged, so stars.bin is byte-identical and
every exoplanet cross-reference still resolves.
Names. HYG's `gl` column already carries its own catalogue prefix ("Gl 581",
"GJ 3512"), unlike the bare numbers in `hd` and `hip`, so prefixing it again
produced 2331 of 8750 stars named "Gl GJ 1076". That corrupted three surfaces at
once: search, the on-screen labels, and exoplanet host-star name matching, which
compares normalised names and could never match "glgj1076" to "gj1076".
Colours. `Number(row['ci']) || 0` cannot tell a blank cell from a real zero, and
0 is a real B-V colour index meaning a hot blue-white A-type star. All 875
affected stars turned out to be blanks — the catalogue contains no genuine zero
inside the distance cutoff — so several hundred red dwarfs were rendering
blue-white. colorIndex is now `number | null` rather than defaulted, because any
numeric default is indistinguishable from a measurement.
Consumers resolve the gap from the spectral type instead. That needs real
parsing: HYG's `spect` column runs to 134 distinct spellings among the affected
stars alone, including a bare lowercase "m" for 354 of them, plus "k-m" ranges,
"dM4" luminosity prefixes and "K:" uncertainty flags. 622 of the 875 recover a
class this way — 497 of them M-class — and the remaining 253, which carry no
classification at all, fall back to neutral white.
The parse is anchored at the start of the string rather than scanning it. A scan
is the obvious implementation and is quietly wrong: the ETL writes the literal
"Unknown" for unclassified stars, that contains a K, and every one of those 253
would have been classified as an orange K-type. A test covers it.
Also lifts parseOptionalNumber out of fetchExoplanets into lib/csv, where both
fetchers now use it, and gives magnitude a faint default instead of 0 — no
current star is affected, but 0 would mean "as bright as Vega" and render an
unphotometered star as one of the largest points on the map.
Tests: 145 passing, up from 116, including the first coverage of
StarFieldRenderer. Build, both typechecks and the Playwright suite are green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
star-map
An interactive 3D star map in the spirit of Star Citizen's in-game starmap, but populated with real astronomical data instead of fictional systems. Browse the solar neighbourhood, fly into a star's system to see its planets on their real orbits, and drill into a single body for the NASA figures behind it.
This repo also hosts a small Claude Code plugin marketplace — see Plugins below.
Running it
npm install
npm start # dev server on http://localhost:4200
npm run build # production bundle into dist/
Requires the Node version in package.json's Angular toolchain range (Node 22.22.3+ or 24.15+).
npm test # unit/component tests (Vitest, jsdom)
npm run e2e # end-to-end tests (Playwright + Chromium) — see e2e/README.md
npm run etl # refresh the astronomical datasets — see below
npm run etl:typecheck # type-check the ETL scripts (they build separately from the app)
npm run e2e:typecheck
What's in it
Galaxy view — every HYG-catalogue star within 50 parsecs as a point field, positioned from real RA/Dec/parallax, coloured by spectral index and sized by magnitude. Names label the stars nearest the camera. Behind them sits a backdrop of notable deep-sky objects and a Milky Way panorama.
System view — selecting a star flies the camera continuously into its system rather than cutting to a new scene. The Sun gets the real solar-system bodies from JPL Horizons; other stars get their confirmed exoplanets. Orbits are drawn as ellipses and bodies are propagated along them by a Kepler solver against the current epoch.
Body detail — a dedicated close-up scene and info panel for one planet, moon or exoplanet, with real photography where NASA/ESA/USGS imagery exists.
Search — name search across stars, solar-system bodies and exoplanets, navigating to the same place an in-scene click would.
Architecture notes
- Rendering runs on Three.js
WebGPURenderer, which falls back to a WebGL2 backend automatically. The render loop runs outside Angular's change detection. - Two coordinate scales. The galaxy view works in parsecs and the system view in AU — about eight orders of magnitude apart, which wrecks float precision if rendered in one unit space. The camera rig recentres the active star to the origin ("floating origin") and swaps the unit scale and near/far planes at the transition point.
- No backend. Every dataset is baked at build time into
src/assets/data/and served as a static asset. Nothing queries an astronomy API at runtime.
Data pipeline
npm run etl runs tools/etl/build.ts, which fetches each source, writes the static assets,
then validates the combined output. Raw responses are cached under tools/etl/.cache/, so
re-runs are cheap and offline-friendly; set ETL_FORCE_REFRESH=1 to bypass the cache.
| Script | Source | Output |
|---|---|---|
fetchStars.ts |
HYG database (Hipparcos/Yale/Gliese) | stars.bin, stars-index.json |
fetchSolarSystem.ts |
JPL Horizons / SSD | bodies.json |
fetchExoplanets.ts |
NASA Exoplanet Archive (TAP) | exoplanets.json |
fetchDeepSky.ts |
OpenNGC | deepsky.json |
Star positions ship as a packed Float32Array (stars.bin) rather than JSON to keep the
initial payload and parse cost down; stars-index.json carries everything else in the same
order.
ETL_STAR_DISTANCE_PC (default 50) sets the star-field distance cutoff.
On deep-sky distances
OpenNGC publishes no distance column, so distance has to be inferred — and the inference fails for precisely the best-known objects. M31, M33 and M42 are Local Group members whose redshift is negative or absent, and the catalogue's parallax for a galaxy comes from a cross-matched foreground star (it lists 6 mas for M31, implying 167 pc for something 780,000 pc away).
So deep-sky records store a unit direction on the celestial sphere rather than a position:
the line of sight is always known precisely, and the objects are drawn as a fixed-radius
backdrop shell where true distance would be unusable anyway. distancePc is optional metadata,
derived from parallax for galactic objects or the Hubble law for genuinely distant galaxies,
and left null — with its distanceMethod — whenever neither is trustworthy. Roughly 330 of
the 463 cataloged objects get a distance; the rest honestly report none.
Layout
src/app/
core/engine/ Three.js renderer, render loop, resize
core/data/ static-asset loading and caching
features/galaxy-system/ shared galaxy+system scene, camera rig, star field,
deep-sky backdrop, orbits, labels
features/body-detail/ close-up scene and info panel
features/search/ name search across every dataset
shared/astro/ coordinates, Kepler propagator, deep-sky classification
shared/models/ record contracts shared by the app and the ETL
shared/rendering/ skybox, glow sprites, texture catalog
shared/state/ navigation store (Angular signals)
tools/etl/ build-time data pipeline
e2e/ Playwright end-to-end tests
The design document behind all of this is .junie/plans/nasa-star-map.md.
Plugins
This repo doubles as a Claude Code plugin marketplace. Adding it and installing a plugin defaults to user scope, meaning the plugin becomes available in every project on your machine, not just the one you happen to be in:
/plugin marketplace add avalon-vanguard/star-map
/plugin install caveman@star-map
Scope can be overridden at install time if you want it tied to a single repo instead:
# Shared with collaborators via that repo's .claude/settings.json
/plugin install caveman@star-map --scope project
# Just for you, in that one repo only (gitignored)
/plugin install caveman@star-map --scope local
See Claude Code plugin installation scopes
for details on user / project / local scope.
- caveman —
/cs:cavemanultra-compressed communication mode.- Command:
commands/cs/caveman.md - Agent:
agents/cs-caveman-mode.md - Skill:
skills/caveman/SKILL.md
- Command:
Data credits
Star catalogue: HYG database (Hipparcos, Yale
Bright Star, Gliese). Solar-system ephemerides: NASA/JPL Horizons. Exoplanets: NASA Exoplanet
Archive. Deep-sky objects: OpenNGC. Body and skybox
imagery: NASA/JPL/USGS public domain and Solar System Scope (CC BY 4.0) — per-file provenance
is recorded in src/app/shared/rendering/texture-catalog.ts.