Website of C4PO — the Collaboratory for Physical Oceanography at the Virginia Institute of Marine Science, live at oceansensing.org.
Built with Astro. Static output, and close to zero
client-side JavaScript — the exceptions are the theme toggle, the photo
shuffle and lightbox on observation pages, the asset map, and the UTC clock.
The map is the one heavyweight: gliders from four national data centers,
NOAA saildrones, the Argo array, animated currents at two depths,
sea-surface temperature and salinity, isobaths, a detailed coastline and EEZ
boundaries, and ECMWF 10 m wind and 2 m air temperature — with the color
scale, its range, the isobath opacity and every layer under the reader's
control. One button takes a reader's own data over the top of it — KML, KMZ,
GeoJSON or a shapefile, zipped or as loose parts — with the format worked out
from the bytes rather than the file name — from a file or from a pasted link,
where the host allows it. The ESPC fields show the step nearest the reader's own clock rather
than a fixed offset from the model run, the currents publishing the two
steps that span a six-hour window so the tide in that model can be stepped
through, and the map's attribution says which hour is drawn and how far from
now it is. The address bar carries the whole view as you move, so the URL is
a link to exactly what is on screen — with a Copy link button beside the
map, since selecting a URL out of a phone's address bar is an awkward
gesture. Beside it, one button saves the figure as a PNG — doubled for print,
with the color bar and the credit line baked in, so a figure dropped into a
talk still says where it came from.
It carries two pages. /visualization/
is the general-purpose one and
/observations/hurricanes/
is framed around the storm season. They run the same engine off the same data
and differ only in their preset — which layers open, and where. Adding
another such page means one .astro file passing layers and home to
AssetMap.
It lives in packages/ocean-map, an npm workspace
package rather than a page component, so it can be used elsewhere: see its
README,
EMBEDDING.md for another website, and
PORTING-IOS.md for the native app.
Anything added to it has to keep both of those viable —
BOUNDARIES.md says what that means.
A third page, /dev/visualization/,
runs a sandbox fork of the same engine —
packages/ocean-map-dev — against the same live
data. It is where an idea gets tried before it is held to the gates the real
map is held to, and it says so on the page. What is in it at the moment is a
polar stereographic view of either pole, with the longitude at the top of
the screen adjustable, because Web Mercator puts the pole at infinity and
draws the ice at the one aspect ratio that makes it unreadable.
Every push to main deploys automatically via GitHub Actions, and a scheduled
run rebuilds hourly to refresh the map data; the hurricane page picks that up
on its own without losing your place on the map.
There is also a
seawater calculator under Data
& Tools: every thermodynamic property at full TEOS-10 precision, evaluated
from the Gibbs function rather than from the 75-term polynomial, with the
Absolute Salinity anomaly applied from position. It runs entirely in the
browser. The physics is
packages/teos10 — another workspace package,
DOM-free and renderer-independent, checked against the GSW reference
implementation on every build.
There is also a
Slocum glider data decoder reads
the binary files a Slocum logs — sbd, tbd, mbd, nbd, dbd, ebd and
their compressed forms — and writes CSV, netCDF, or the OceanGliders
OG1.0 format. Seawater properties from the CTD are one checkbox. Nothing is
uploaded.
Drop a whole deployment folder, data and its cache directory, in either
order. Files from several gliders, or from one glider with a break of three
days or more, come out as separate deployments in separate files.
Two plots, and either will draw anything: X, Y and color take any column, each with its own limits, as a scatter, a line or both, in any of twenty color scales. Point size and plot size are yours, and each plot saves as a PNG. They differ only in what they open on — the dive on the whole record against time, the profile on a sensor against depth.
The files and your setup are kept in the browser, so a reload comes back to
where you were. Clearing the data leaves the caches and your plot setup
behind, so the next segment off the same glider comes up drawn the way the
last one was. The decoder itself is
packages/slocum, a port of
SlocumIO.jl checked against
dbdreader value for value.
Off the same engine there is a
glider ballast calculator:
how much lead to add, what the vehicle should then read in the tank, and
whether its buoyancy engine can cover the water it will fly in. The arithmetic
is packages/glider-ballast. The vehicle
numbers it ships are stand-ins and the page says so — replace them from your
own ballast sheet.
All content lives in Markdown and data files — you never need to touch layout code:
| To add… | Edit… |
|---|---|
| a publication | paste its BibTeX entry into src/data/publications.bib |
| a presentation | new entry in src/data/presentations.yaml (type: invited / conference / workshop / outreach) |
| a news post | new .md file in src/content/news/ |
| a person | new .md file in src/content/people/ |
| a research project | new .md file in src/content/projects/ |
| a past project | set status: completed in the project's frontmatter — the Research page moves it to a "Past projects" section automatically |
| a dataset | new entry in src/data/datasets.yaml |
| a software tool | new entry in src/data/software.yaml |
| a seawater property | one entry in packages/teos10/index.ts's build(), and a check in scripts/test-teos10.mjs — the label, unit and precision live with the physics so the screen, the clipboard and the CSV cannot disagree |
| a glider to the ballast calculator | one entry in packages/glider-ballast/vehicles.ts. Set illustrative: false only with a real ballast sheet in hand — that flag is what turns off the page's caution |
| a derived column to the Slocum decoder | one entry in packages/slocum/derive.ts, and a range check in scripts/test-slocum.mjs — derived columns are marked as such in both outputs, so a reader can always tell them from what the glider recorded |
| a significant observation | new .md file in src/content/observations/ — add map: assets for the live asset map, or surveys: entries for dated photo panels |
| a CV item | new entry in the matching src/data/cv/<person>/*.yaml file (grants, advising, service, …) — publications and presentations flow in automatically |
| a member's CV | new directory src/data/cv/<person-id>/ (id matching their file in src/content/people/) with any of the section files — their page appears at /cv/<person-id>/ |
Site title, navigation, contact info, and the list of author names bolded on
the Publications page live in src/config.ts. Colors, fonts, and spacing
live in src/styles/tokens.css.
npm install
npm run dev # dev server at localhost:4321
npm run build # production build into dist/
npm run verify # everything CI checks, in one commandnpm run verify builds, type-checks, checks the docs for drift, and runs the
map, multimap, calculator, decoder and clock test harnesses — over 1,400
assertions in all. CI runs the same command and refuses to deploy
if it fails, so running it before you push is the quickest way to find out
whether a change will publish.
The individual pieces, if you want one on its own:
npm run check # type-check
npm run check:docs # docs reference real scripts, real paths, the right URL
npm run test:units # the map's renderer-independent modules, with no DOM at all
npm run test:teos10 # the TEOS-10 package against GSW, against calculus, against physics
npm run test:ballast # the glider ballast arithmetic, against its own identities
npm run test:seawater # seawater calculator, against the built bundle
npm run test:ballast-page # glider ballast calculator, against the built bundle
npm run test:slocum # the Slocum decoder, against dbdreader's recorded answers
npm run test:slocum-page # Slocum decoder page, against the built bundle
npm run test:prose # built pages keep the spaces Astro likes to eat
npm run test:schema # every published data file against the contract in schema.ts
npm run test:contrast # map colors stay visible, and every conceded clash is named
npm run test:map # asset map, against the built bundle
npm run test:multimap # two maps on one page stay out of each other's way
npm run test:clock # UTC clock, against the built bundleThe test harnesses read from dist/, so build first or they test stale code.
Refreshing map data by hand — CI does all of this on every deploy, so you only need it locally when working on the map:
npm run data # storms, gliders (four regional ERDDAPs), USVs, Argo floats
npm run data:currents # global + regional current grids, surface and 60 m
npm run data:tiles # the 1/12° current tiles (~92 MB per depth, per forecast hour built)
npm run data:fields # global + regional sea-surface temperature and salinity grids
npm run data:wind # ECMWF 10 m wind — the one pipeline with a Python dependency
npm run data:field-tiles # native-resolution Navy field tiles (OISST needs none — its regions already are native)
npm run data:basemaps # re-sample basemap ocean colors (slow; GEBCO's WMS)The tiles are gitignored, so a fresh clone has none and the map simply uses the coarser grids until you build them.
data:wind is the exception to "no Python dependencies": ECMWF packs its open
GRIB2 with CCSDS/AEC, which the standard library cannot decode. Install it
with pip install -r scripts/requirements-wind.txt — binary wheels, no system
packages. Everything else runs on a bare Python 3.
Pushing to main deploys to GitHub Pages, gated on npm run verify — if the
checks fail, nothing is published. The same workflow runs hourly on a schedule
to refresh the map data; that run commits nothing, so the repository does not
grow.
The full-resolution tiles are the exception to "refresh everything hourly": the current tiles are ~92 MB per depth and the Navy temperature and salinity tiles another ~43 MB each, and none of them change until the ocean model runs, once a day at 12Z. CI caches them keyed on that model run and rebuilds only when it advances.
DNS lives at the registrar: apex A records to 185.199.108.153,
185.199.109.153, 185.199.110.153, 185.199.111.153, and a www CNAME to
oceansensing.github.io. The apex is canonical and HTTPS is enforced. The same
zone carries the lab's PrivateEmail MX records — do not let a registrar "Mail
Settings" default overwrite them, or lab email stops.
Working on this repo with Claude Code? CLAUDE.md has the architecture notes
and PLAN.md tracks what is still open.
Copyright (c) 2026 Donglai Gong and C4PO. All rights reserved — see LICENSE. The repository is public so the site can be served from GitHub Pages and so the work is open to inspection; that is not a grant of any license to reuse it.
Third-party material keeps its own terms: dependencies as declared in
package.json, and the scientific data — served from
ocean-data-repo — under the terms of
the bodies that produced it (GEBCO, Natural Earth, NOAA, IOOS, Ifremer, the
US Navy via HYCOM, Marine Regions and others), each credited on the map.