Interactive celestial map for the browser: stars, constellations, deep-sky objects, the Milky Way and planets, in 67 map projections — drawn to canvas, exportable to SVG.
The upstream library is pinned to D3 v3, last released in 2022. The whole
rendering path was migrated, and D3 now ships inside the bundle — no global
d3, no separate plugin script.
The map is a class now, so independent instances can live side by side, each
with its own settings form. The old global Celestial.display()
still works unchanged.
ESM, CJS and a browser IIFE build, tree-shakeable, with hand-written TypeScript declarations for the public surface.
The Milky Way no longer inverts near the edge of the globe, star colours are right again, and a set of silent v3→v7 breakages — SVG export, resize, hour angle — were caught and repaired.
A migration is only worth anything if the picture does not move. Every
(right ascension, declination, projection, rotation) is deterministic,
so the old library's output was pinned and compared against the new one.
| measurement | result |
|---|---|
| projections × rotations × sky points | 67 × 4 × 413 = 110 684 |
| maximum difference from the D3 v3 original | 0.000 px |
| rendered images, old vs new | differ only at anti-aliasing level |
| unit tests · browser assertions | 66 · 27 |
| files to load (was 3: d3 + plugin + celestial) | 1 — 295 KB |
To show the sky above you, the library needs the UTC offset of your position. A browser cannot work that out for an arbitrary point on Earth — only for its own zone — so it takes an outside source.
Upstream baked the author's TimeZoneDB account id into the library, so every
page embedding it sent its visitors' coordinates to a third party, unasked,
on a shared quota. This fork ships no key: you hand it a
timezoneResolver and it calls that instead. “My sky now”
above uses one — it resolves against
a small service that
keeps the timezone polygons inside its own binary, so nothing is asked of
anyone else at runtime.
Celestial.display({
timezoneResolver: (lat, lon, when) =>
fetch(`https://api.blackit.hu/timezone?lat=${lat}&lon=${lon}&t=${when}`)
.then(r => r.json())
.then(j => j.offsetMinutes)
});
With neither a resolver nor a key of your own, no request is made at all and the offset is estimated from longitude — right to within an hour or so, except where zones follow politics rather than meridians.
import Celestial from "celestial-chart"; Celestial.display({ container: "celestial-map", projection: "aitoff", stars: { limit: 6, colors: true }, constellations: { names: true, lines: true } }); // or, for several independent maps on one page: import { SkyMap } from "celestial-chart"; const north = new SkyMap({ container: "map-a", center: [0, 90, 0] }); const south = new SkyMap({ container: "map-b", center: [0, -90, 0] });