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

@gcu/spinifex

v0.1.0

Published

Web GIS toolkit: map rendering, GeoJSON / Shapefile / GeoTIFF / CSV loaders, SRTM elevation tiles with IndexedDB cache, drawing interactions, DEM sampling/profiles, GDAL Wasm processing (contour, slope, aspect, hillshade). Ships vendored OpenLayers 10 + p

Readme

spinifex

Web GIS for auditable notebooks. Map rendering, geodata loaders, SRTM elevation, drawing interactions, GDAL processing. Built on OpenLayers 10, runs entirely in the browser.

const { spx } = await load("./ext/spinifex/index.js");

const map = spx.map("#map", { center: [-43.5, -20.25], zoom: 10, basemap: "topo" });
const dem = await spx.srtm(map, [-44, -21, -43, -20]);
const elev = dem.data.sample(-20.5, -43.5);  // bilinear elevation lookup

~1,260 lines of source across 11 modules. Bundles to a single index.js (~507 KB including vendored OL + proj4).


API

spx.map(target, opts?)

Create an interactive map.

const map = spx.map("#map", {
  center: [-43.5, -20.25],  // [lon, lat]
  zoom: 10,
  basemap: "topo"            // "topo" | "osm" | "satellite"
});

Options:

| Option | Default | Description | |--------|---------|-------------| | center | [0, 0] | [lon, lat] initial center | | zoom | 2 | Initial zoom level | | basemap | "topo" | "topo" | "osm" | "satellite" or custom tile URL | | scalebar | true | Show scale bar (metric by default) | | scaleUnits | "metric" | "metric" | "imperial" | "nautical" | | coordinates | true | Show lat/lon at cursor position |

Returns a map wrapper with:

| Property/Method | Description | |----------------|-------------| | bounds | [west, south, east, north] in WGS84 (getter) | | center | [lon, lat] (getter/setter) | | zoom | number (getter/setter) | | fitBounds(bbox, padding?) | Fit view to extent | | setBasemap(name) | Swap tile source | | exportImage(type?) | Export map as data URL (default "image/png") | | measure(type?) | Interactive measurement (see below) | | ol | Raw OpenLayers map instance |

spx.srtm(map, bounds?, opts?)

Download SRTM GL1 elevation tiles (30m resolution) from AWS S3, mosaic them, render as a terrain layer.

const dem = await spx.srtm(map, [-44, -21, -43, -20], {
  name: "SRTM",
  onProgress: (tileName, i, total) => console.log(`${tileName} (${i}/${total})`)
});

Bounds can be: [west, south, east, north] array, a layer with .bounds, or omitted to use the map's current view.

Returns a SpxLayer with DEM methods attached:

| Property/Method | Description | |----------------|-------------| | data | DEM instance | | sample(lat, lon) | Bilinear elevation lookup, returns meters or null | | profile(latA, lonA, latB, lonB, opts?) | Cross-section extraction | | width, height | Grid dimensions | | min, max | Elevation range (meters) | | bbox | [west, south, east, north] |

Profile result:

const p = dem.data.profile(-20.3, -43.8, -20.5, -43.2, { samples: 500 });
// p.dist  — Float64Array, cumulative distance in meters (Haversine)
// p.elev  — Float32Array, sampled elevations
// p.lat   — Float64Array
// p.lon   — Float64Array

Tile caching: tiles are automatically cached in IndexedDB (spx-srtm database) as raw gzipped bytes (~2.5 MB each). Subsequent loads skip the network fetch. See Cache management below.

Limits: max 16 tiles per request. Each tile covers 1\u00b0\u00d71\u00b0 at 3601\u00d73601 pixels.

spx.csv(map, source, opts?)

Load CSV with auto-detected coordinate columns.

const layer = await spx.csv(map, "data.csv", { name: "Samples" });
// layer.data — parsed rows as array of objects

Detects columns named lon/longitude/lng/x/easting and lat/latitude/y/northing. Falls back to data-only layer (no map geometry) if coordinates aren't found. Source can be a URL, text string, File, or Blob. Uses PapaParse (lazy-loaded from CDN).

spx.geojson(map, data, opts?)

Load GeoJSON.

const layer = spx.geojson(map, geojsonObject, { name: "Boundary", crs: "EPSG:4326" });

spx.shp(map, source, opts?)

Load Shapefile.

const layer = await spx.shp(map, file, { name: "Geology" });

Source: ArrayBuffer, File, Blob, or URL. Uses shpjs (lazy-loaded from CDN).

spx.tif(map, source, opts?)

Load GeoTIFF (supports Cloud-Optimized GeoTIFF streaming).

const layer = await spx.tif(map, "https://example.com/dem.tif");

Renders as WebGL tile layer. Source: URL string, ArrayBuffer, File, or Blob.

spx.load(map, file)

Auto-detect format by extension and load.

const layer = await spx.load(map, file);

Supports .csv, .geojson/.json, .shp/.zip, .tif/.tiff. Falls back to GDAL for unrecognized formats (if GDAL is loaded).

spx.draw(map, type, opts?)

Interactive drawing. Returns a promise that resolves when the user finishes.

const pt = await spx.draw(map, "point");
// pt.coord — [lon, lat]

const line = await spx.draw(map, "polyline", { maxPoints: 2 });
// line.coords — [[lon, lat], ...]

const poly = await spx.draw(map, "polygon");
// poly.coords — [[lon, lat], ...]

const rect = await spx.draw(map, "rectangle");
// rect.extent — [west, south, east, north]

map.exportImage(type?)

Export the current map view as a data URL. Composites all visible layers.

const dataUrl = await map.exportImage();          // PNG
const jpegUrl = await map.exportImage('image/jpeg');

// Display inline
const img = document.createElement('img');
img.src = dataUrl;
ui.display(img);

map.measure(type?)

Interactive distance or area measurement. Returns a promise that resolves when the user finishes drawing.

const m = await map.measure('distance');
// m.distance — total distance in meters
// m.unit — 'm'
// m.coords — [[lon, lat], ...]
// m.layer — the drawn feature layer (call m.layer.remove() to clean up)

const a = await map.measure('area');
// a.area — area in m²
// a.unit — 'm²'
// a.coords — [[lon, lat], ...]
// a.layer — the drawn feature layer

The measurement line/polygon stays on the map with a dashed amber stroke. Remove it with m.layer.remove() when done.

spx.proj(code, def)

Register a custom CRS definition.

spx.proj("EPSG:31983", "+proj=utm +zone=23 +south +datum=WGS84 +units=m");

GDAL processing

Full GDAL via Wasm (gdal3.js@2), lazy-loaded on first use. Unlocks contour generation, slope/aspect/hillshade, and universal format support for anything GDAL can read.

spx.gdal()

Pre-load the GDAL Wasm runtime (~5 MB). Called automatically by other GDAL functions on first use.

await spx.gdal();

spx.load(map, file) with GDAL fallback

When spx.load() can't match the file extension to a built-in loader, it falls back to GDAL. GDAL auto-detects vector vs raster, converts internally (ogr2ogr to GeoJSON, gdal_translate to GeoTIFF), and returns a layer.

const layer = await spx.load(map, file);   // .gpkg, .kml, .gdb, .ecw, etc.

spx.contour(dem, opts?)

Generate contour lines from a DEM as GeoJSON.

const geojson = await spx.contour(dem.data, {
  interval: 100,      // contour interval in meters (default: 100)
  attribute: "elev"   // elevation attribute name (default: "elev")
});
// geojson — standard GeoJSON FeatureCollection of LineStrings

Display on the map:

const contourLayer = spx.geojson(map, geojson, { name: "Contours" });

spx.slope(dem)

Compute slope raster from a DEM. Returns raw GeoTIFF bytes as ArrayBuffer.

const slopeBuf = await spx.slope(dem.data);
const slopeLayer = await spx.tif(map, slopeBuf, { name: "Slope" });

spx.aspect(dem)

Compute aspect raster from a DEM. Returns raw GeoTIFF bytes as ArrayBuffer.

const aspectBuf = await spx.aspect(dem.data);
const aspectLayer = await spx.tif(map, aspectBuf, { name: "Aspect" });

spx.hillshade(dem, opts?)

Compute hillshade raster from a DEM. Returns raw GeoTIFF bytes as ArrayBuffer.

const hsBuf = await spx.hillshade(dem.data, { azimuth: 315 });
const hsLayer = await spx.tif(map, hsBuf, { name: "Hillshade" });

How it works

DEM processing functions build a minimal GeoTIFF in memory (raw Float32 data with TIFF IFD headers), pass it to GDAL Wasm, and return the output. This means any DEM object from spx.srtm() can be processed directly — no file I/O, no server, everything in-browser.

Cache management

SRTM tiles are cached in IndexedDB to avoid re-downloading.

await spx.cache.list()            // [{key: 'S21W044', size: 2621440, ts: 1709654400000}, ...]
await spx.cache.size()            // {count: 3, bytes: 7864320}
await spx.cache.delete('S21W044') // remove one tile
await spx.cache.clear()           // remove all tiles

Layers

All loaders return a SpxLayer wrapping an OpenLayers layer.

layer.show()
layer.hide()
layer.remove()
layer.zoomTo()
layer.visible      // getter
layer.bounds       // [west, south, east, north] or null
layer.name         // string
layer.type         // 'csv' | 'geojson' | 'shp' | 'tif' | 'srtm'
layer.data         // raw data (rows, GeoJSON, DEM, etc.)
layer.ol           // raw OpenLayers layer instance

A floating layer panel appears automatically when layers are added (toggle visibility, double-click to zoom).


DEM

The DEM class handles elevation sampling and profile extraction.

const dem = new DEM(float32Array, width, height, [west, south, east, north]);
dem.sample(lat, lon)                             // bilinear interpolation, null if OOB
dem.profile(latA, lonA, latB, lonB, {samples})   // cross-section
dem.min, dem.max                                 // elevation range (excluding nodata)

Nodata value is -9999. Rendering uses a 7-stop terrain color ramp (green \u2192 brown \u2192 white) with hillshade. Downsamples to 2048\u00d72048 max for canvas rendering.


Architecture

Modules

src/
  main.js       11 lines    module loader (import order)
  api.js        26 lines    public spx object
  deps.js       26 lines    lazy CDN loading (PapaParse, shpjs)
  proj.js        6 lines    projection registration
  render.js    118 lines    styling + DEM terrain canvas
  map.js        97 lines    map creation & control
  layers.js    106 lines    SpxLayer wrapper + layer panel UI
  loaders.js   240 lines    CSV, GeoJSON, Shapefile, GeoTIFF
  srtm.js      263 lines    SRTM tiles + IndexedDB cache
  dem.js        91 lines    DEM sampling & profiles
  draw.js       62 lines    drawing interactions
  gdal.js      216 lines    GDAL Wasm loader & DEM tools

Dependencies

| Dependency | Version | Loading | |-----------|---------|---------| | OpenLayers | 10.5.0 | Vendored, tree-shaken | | proj4 | 2.15.0 | Vendored | | PapaParse | 5.x | Lazy-loaded from CDN (CSV) | | shpjs | 6.x | Lazy-loaded from CDN (Shapefile) | | gdal3.js | 2.x | Lazy-loaded from CDN (fallback formats) |

Build

node ext/spinifex/build.js    # bundle src/ into index.js

The build concatenates ES modules in import order, strips import/export statements, and prepends a vendored OL + proj4 bundle. The vendored bundle is built separately via vendor/build.js (Rollup + terser).


Styling

GCU aesthetic: amber (#c89b3c) accent for points, lines, and polygon fills. Default styles are dispatched by geometry type. Terrain rendering uses a 7-stop gradient from forest green (low elevation) through brown to white (high elevation), alpha-blended with a simple hillshade.


Testing

54 tests in test/spinifex.test.mjs. Run with node --test test/spinifex.test.mjs.

  • DEM — constructor min/max with nodata, sample() bilinear interpolation (corners, center, between pixels, OOB, nodata fallback), profile() array lengths, distance monotonicity, Haversine accuracy, default sample count
  • SRTMtileKey() all quadrants (N/S/E/W, zero padding, negative zero), tileUrl() AWS S3 URL structure, tilesForBbox() single/multi-tile/fractional/equator-crossing/empty
  • TerrainterrainColor() at ramp stops (0, 0.5, 1), clamping, interpolation between stops
  • LoadersdetectColumn() with lon/latitude/easting/northing/x/y/coordx variants, case insensitivity, missing columns, whitespace
  • Haversine — zero distance, 1° lat/lon at equator, longitude at 60°N, antipodal, symmetry

Roadmap

Infrastructure

  • ~~Tests for pure-math functions~~ done
  • ~~Add spinifex to CLAUDE.md project structure and build checklist~~ done
  • Build-time size tracking

Feature interaction

  • Click/hover on features to inspect attributes (popup/tooltip)
  • Attribute table for loaded vector data
  • Feature selection by click, box, or polygon
  • Style-by-attribute: color/size features based on column values
  • Custom style functions passed to loaders

Map controls

  • ~~Coordinate display on mouse move~~ done
  • ~~Scale bar~~ done
  • ~~Map export as PNG/JPEG~~ done (map.exportImage())
  • ~~Measurement tools (distance, area)~~ done (map.measure())

Tile management

  • LRU eviction policy for IDB cache (configurable max size)
  • Cache age expiry
  • Pre-fetch tiles for visible extent

Data formats

  • KML/KMZ loader (native, without GDAL)
  • GPX loader (native, without GDAL)
  • WMS/WMTS layer support
  • Vector tile support (MVT)

DEM processing

  • Pure-JS slope/aspect/hillshade (lightweight alternative to GDAL for simple cases)
  • Viewshed analysis
  • Watershed delineation
  • Volume calculation between surfaces
  • Configurable color ramps for DEM rendering

Drawing

  • Edit/modify drawn features after creation
  • Snap to existing features
  • Drawing undo/redo
  • Export drawn features as GeoJSON

Integration

  • Bridge to gslib/atra for geostatistical workflows on DEM data
  • Spatial query: select features by extent, polygon, or buffer