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

polar-math

v1.1.1

Published

Prepared polar-table interpolation and target calculations.

Readme

polar-math

Pure calculations for canonical polar-format polar tables. The package has no Signal K dependency.

Polar.fromTable(table) validates and prepares an immutable polar table once. All query methods accept their dynamic inputs explicitly and return { value, state }. Measurements use SI units: m/s and radians.

const { Polar } = require('polar-math')

const polar = Polar.fromTable(table)
const speed = polar.speedAt({ tws, twa, performanceFactor: 1.0 })
const vmg = polar.vmgAt({ tws, twa, performanceFactor: 1.0 })
const targets = polar.targetsAt({ tws, performanceFactor: 1.0 })
const range = polar.rangeAt({ tws })

state always includes available. For valid queries it also includes the TWS range state and, for angle-dependent calls, the TWA range state. Invalid numeric input (including a twa outside [-π, π]) returns value: null and state.reason: 'invalid_input'.

state.tws is one of:

  • in_range — within the table's TWS axis
  • below_range — below the lowest TWS row (result is clamped to it)
  • above_range — above the highest TWS row (result is clamped to it)

state.twa (angle-dependent calls only) is one of:

  • in_range — within the sailable TWA range
  • pinching — between the pinch boundary and the beat angle; a reduced positive speed is returned
  • in_irons — below the pinch boundary; speedAt/vmgAt return value: null
  • extrapolated — beyond the table's last real TWA point but within the modeled run extension
  • above_range — beyond the modeled range; speedAt/vmgAt return value: null

The performance factor is deliberately a per-query input and defaults to 1.0, meaning the polar's unadjusted performance. Values below 1.0 scale the calculated speed and target values down.

Interpolation and extrapolation

Within a TWS row, a query angle that falls between two real points (measured axis columns, plus any derived.rows beat/run target inserted into the same list) is interpolated using a monotone cubic Hermite spline (PCHIP) — smooth (continuous slope) and passing exactly through the input and synthetic curve points, but with a built-in constraint that it can never overshoot or oscillate between two points the way a plain cubic spline can. Between two TWS rows, results are still linearly interpolated (TWS rows are few and widely spaced, and are never rendered as a continuous curve themselves, so the simpler method is preferred there). Angles outside the real data span (below the beat angle, or beyond the last real point) fall back to the modeled extrapolation described below.

Beat-side (pinching)

Below the beat angle, speed is modeled down to a fixed 25° endpoint. A synthetic zero-speed point is prepended at 25°, and the same PCHIP fit above interpolates through it like any other curve point. This endpoint is part of the curve construction and is independent of the pinch cutoff. rangeAt's reported minTwa is the endpoint plus PINCH_FACTOR (currently 0.8) of the angular distance from that endpoint to the beat angle:

minTwa = 25° + 0.8 × (beatAngle - 25°)

Queries below this boundary are classified as in_irons and return value: null; the boundary does not alter the PCHIP curve. The 25° endpoint is only added when a valid beat target exists and the row's first point is above 25°.

Run-side (gybe)

The deepest real angle in a row is rarely 180° (dead downwind), but the polar plot is mirrored port/starboard at 180°, so a query (or a rendered curve) needs something defined all the way there. Unlike the beat side, real sailing behaviour genuinely continues past the last known angle — so this side does need an extrapolated model, in two steps, both operating on VMG (speed × |cos(twa)|) rather than raw speed, because VMG — not speed — is the quantity that peaks at the run angle and is expected to behave predictably beyond it:

  1. Mirror point. The run angle is a peak: VMG rises up to it and falls beyond it. As a first extrapolated point, the model assumes the fall mirrors the rise — the VMG at runAngle + d is assumed equal to the VMG already measured at runAngle - d, where d is the distance back to the nearest real axis point below the run angle. This adds at most one synthetic point, and only when it would land beyond the last real point and at or before 180°; if real data already reaches (or the mirror would overshoot) 180°, no mirror point is added. This point also becomes part of the PCHIP fit above, like any other point.
  2. Taper to zero slope at 180°. From the deepest known angle (the mirror point, or the last real point if no mirror was added), the VMG slope is assumed to decrease linearly to exactly zero at 180° — the point where the boat gybes and the polar mirrors onto itself, so VMG must be momentarily flat there. The starting slope for this taper is taken directly from the PCHIP tangent at that point (converted from dSpeed/dTWA to dVMG/dTWA), so the taper connects smoothly to the interior curve rather than starting from an independent estimate. Integrating that linear slope gives a quadratic VMG curve out to 180°, converted back to boat speed at query time. Two safety clamps apply: speed is never negative, and VMG is never allowed to increase past its value at the deepest known angle.

Both the mirror point and the taper are skipped if a row has no run target, too few points to establish a slope, or is already complete out to 180°.

Opting out: extrapolate: false

Every query method accepts extrapolate (default true). Pass extrapolate: false to restrict results to real data only — no beat-side 25° endpoint, no run-side mirror/taper. rangeAt then reports the TWA span actually covered by measured axis columns and derived targets, and speedAt/vmgAt return value: null (state.twa: 'below_range'/'above_range') for anything outside it. Use this when extrapolated data would be misleading for the caller's purpose (e.g. feeding a live performance calculation) rather than just for rendering a continuous curve.

Symmetry

Only port/starboard-symmetric tables are supported: polar-format requires symmetry.portStarboardSymmetric: true, and all lookups here always mirror speed across the beam (Math.abs(twa)). There is no asymmetric-table support yet.