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

polarflex

v0.3.0

Published

A zero-dependency polar layout engine for the web. Flexbox semantics — justify, gap, flex — on circles, arcs and spirals. Pure JS core; render to DOM, Canvas, SVG or CSS.

Downloads

87

Readme

⊙ PolarFlex

A zero-dependency polar layout engine for the web. Flexbox semantics — justify, gap, flex, alignment — on circles, arcs and spirals. The core is pure JavaScript with no DOM access, so it runs anywhere: browsers, workers, Node, any framework. Renderers are thin consumers.

npm i polarflex         # ESM, TypeScript types included, Node ≥ 18
// or straight off a CDN — there is no build step to need one
import { Orbit, layoutOrbit, sectorPath } from 'https://esm.sh/polarflex';

Working on the repo itself:

npm test                # 66 unit tests, node --test, no deps
npm run typecheck       # exercises the .d.ts against real usage
npm run demo            # serves the repo → open /demos/
# (or: python3 -m http.server 4173)

The demos must be served over HTTP (they use ES modules) — opening the files directly with file:// won't work.

Using an AI coding agent? Point it at llms.txt (shipped in the package). It front-loads the conventions an LLM gets wrong from priors — the 12-o'clock-clockwise angle basis, animating inputs instead of transforms, the closed-ring center convention — plus copy-correct recipes.

The mental model

CSS lays out along rows and columns. PolarFlex bends the flexbox vocabulary around an origin:

| Flexbox | PolarFlex | Notes | | --- | --- | --- | | main axis | the arc | angles in degrees, 0° at 12 o'clock, clockwise | | cross axis | the radius | | | justify-content | justify | start · center · end · space-between · space-around · space-evenly | | gap | gap (degrees) or gapLength (px of arc) | | | flex-grow | flex | items split leftover sweep by weight — a donut chart is just data-as-weights | | align-items | align / per-item balance | a 0–100 scale: 0 = inner edge on the baseline circle, 50 = centered, 100 = outer edge | | writing-mode-ish | orientation | axis (upright) · radial (reads outward) · tangential (follows the curve) | | flex-shrink-ish | overflow | visible (spill, the default) · shrink (compress sweeps proportionally; gaps survive) · grow (enlarge the radius until content fits — the polar answer: a ring can always grow) |

Items can size themselves by measurement (real DOM boxes), fixed sweep (degrees), arc length (px along the curve), or flex weight.

When content exceeds the range, the result always reports it (overflow, free), and the overflow option decides what happens: 'shrink' scales every non-flex sweep by one factor (reported as result.scale), 'grow' solves for the baseline radius at which measured/arc-length content fits (reported as result.radius) — fixed-degree sweeps never shrink or trigger growth, because degrees don't change with radius.

One convention worth knowing: on a closed ring (full 360°) there are no edges, so the space-* modes become phase choices. PolarFlex uses the instrument-maker's convention — item 0's center sits exactly at startAngle — so a 60-tick dial puts its 12-o'clock tick truly vertical and rotate means what you think it means. (justify: 'start' keeps edge semantics: item 0's leading edge at startAngle, the donut-chart convention.)

Fast paths and the slow path

Radial/tangential/fixed-sweep items have closed-form angular footprints — the solver is a single O(N) pass. Upright (axis) items are the hard case: their angular footprint depends on where they sit, which depends on everyone's footprint. The engine relaxes that system iteratively (exact 4-corner extents, O(K·N), K ≤ 16 — converges in 2–3 passes in practice). The playground demo draws the solver's working live.

Architecture

src/
  math.js     the kinematics — conversions, chords, sagitta, spirals, roses
  layout.js   THE BRAIN — layoutOrbit / layoutCurve / hitTest (pure functions)
  paths.js    THE MUSCLE — sectorPath / arcPath / spokePath / spiralPath / rosePath
  dom.js      vanilla DOM adapter — Orbit (ResizeObserver-batched, rAF-coalesced)
  canvas.js   Canvas2D glue — fitCanvas / drawPath, HTML-in-Canvas detection

Shapes are strings, not commitments. Every generator in paths.js returns SVG path data — the one currency accepted by three renderers:

svgPathEl.setAttribute('d', d);        // SVG
ctx.fill(new Path2D(d));               // Canvas2D
el.style.clipPath = `path("${d}")`;    // CSS

So PolarFlex is not "an SVG library." HTML stays real HTML — native z-index, focus, tooltips, selection, accessibility — positioned by plain CSS transforms. Canvas paints the heavy geometry. SVG remains available when you want it. When the emerging HTML-in-Canvas API (layoutsubtree / canvas.onpaint) ships, the same placements can drive DOM inside WebGL scenes — supportsHTMLInCanvas() is already there.

Quickstart (React)

import { useOrbit } from 'polarflex/react';

function RadialMenu({ items }) {
  const { ref, orbit } = useOrbit({ justify: 'space-evenly', gap: 4, radius: 'auto' });
  return (
    <nav ref={ref} style={{ position: 'relative', aspectRatio: '1' }}>
      {items.map((it) => (
        <button key={it.id} data-pf-flex={it.weight} onClick={it.run}>
          {it.label}
        </button>
      ))}
    </nav>
  );
}

The division of labor is clean: React owns the children, PolarFlex owns their positions. Keys, conditionals and .map() just work — the Orbit's MutationObserver picks up reconciliation, the batched ResizeObserver re-measures content changes, and per-item layout props are ordinary JSX attributes (data-pf-flex, data-pf-sweep, data-pf-orientation, data-pf-balance, …) that re-solve the ring when a render changes them. Pass fresh options every render — updates are merged and coalesced into one rAF solve.

SSR/RSC-safe: the Orbit is constructed inside an effect, so server renders emit plain HTML and positioning hydrates on the client. StrictMode's double-mounted effects are handled. orbit.current exposes the full imperative API (placements, setItem, hit-testing) when you need it. React is an optional peer dependency — only the polarflex/react entry touches it.

Quickstart (vanilla DOM)

<div id="menu">
  <button>Compose</button>
  <button data-pf-flex="2">Search</button>
  <button data-pf-orientation="tangential">Share</button>
</div>
import { Orbit } from 'polarflex';

const orbit = new Orbit(document.querySelector('#menu'), {
  radius: 'auto',          // fit the container (or a number in px)
  justify: 'space-evenly',
  gap: 4,                  // degrees
  align: 50,               // 0 inner … 100 outer
});

orbit.set({ rotate: 90 }); // re-solves next frame

The adapter implements the Mount → Measure → Math → Paint lifecycle: children register automatically (MutationObserver), one batched ResizeObserver measures real boxes, the pure engine solves, and positions land in a single rAF write pass as transform plus CSS custom properties (--pf-x, --pf-y, --pf-rotation, --pf-angle, --pf-radius, --pf-index). Because the output is a transform, CSS transitions animate every re-layout for free — the radial-menu demo's bloom is one transition line, staggered with --pf-index.

One honest caveat: a CSS transition interpolates each item's transform in a straight line between solves. Perfect for blooms and reflows; wrong for rotation, where items should travel along the arc. For that, animate the layout inputs instead — orbit.animate() tweens numeric options and re-solves every frame (solves cost microseconds), so items move in polar space. Angular keys take the shortest way around, and it respects prefers-reduced-motion:

orbit.animate({ rotate: -120 }, { duration: 800, easing: 'expo-out' });
// → { finished: Promise<boolean>, cancel() } — re-animating a key takes
//   over seamlessly from its current mid-flight value

Prefer to paint yourself? Use apply: 'vars' (custom properties only) or apply: 'none' + onLayout.

Stacking multiple orbits (read this before you nest rings)

A common composition is several Orbits layered over the same center — a main ring with a sub-ring, ornament rings behind interactive ones:

<div class="stage">
  <div class="ring" id="sub-ring">…</div>
  <div class="ring" id="main-ring">…</div>
</div>

The rings are full-size positioned overlays, and a transparent element still captures pointer events — the top ring's empty box will swallow every hover and click meant for the ring beneath it. The fix is one CSS pattern: ring containers pass events through, interactive items opt back in:

.ring { position: absolute; inset: 0; pointer-events: none; }
.ring :is(button, a, [tabindex]) { pointer-events: auto; }

With that in place, layers stack freely, z-order is plain DOM order, and items can even overhang their container — hit areas follow the elements, not the boxes. (The radial-menu demo is the reference implementation.)

Quickstart (headless core — any framework, workers, Node)

Vue/Svelte/Solid: use the same pattern as React — bind a container ref, hand it to new Orbit(el, options) in your mount hook, destroy() on unmount. Or skip the DOM adapter entirely and consume the pure solver:

import { layoutOrbit, sectorPath } from 'polarflex';

const { placements } = layoutOrbit(
  data.map(d => ({ flex: d.value, id: d.name })),
  { radius: 120, gap: 2, justify: 'start' }
);

for (const p of placements) {
  ctx.fill(new Path2D(sectorPath({
    innerRadius: 90, outerRadius: 150,
    startAngle: p.startAngle, endAngle: p.endAngle,
    cornerRadius: 8,
  })));
}

hitTest(placements, x, y, { innerRadius, outerRadius }) gives you pointer→slice resolution with pure polar math (law of cosines, angular intervals) — no pixel-picking.

API surface

layout.jslayoutOrbit(items, options), layoutCurve(items, options), hitTest(…), rotationFor(orientation, angle), tangentRotation(angle, r, r′)

layoutCurve has two modes. Step mode (pass step): items at uniform angular intervals — unchanged since 0.1. Flow mode (pass endAngle): a full solver in arc-length space — measurement / arcLength / sweep / flex sizing with justify / gap / gapLength distribution along any r(θ), where the currency is px of curve (which is what "equal spacing" means on a spiral — equal degrees bunch up as the radius grows). Flow-mode placements also rotate along the curve's true tangent (tilted by atan(r′/r)), not the circle approximation.

math.jspolarToCartesian, cartesianToPolar, normalizeAngle, unwrapAngle, angularDelta, angleInRange, arcAngle/arcLength, chordAngle/chordLength, polarDistance, sagitta, archimedeanRadius, logarithmicRadius, roseRadius, legibleRotation, mulberry32

paths.jssectorPath (rounded corners, pies, donuts, full rings), arcPath (stroke alignment, >180° splitting), spokePath (sine/zigzag/seeded-random drift with taper envelopes), curvePath (any r(θ)), spiralPath, rosePath, ringPath

dom.jsOrbit (with set, setItem, animate, layout, hit-testable placements), orbitify(selector, options), orbitStyles(); per-item config via data-pf-sweep, data-pf-flex, data-pf-orientation, data-pf-balance, data-pf-radius-offset, data-pf-arc-length, data-pf-ignore

canvas.jsfitCanvas (DPR-correct sizing), drawPath, supportsHTMLInCanvas

gestures.js (polarflex/gestures) — interaction on a polar basis. A generic "swipe" classifies as left/right/up/down; near a ring the meaningful split is tangential px (r·Δθ — spin it) vs radial px (Δr — pull it). createPolarTracker is a pure recognizer fed (x, y, t) samples (states: tap · press · hold · spin · radial; arc-length slop so tolerance is fair at every radius; unwrapped winding angle and turns for cranks; smoothed flick velocity) — Node-testable with synthetic streams. createInertia + nearestDetent are the flick physics. polarGestures(el, handlers) is the DOM binding — it captures the pointer only once a gesture classifies, so taps on child buttons stay perfectly native. And the payoff:

import { attachDial } from 'polarflex/gestures';

attachDial(orbit, { snap: 'items', inertia: true });
// drag spins along the arc · flick coasts · settles on a placement —
// the layout's own solve is the detent table. Open arcs rubber-band.

react.js (polarflex/react) — useOrbit(options){ ref, orbit }; React is an optional peer dependency of this entry only

The demos (/demos)

| | Proves | | --- | --- | | Atlas (index) | the hero astrolabe is solved by the engine it advertises, re-solving per frame | | Playground | every engine option live, with the solver's claimed angular intervals drawn underneath | | Radial menu | real <button>s, native stacking/tooltips/focus, bloom = one CSS transition, composable sub-arcs | | Donut chart | one solve drives Canvas slices and HTML labels; polar hit-testing; ~µs solves at 60 fps | | Chronograph | ticks/numerals/word-ring/subdial as four engine outputs on two renderers | | Spiral catalogue | layoutCurve on an Archimedean spiral, drag-to-wind, legibility auto-flip | | Fifths | a playable circle-of-fifths: grab-and-spin via attachDial (placements as detents), Web Audio triads, fixed canvas window | | Dial | the gestures playground: spin/flick/tap/hold/radial with live recognizer telemetry, native-click proof, open-gauge rubber-banding | | Commands | a nested marking menu: center: 'press' makes stroke direction the polar angle; slide onto a ▸ command to unfold its children in one drag | | Airlock | tangential vs radial on one surface — spin the carousel, or pull a crate past the threshold to jettison it; survivors close ranks via one re-solve |

Design notes / departures from spec v1.0

  • No per-shape <svg> wrappers. The spec's "every primitive renders its own absolutely-positioned <svg>" achieved native stacking, but path-data-as-strings achieves the same with renderer freedom (and fewer DOM nodes). The Z-index story is unchanged: HTML items are true siblings.
  • The React hook protocol maps 1:1 onto Orbit. usePolarLayout ≈ subscribe to onLayout + a registry keyed by ref; the core was built so that adapter is an evening's work, not a rewrite.
  • deg/rad, the kinematics, spirals, rose and sagitta utilities follow the spec's math.ts design (§7), with angles in degrees at the API surface and the screen convention fixed library-wide.

MIT licensed. No dependencies, no build step, no opinions about your renderer.