npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

terrella

v0.4.0

Published

An interactive globe for the web. Highlight countries and regions, place markers, choose a render style.

Readme

terrella

An interactive globe for the web. Highlight countries and regions, place markers, draw arcs, colour countries by number, and choose how it looks. One API, three outputs: a small 2D canvas, a real sphere with WebGPU, and static SVG from a server.

A terrella is a small model of the Earth. William Gilbert built one in 1600 to study magnetism, which is roughly what this is.

npm install terrella
import { createGlobe } from "terrella";
import { world } from "terrella/world";

const globe = createGlobe(document.querySelector("#globe"), {
  world,
  regions: [{ id: "sea", name: "Southeast Asia", countries: ["PH", "ID", "VN"] }],
  markers: [{ name: "Manila", coords: [121.0, 14.6], timezone: "Asia/Manila" }],
  onCountryClick: (country) => console.log(country.name),
});

globe.focus("sea");

Drag to spin, flick to throw, hover a marker for its local time, hover a country for its name, arrow keys to turn.

Why this exists

Most lightweight globe libraries do markers and arcs but cannot highlight a country. The ones that can are built on three.js and cost hundreds of kilobytes. terrella is the middle: countries, regions, choropleths and labels on a 2D canvas with d3-geo doing the projection maths, and the same options on a sphere when you want one.

The fiddly parts are the same in every hand-rolled globe, and they are what this actually solves:

  • Country ids. The atlas uses ISO numeric codes; you have "NG". Both work.
  • Markers that hide as they pass the horizon, because an orthographic projection happily draws the far side onto the near side.
  • A drag that feels like a throw rather than a jump, and a camera that glides rather than cuts.
  • Not animating at all when someone has asked their machine to stop moving.
  • A globe that reads on a dark page as well as a light one.

Sizes

| Build | Gzipped | | --- | --- | | terrella (ESM, d3-geo external) | 20 KB | | terrella/world (the bundled atlas) | 39 KB | | terrella/three | three.js is a peer dependency, on top | | dist/terrella.global.js (everything, one script tag) | 70 KB |

Naming countries

Anywhere a country is named, use an ISO 3166-1 numeric code (4, "4" or "004"), an alpha-2 code ("NG") or an alpha-3 code ("NGA"). Case does not matter. Whole continents and UN sub-regions come from countriesIn:

import { countriesIn, country, countryName } from "terrella";

countriesIn("Africa");                 // 60 numeric ids
countriesIn("South-eastern Asia");
country("NG");                         // { id: "566", alpha2: "NG", name: "Nigeria", region: "Africa", ... }
countryName(566);                      // "Nigeria"

The atlas

Pass any TopoJSON with a countries object, or use the bundled one (world-atlas countries-110m, 39 KB gzipped):

import { world } from "terrella/world";
createGlobe(el, { world });

// or once, for every globe afterwards
import "terrella/world/register";
createGlobe(el, { regions });

The browser build registers it for you, so a page with one script tag needs nothing else.

Regions and countries

regions: [
  {
    id: "africa",
    name: "Africa",
    countries: countriesIn("Africa"),
    highlight: ["NG", "KE", "ZA"],   // painted in the highlight colour
    color: "#a9cdec",                // optional, else the palette's region colour
  },
]

focus("africa") glides the camera to the region's centroid and shows only that region. focus(null) releases it. Countries under the pointer are reported and lit:

createGlobe(el, {
  world,
  hoverCountries: true,                  // implied by either callback
  onCountryHover: (country) => ...,      // null when the pointer leaves land
  onCountryClick: (country, event) => ...,
});

Values

A number per country becomes a choropleth. The default ramp runs from the land colour to the highlight; give a scale for anything else.

createGlobe(el, {
  world,
  values: { NG: 213, ET: 120, EG: 109, CD: 99 },
  scale: { range: ["#edf4fb", "#1769a8"] },     // or (value) => colour
});

globe.setValues({ NG: 220 });

Markers, arcs, labels

markers: [{ name: "Manila", coords: [121, 14.6], timezone: "Asia/Manila", color, size }],
arcs: [{ from: [3.4, 6.5], to: [-46.6, -23.5] }],
labels: true,   // or { markers: true, countries: "regions" | "all" | ["NG", "KE"] }

Labels sit at each country's centroid, hide past the horizon, fade toward the limb, and are drawn with a halo so they read on any palette.

The camera

await globe.focus("sea");                     // glides over 900 ms
await globe.focus("sea", { duration: 0 });    // jumps
await globe.flyTo([121, 14.6]);               // face a coordinate
await globe.flyTo({ longitude: 20, tilt: -10 });

const tour = globe.tour(
  [{ region: "africa" }, { at: [121, 14.6] }, { region: "latam", dwell: 4000 }],
  { dwell: 2500, loop: true },
);
tour.stop();

Dragging cancels a glide in progress. Under reduced motion every move is a cut.

Styles

Seven built in. Pass style at construction or call setStyle later.

| Style | What it draws | | --- | --- | | solid | Flat country fills with hairline seams. The default. | | dots | Land as a field of dots, fading toward the limb. | | wireframe | Coastlines over a graticule. | | hatched | Diagonal rules clipped to the land: the engraved map. | | pixel | Blocks on a coarse grid. | | ascii | Characters, solid at the centre and dissolving at the limb. | | stipple | Jittered dots of varying weight: the engraver's hand. |

Write your own by passing an object. prepare runs once and whatever it returns is handed to every paint:

globe.setStyle({
  name: "my-style",
  prepare: ({ land, countries }) => ({ /* expensive work, once */ }),
  paint: (frame, state) => {
    frame.ctx.fillStyle = frame.palette.land;
    frame.ctx.beginPath();
    frame.path(frame.land);
    frame.ctx.fill();
  },
});

A style written once works in every output: the 2D canvas, the sphere's texture, and the SVG renderer.

Colour and theme

Nine colours, any subset. Three layers, each overriding the last: a theme preset, --terrella-* custom properties on the element, and palette.

createGlobe(el, { world, theme: "auto" });   // follows the system, live
globe.setTheme("dark");
globe.setPalette({ highlight: "#d9481b" });
#globe { --terrella-land: #d3d0c5; --terrella-highlight: #d9481b; }

Palette keys: ocean, land, border, region, highlight, marker, markerRing, rim, arc, and optionally dot, outline, hover, label.

Day and night

createGlobe(el, { world, terminator: true });
createGlobe(el, { world, terminator: { date: new Date("2026-06-21T12:00Z"), opacity: 0.4 } });

The night side follows the real sun and moves once a minute.

Projections

globe.setProjection("naturalEarth");   // "orthographic" | "naturalEarth" | "equirectangular"

Three renderers, one API

import { createGlobe } from "terrella";          // 2D canvas
import { createGlobe } from "terrella/three";    // a sphere: WebGPU, WebGL2 elsewhere
import { renderSVG } from "terrella/svg";        // a string, anywhere

The three.js renderer takes the same options plus atmosphere, atmosphereColor, background, zoom and textureSize, and returns the same instance plus scene, camera, renderer and controls. Every style is drawn into an equirectangular texture by the same painters, so a style written for one works on the other. It cannot change projection: a sphere is the projection.

renderSVG needs no browser:

const svg = renderSVG({ world, regions, markers, labels: true, longitude: 20, width: 800 });

Every live instance also has toSVG(), which renders exactly what is on screen.

React

import { Globe } from "terrella/react";
import { world } from "terrella/world";

<Globe world={world} regions={regions} markers={markers} globeStyle="dots" focus="sea" />

Every option is a prop; the render style is globeStyle because style is CSS in React. Props with a setter change the globe in place, the rest rebuild it. For server rendering, pass the SVG as fallback and the canvas replaces it on mount:

<Globe world={world} regions={regions} fallback={renderSVG({ world, regions })} />

HTML element

For pages with no build step, or tools where an element is the only thing you can write:

<script src="https://unpkg.com/terrella/dist/terrella.global.js"></script>

<terrella-globe
  style-name="dots" theme="auto" labels terminator
  regions='[{ "id": "sea", "countries": ["PH", "ID", "VN"] }]'
  markers='[{ "name": "Manila", "coords": [121, 14.6] }]'
></terrella-globe>

<script>
  document.querySelector("terrella-globe")
    .addEventListener("terrella:countryclick", (e) => console.log(e.detail.name));
</script>

Arrays are JSON in attributes or properties set from a script. Events: terrella:markerhover, terrella:markerclick, terrella:countryhover, terrella:countryclick. The instance is on .globe.

Accessibility

On by default. The canvas takes focus and the arrow keys turn it (shift for bigger steps, Home to reset). A visually hidden block lists every region's countries and gives every marker a button that turns the globe to it. Set accessible: false if the page describes the globe itself.

Instance

| Call | What it does | | --- | --- | | focus(id, { duration }) | Glide to a region and hold. null releases. | | flyTo(coords \| { longitude, tilt }, { duration }) | Face a point and hold. | | tour(stops, { dwell, loop }) | Visit stops in turn. Returns { stop, finished }. | | setStyle(style) | A name, or your own painter. | | setProjection(name) | 2D only. | | setPalette(colors) | Merge new colours in. | | setTheme(theme) | Swap the preset and re-read CSS custom properties. | | setMarkers(list) | Replace the markers. | | setValues(values, scale) | Replace the choropleth. null clears. | | setLabels(options) | Switch labels on, off, or change them. | | setTerminator(options) | Switch the night side on, off, or move it. | | setSpin(degreesPerSecond) | Ambient rotation. Zero holds still. | | longitude | The longitude at the centre now. | | toSVG({ width }) | What is on screen, as SVG. | | destroy() | Stop the loop and remove everything. |

Options

| Option | Default | | | --- | --- | --- | | world | the registered default | TopoJSON with a countries object | | style | "solid" | | | projection | "orthographic" | | | theme | "light" | "light", "dark", "auto" | | palette | | any subset of the nine colours | | regions, markers, arcs, values, scale | | see above | | labels, terminator | off | | | hoverCountries | on when a country callback is given | | | spin | 3.2 | degrees per second | | tilt, longitude | -14, 18 | the starting view | | ratio | 1 | canvas height as a multiple of width | | radius | 0.46 | sphere radius as a fraction of width | | draggable, tooltips, accessible | on | | | pulseMs | 1600 | marker pulse period; 0 disables | | respectReducedMotion | on | | | dotSpacing, dotSize | 2.2, 1.1 | for the sampled styles | | label, locale | | aria-label override; clock locale | | onMarkerHover, onMarkerClick, onCountryHover, onCountryClick | | |

Data

data/countries-110m.json is world-atlas 110m (Natural Earth, public domain). data/iso3166.csv is the ISO 3166-1 table with UN M49 regions; npm run iso3166 regenerates src/data/iso3166.ts from it.

Seeing it without installing anything

preview.html at the repo root is a single self-contained file: open it in a browser and it runs, with no server, no build and no network. One globe is pinned on the page and each step of the copy drives it with a real call.

It is generated. Edit demo/preview.template.html and run:

npm run preview

The page chrome lives in demo/demo.css and the regions, markers and palettes in demo/demo-data.js; both are inlined by the build and shared with the two development pages under demo/, which do need a build and a local server. Fonts are the one thing fetched from the network; offline, the page falls back to the system fonts and everything else still works.

Development

npm install
npm test          # vitest
npm run typecheck
npm run lint
npm run build
npm run demo      # then open http://localhost:8080/demo/

Licence

MIT. Country data is public domain (Natural Earth) and CC BY-SA 4.0 (the ISO table's regional codes).