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

@sailingnaturali/chs-constituents

v0.2.1

Published

Fit tidal-current harmonic constituents from published predictions, in the browser or on a SignalK server

Readme

chs-constituents

Fit tidal-current harmonic constituents from Canadian Hydrographic Service predictions, so your chartplotter, SignalK server, or app can predict currents offline.

You run this yourself, for your own boat. That is not a stylistic choice — see Why you have to run this yourself.

npm install
npm run fit -- --only "Active Pass" --validate-from 2026-06-01

Why you have to run this yourself

In US waters this problem does not exist. NOAA publishes tidal-current harmonic constituents as public-domain data; anyone can bundle them into anything, commercial or not, and offline current prediction just works.

Canada does not do this. The CHS licence agreement you accept by using the IWLS API is a bespoke Crown licence, not the Open Government Licence. Three clauses matter:

  • Clause 3 — CHS copyrights "shall not be sold, licensed, leased, assigned or given to a third party."
  • Clause 4 — prohibits derivative products "for commercial purposes, for sale or profit under any form whatsoever."
  • Clause 10permits non-profit derivative products, provided you carry a prominent notice: your name, acknowledgment that the product contains CHS intellectual property, that Crown copyright is retained, that it is not to be used for navigation, and that CHS does not endorse it.

So clause 10 lets you derive constituents for your own non-commercial use, and clause 3 stops anyone from handing you a finished bundle. Hence a pipeline instead of a dataset. This repository contains no CHS-derived data and never will. Running it produces data on your machine that is yours to use and not yours to redistribute.

If that seems like a lot of ceremony to predict when the tide turns at Dodd Narrows: yes.

This isn't hypothetical

XTide shipped Canadian harmonic constants with explicit non-commercial notices attached. Debian packaged them separately as xtide-data-nonfree precisely because commercial distribution was not permitted. Its author was then contacted by what he understood to be the Department of Justice Canada asking him to strengthen the warnings; he stopped maintaining the non-US data in 2012, and XTide has shipped US-only ever since.

A published notice is not a substitute for a licence you were never granted. That is why this repo ships code.

There is no open alternative

Before building this, we checked whether any openly-licensed source publishes tidal current constituents for Canadian waters. None does:

| Source | Why not | |--------|---------| | Parker (1977), NOAA Tech Report NOS 69 | Genuinely publishes per-station current constants for 90 stations, and is US-federal public domain. But its coverage is 48.023°N–48.947°N — a search for any 49° or 50° latitude across all 62 pages returns nothing. Nearest station to Active Pass is 4.2 nm; to the Discovery Islands rapids, ~100 nm. Also only five constituents, with no overtides. | | WebTide / Foreman et al. (2000) | One source, not two. Eight astronomical constituents of gridded finite-element output; the mesh does not resolve a 200 m pass as a flow-carrying channel. Carries no licence statement at all. | | neaps tide-database, XTide | Tide height only, structurally — the schema has no field for flood/ebb axis, so a current station cannot be represented. | | NOAA Puget Sound Current Survey (2021) | Clean, public domain, 29 constituents per station — and stops at the border. | | Ocean Networks Canada | CC-BY 4.0 and genuinely independent of CHS, but its Salish Sea instruments sit in the open strait, not in the passes. |

For Canadian tidal gates, CHS is the only source. That is the whole reason this tool exists.


What it does

Per station:

  1. Fetch wcsp1 (speed) and wcdp1 (direction) continuous predictions from IWLS, in 7-day chunks, cached on disk.
  2. Project onto the flood axis: speed · cos(direction − floodDirection), giving a signed along-channel velocity. The projection is linear, so this is equivalent to a full 2D fit projected onto the same axis.
  3. Solve for harmonic constituents by least squares over a fixed basis, using the astronomy (speeds, V₀, nodal f/u) from @neaps/tide-predictor.
  4. Validate (optional) against CHS's own wcp1-events out-of-sample, and assign a confidence tier.

Output conforms to currents.schema.json.

Usage

# One station, validated — good for a first run (~2 minutes)
npm run fit -- --only "Active Pass" --validate-from 2026-06-01

# All bundled Salish Sea gates, validated
npm run fit -- --validate-from 2026-06-01 --output my-currents.json

# Shorter training window (faster, but see Accuracy below)
npm run fit -- --training-days 90 --validate-from 2026-06-01

| Flag | Default | Notes | |------|---------|-------| | --stations | the bundled station registry | Any [{id, label}] list of CHS current stations | | --training-days | 210 | Length of the fitted series; below 183 the fit cannot separate K1/P1 | | --training-start | 2025-07-01 | UTC start of the training series | | --validate-from | (off) | Enables out-of-sample validation and tiering | | --validate-days | 7 | Length of the validation window | | --request-interval | 2.5 | Seconds between requests | | --only | — | Substring match on label, repeatable | | --cache-dir | .cache | Cached chunks; safe to delete, expensive to refill |

How long it takes

About 52 requests per station — 26 seven-day chunks × 2 series — which at the 2 s default interval is roughly 1.7 minutes per station. A default run (~30 live current stations) takes under an hour; the 19 registry-named gates alone, about half.

Cached chunks are free, and chunks are anchored to a fixed 7-day grid rather than to your start date, so re-running with --training-days 90 after a 180-day run refetches nothing.

Start with the passes near you. Most boats never need Arran Rapids.

Accuracy

Accuracy is bounded by the source, not by the fit. CHS's continuous series (wcsp1) is 15-minute sampled, and its own continuous extrema disagree with its precise event series (wcp1-events) by 15–30 minutes at complex narrows. This tool fits the continuous series, so it inherits that floor. Magnitudes agree to ~0.01 kn; it is the timing that slips.

Measured against CHS's own event predictions, out-of-sample, across the Salish Sea gates. The headline median — and the tier — judge extremum timing only; slack timing is measured and reported separately (validationSlackMedianMin), because at weak, slow-reversing stations the zero crossing is far noisier than the peaks (Juan de Fuca East: ~17 min on extrema, ~83 min on slacks). Thresholds (the authority is TIERS in src/validate.ts):

| Tier | Median extremum-timing error | Character | |------|------------------------------|-----------| | high | ≤ 5 min | Clean reversing passes | | medium | ≤ 20 min | Complex narrows | | low | ≤ 35 min | Violent, nonlinear rapids | | quarantine | worse, or a reversed flood axis | Do not use |

The default training window is 210 days, set by the Rayleigh criterion: below 183 days the fit cannot separate K1 from P1 (which drives diurnal inequality) or S2 from K2, and below 206 it cannot separate N2/NU2 or 2N2/MU2. fit() reports what a given window cannot resolve.

Longer training does not help beyond that: 365 days measured no better than 180 at Dodd Narrows (49 min vs 45.5). If a gate is bad, it is bad because the physics there is nonlinear, not because the fit is underfed.

A reversed flood axis quarantines a station outright. That means the fit predicts flood where CHS says ebb — far more dangerous than a timing error, since the current is not merely mistimed but backwards. The test is the sign of the modelled velocity at CHS's own extremum times, and the bar is systematic disagreement (≥ 60% of extrema): a genuinely reversed axis is wrong at nearly every peak. Occasional wrong signs are timing error at a weak station, not a flip — an earlier nearest-event comparison conflated the two and false-quarantined three directionally-perfect stations (Tillicum Bridge and Calamity Point both measure 0 wrong signs under the sound test; see test_flip_detection_uses_sign_not_nearest_event).

Prefer live CHS data when you have a connection. This is an offline fallback, not a replacement. Always carry official CHS current tables.

Using the output

Attach the clause 10 notice wherever the data surfaces. The generated bundle carries it in its note field; if you display predictions in a UI, put it somewhere a user will actually see:

Contains information licensed under the Canadian Hydrographic Service licence agreement. Crown copyright is retained by His Majesty the King in Right of Canada. This product has not been endorsed by CHS. Not to be used for navigation.

And keep the output out of version control — .gitignore already excludes currents.json.

Gotchas

These cost real debugging time. They are in the code as comments too.

  • The phase reference must match the synthesis. V₀ is evaluated once at the series start and ωt carries it forward, exactly as @neaps/tide-predictor does. Reference it anywhere else and the fit still "succeeds" while returning collapsed amplitudes — silent and total. (The Python predecessor hit the same trap through utide's epoch argument.)
  • IWLS caps a request at 7 days and documents 3/sec and 30/min. The default 2 s interval stays inside the minute limit; going faster works until it doesn't, and a throttled API is everyone's problem.
  • Fit and synthesis must come from the same library. neaps and utide disagree by 180° on M3 and ~5° on 2N2, and at slow-reversing stations that moves slack by many minutes — see docs/validation/ts-port.md. Harmless while both ends of the round trip are neaps; not harmless if you mix engines.
  • floodDirection comes from CHS /metadata and is occasionally wrong for the actual channel axis, which is what produces label flips. Validation catches it; run with --validate-from.
  • Requires a residential IP; datacenter IPs get refused, and CHS has refused non-browser User-Agents too. The default UA stays an honest chs-constituents/1.0 on purpose — this tool doesn't masquerade by default. If your runs are refused despite a residential IP, pass a browser-ish string yourself: --user-agent on the CLI, userAgent in buildBundle.

Station lists

By default every live IWLS current station is fitted (~30 today). The 19 gates named in the shared @sailingnaturali/station-corrections registry get their curated names and stable keys from it; unmatched stations keep their official IWLS names. Station identity (which gates exist, what they're called) is curated once in that registry, not a copy kept in this repo, and read by everything that needs it. A registry station's public id is a stable key that survives label renames. That dependency carries identifiers and hand-written names, not CHS predictions or constituents, so depending on it does not cross the line drawn above. --stations <path> still takes a [{id, label}] file for anything the registry does not cover; the tool has nothing Salish Sea specific in it. The IDs are CHS's 24-hex station identifiers, which you can look up for any station through the IWLS API. A file-supplied station has no registry key, so its id is derived from its label instead (the fallback slug() in src/pipeline.ts).

Licence

Code is MIT. Output is not — see Why you have to run this yourself.