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

@pepperhorn/fingering-components

v0.2.2

Published

SVG key shapes and a JSON spec for woodwind and brass fingering charts

Readme

Fingering chart components

SVG key shapes and a JSON spec for building fingering charts across woodwind (and, with the same primitives, valved brass) instruments.

The design splits into three layers, and the split is the whole point:

| Layer | Answers | Changes when | | --- | --- | --- | | Shapes (src/keys.js) | what a key looks like | never, more or less | | Layout (instruments/*.json) | where each key sits, what it's called | you add an instrument | | Fingering (fingerings/*.json) | which keys are down for this note | constantly |

Because the layout owns geometry, a fingering is just a list of key ids. That makes the note-by-note data terse enough to hand-write, diff in git, or generate from a spreadsheet.

{ "note": "Bb", "octave": 3, "down": ["lh1","lh2","lh3","rh1","rh2","rh3","lh-bb"] }

Try it

node verify.mjs        # validates every layout and every fingering reference
node build-preview.mjs # rebuilds preview.html
open preview.html      # shape legend, state legend, rendered charts

No dependencies and no build step — plain ES modules, and the JSON is plain JSON. build-preview.mjs only exists to inline everything into one file you can open from the filesystem.

From an installed copy

The generator ships with the package, so an app that depends on it can build its own preview at any point — useful as a build step that drops the page into a site's static directory.

npx fingering-preview public/fingerings.html

The argument is the output path, resolved against the current directory, and any missing parent directories are created. With no argument it writes preview.html next to the package itself.

Shapes

| Shape | Used for | | --- | --- | | circle | tone holes, pearl touchpieces, ring keys, thumb holes | | oval | bis key, alternate F♯, side touchpieces, rollers | | pill | side keys, trill keys, thumb levers | | bar | long lever bars (flute B♭ bar, clarinet linkage) | | spatula | pinky tables — sax LH G♯/C♯/B/B♭ and RH E♭/C, clarinet levers, flute foot | | lever | stemmed side/trill levers; size sm/md/lg, rod stem with pivot (stemStyle: "line" for the hairline) | | roller | paired rollers between spatulas | | teardrop (alias drop) | sax palm keys, and any bulb-and-taper touchpiece | | taper | long, slim tapered drop — engraved-chart palm keys | | leaf | almond pad, widest in the middle — realistic palm keys | | bean | curved, optionally tapered capsule — side keys, clarinet throat A/G♯, sax low B♭ | | plate | rectangle with per-corner radii — tiles into pinky tables | | dome | half-ellipse on a flat base — table end caps, split-circle pinky pair | | cylinder | roller seen side-on — sits between pinky keys | | pin | teardrop hung from a pivot pearl on an arm | | lh-hook (alias hook) | lobe pointing left, rod down its right side — clarinet RH pinky and trill keys | | rh-hook | mirrored hook, rod on the left — clarinet LH pinky F/C | | flag | stem with a loop curled over the top — flute G♯ key | | crook | long arm bent down round a heel — flute B♭ thumb lever | | paddle | slim bar with a bellied head — flute B♮ thumb key | | ell | boat with an upright arm — flute foot C♯ and C levers (nested) | | note | round head with a stem — flute D♯ key | | saucer | cup seen low down, rim and pad — flute foot C/B and D♯ cups | | saucer-top | the saucer from straight above: rim, face, pad ring; size sm/md/lg (or saucer with view: "top") | | stacked | cup overlapping a cup behind it — flute G / linked RH cups | | twin | recorder double hole: ring with a large (part: "a", draws the ring) and small (part: "b") hole; both keys share the ring centre | | bell | brass bell flare, throat to rim — trombone body | | slide | trombone outer slide: two tubes, U crook, hand brace — one key per position | | tick | light dotted marker line, not a key — trombone positions passed | | club | small head on a slim neck — flute RH trill keys |

circle also takes ringed: true (a ring key's metal ring) and hole: true (an open tone hole whose centre stays empty until covered; themable with --fc-hole) and inner: 0.55 (a concentric ring inside the cup, as on flute keys).

Any key takes tag (short text, e.g. a trombone slide position number) and tagAt: [dx, dy]; tags stay upright when the chart is turned horizontal.

A key with showWith: [ids] is hidden unless one of those keys is in use (pressed, half, etc.). A key with hint: true is a visual aid and only draws with the hints: true render option — e.g. the trombone's dotted marks at the positions before the current one: renderChart(trombone, f, { hints: true }).

Guides take dash: "dotted" (or a dasharray) for paths of travel such as the trombone slide.

Any key takes naStyle: "dotted": in the na state it draws as a dotted outline instead of a faint one (flute mechanism cups).

Geometry is per-key and optional: r, rx/ry, w/h, rad, rot. Family defaults live in the layout's defaults block, keyed by shape name.

teardrop takes w (bulb diameter), h (overall length), and tipRound (how blunt the point is — drop it toward 0 for a needle, raise it for a pear). The tip points up by default; dir aims it (up, down, left, right, or the four diagonals) and stacks with any rot. dir works on every shape, so lever and bar can use it too. If h gets close to w the taper has nowhere to go and the shape falls back to a plain bulb rather than breaking.

Partial fills follow the shape: a half teardrop fills from the bulb end up, which is what you want when a chart shows a palm key only partly depressed.

Themes, highlight and two-tone

Colours are CSS custom properties: --fc-ink (pressed), --fc-line (outline), --fc-key (unpressed surface), --fc-accent, --fc-highlight, --fc-ink-2 (second tone), --fc-text, --fc-font, --fc-stroke. The highlight state calls a key out; { twoTone: 'hand' } draws right-hand keys in --fc-ink-2. Setting --fc-key: none; --fc-stroke: 1.1 with a serif font gives the original outline look.

Orientation and looks

renderFingering and renderChart take orient: 'horizontal' (the instrument on its side, mouthpiece left; labels stay upright) and look: 'ghost' (whole diagram faded via --fc-ghost) or look: 'dotted' (unpressed keys as dotted, unfilled outlines; pressed keys stay solid). Every key is wrapped in <g class="fc-key" data-key="…" data-state="…">, so an app can restyle or animate individual keys — e.g. dotted fingerings falling onto a ghosted instrument, piano-tiles style.

Layout variants

A layout can carry named, additive overrides under variants. Pass one or several: renderChart(sax, notes, { variant: 'palm-taper side-levers' }). The saxophone defaults to leaf palm keys and ships palm-teardrop, palm-bean, palm-taper, side-levers, side-pills, high-fs-single, rh-table-stacked, lh-table-domes, rh-table-domes, lh-table-spatulas and rh-table-spatulas; the clarinet ships throat-pills and ring-keys.

States

| State | Meaning | | --- | --- | | open | uncovered / not pressed (outline) | | closed | covered / pressed (solid) | | half | half-holed — recorder and whistle | | quarter, three-q | finer thumb pinch gradations | | ring | ring or plateau touched, hole stays open (clarinet, open-hole flute) | | optional | dashed — may be added for tuning or stability | | alt | hatched — belongs to an alternate fingering being shown alongside | | trill | caret above the key — flick or trill action | | na | key absent on this variant (hidden by default) |

fillFrom on a key sets which edge partial fills grow from (bottom default, or top / left / right).

Positioning: where to tweak

Three ways to place a key. Absolute coordinates still work everywhere — the other two are for the cases where the relationship between shapes is what you're actually adjusting, like a pinky table.

1. Absolute — x, y

The centre point, in viewBox units. Fine for the stacks and one-off keys.

2. Cluster — cluster + flow

A cluster owns an anchor, an optional rotation, shape defaults, and a flow rule that positions its members. Keys join by naming it. This is the level you want for pinky tables: nudge the anchor and every lever moves together, change rotate and the whole table leans.

{
  "id": "lh-table",
  "anchor": [23, 188],       // origin for member offsets
  "rotate": -7,              // degrees, about the anchor; members inherit it
  "flow": { "axis": "y", "pitch": 15.5, "drift": -1.4 },
  "defaults": { "shape": "spatula", "w": 15, "h": 13, "rad": 5 }
}

Flow rules:

| Field | Effect | | --- | --- | | axis | x or y — the direction members march in | | pitch | centre-to-centre spacing along that axis | | drift | steady cross-axis lean, per step — a column that slides sideways | | stagger | alternating cross-axis offset — the sax palm keys | | shape: "arc" | fan members around the anchor instead of in a line | | radius, startAngle, step | arc geometry; negative step reverses direction | | rotateMembers | tilt each member tangentially to the arc | | memberRotate | constant added to that tilt | | tilt | multiplier on the tilt, 0–1, when the full tangent is too dramatic |

Member order in the keys array sets flow index. Override with index to leave a gap (sax side-e sits at index 3, skipping 2), or with dx/dy to opt out of the flow entirely. ddx/ddy nudge a single member without leaving the flow — that's how the sax low B and B♭ step further left than G♯ and C♯.

Arc clusters are what the clarinet lever groups want. The anchor is the pivot outside the fan, so the levers curve away from the body:

{
  "id": "lh-table",
  "anchor": [66, 176],
  "flow": { "shape": "arc", "radius": 46, "startAngle": 195, "step": -16,
            "rotateMembers": true, "memberRotate": -180, "tilt": 0.5 }
}

3. Relative — relativeTo + place

Butt one key against another's edge. Gaps stay correct when you resize either shape, which absolute coordinates don't.

{ "id": "lh2", "relativeTo": "lh1", "place": "below", "gap": 10 }
{ "id": "bis", "relativeTo": "lh1", "place": "below", "gap": 0, "offset": [16, 0] }

place is above / below / left / right / same. gap is edge-to-edge and may be negative to overlap. offset is a free [x, y] nudge applied after placement. Rotation is inherited from the reference unless inheritRotation: false. Chains resolve in any order; a cycle throws.

align — which point of the shape sits on the coordinate

Default is center. Set top, bottom, left, right, or a combination like top-left, and the coordinate becomes that edge or corner instead. Use it when spatulas of different heights need to share an edge rather than a centre line.

Checking your work

contentBounds(layout) returns the bounding box of every key, rotation-safe. Compare it against viewBox after moving things, or use it to auto-fit.

import { contentBounds, resolveLayout } from './src/render.js';
contentBounds(clarinet);              // [x, y, w, h]
resolveLayout(saxophone).byId['lh-b'] // final absolute x, y, rot

Layout schema

{
  "id": "saxophone",
  "name": "Saxophone",
  "family": "single-reed",
  "transpose": -9,              // semitones, written → sounding (required; 0 = concert)
  "horn": "alto",               // default horn, when the layout is shared
  "horns": {                    // optional: versions of the instrument, each with its transpose
    "soprano": { "name": "B♭ soprano", "transpose": -2 },
    "alto": { "name": "E♭ alto", "transpose": -9 },
    "tenor": { "name": "B♭ tenor", "transpose": -14 },
    "baritone": { "name": "E♭ baritone", "transpose": -21 }
  },
  "viewBox": [0, 0, 108, 286],
  "defaults": { "circle": { "r": 9 }, "pill": { "w": 8, "h": 15 } },
  "panels": [
    { "id": "front" },
    // a rear panel is how the thumb side gets drawn: flute's two back keys,
    // clarinet's thumb hole + register key, recorder's thumb hole
    { "id": "rear", "label": "back", "origin": [92, 26], "box": [0, 0, 36, 66] }
  ],
  "keys": [
    {
      "id": "lh1",              // stable, referenced by every fingering
      "label": "Left 1 (B)",    // human-readable, used for accessibility
      "shape": "circle",
      "x": 41, "y": 44,         // centre point, in viewBox units
      "panel": "front",         // omit for front
      "hand": "L",              // L | R  — for teaching overlays and filtering
      "digit": "1",             // 1..4 | thumb | palm
      "group": "lh-stack",      // for highlighting a whole cluster
      "default": "open"         // e.g. flute rh-eb defaults to closed
    }
  ]
}

Fingering schema

Every file in fingerings/ has the same shape, and every note is at written pitch — the renderer transposes with the layout's transpose.

{
  "instrument": "tin-whistle",   // layout id
  "horn": "C",                   // optional: which of the layout's horns
  "pitch": "written",            // always written
  "note": "…",                   // what the sheet covers
  "fingerings": [ … ]
}

Each fingering; everything not named is open (or the key's default).

{
  "note": "Bb",
  "octave": 3,                             // written octave, C4 = middle C
  "note_text": "alternate B♭ fingerings",  // small annotation under the label
  "down": ["lh1", "bis"],
  "half": ["thumb"],
  "ring": ["lh2"],
  "optional": ["rh-eb"],
  "trill": ["trill-d"],
  "states": { "lh3": "alt" },              // explicit override, wins over the above
  "alternates": [ { "down": ["lh1", "side-bb"] } ]
}

Rendering

import { renderFingering, renderChart, sounding } from './src/render.js';
import sax from './instruments/saxophone.json' with { type: 'json' };
import data from './fingerings/saxophone.json' with { type: 'json' };

renderChart(sax, data.fingerings, { columns: 9 });   // full sheet, SVG string
renderFingering(sax, data.fingerings[0]);            // single diagram

// labels at sounding pitch, for a given horn
renderChart(sax, data.fingerings, { pitch: 'concert', horn: 'tenor' });
sounding(sax, { note: 'C', octave: 5 });             // { note: 'Eb', octave: 4 } on alto

Pitch

| Instrument | transpose | Horns | | --- | --- | --- | | flute, trombone, Nuvo Dood, Nuvo TooT | 0 | | | clarinet, trumpet (B♭) | −2 | | | soprano recorder | +12 (written an octave below sounding) | | | saxophone | −9 (alto) | soprano −2, alto −9, tenor −14, baritone −21 | | tin whistle | 0 | D, C, B♭, F, E♭ — one sheet per horn |

verify.mjs enforces the schema: every layout has an integer transpose (matching its default horn), every fingering file is "pitch": "written", names a real horn if it names one, and gives every note a written octave.

Ranges

Every layout carries ranges — three nested skill bands at written pitch:

"ranges": { "beginner": { "low": "D4", "high": "C6" }, "intermediate": { … }, "pro": { … } }

A horn may override with its own ranges (tin whistles do: each sheet is at that whistle's pitch). verify.mjs checks the bands nest and that every note in pro has a fingering.

React wrappers are in src/react.jsx — <Fingering>, <Chart>, and <FingeringEditor> (click a key to cycle its state, so charts get authored by clicking rather than by typing key ids).

Theme through CSS custom properties: --fc-ink, --fc-line, --fc-paper, --fc-font.

Registers

Beside ranges, every layout carries registers — an ordered list of bands that names a written pitch by register rather than by octave number, so an app can say "High G" instead of "G5":

"registers": [
  { "from": "Bb3", "name": "Lowest" },
  { "from": "C4",  "name": "Low" },
  { "from": "C5",  "name": "Middle" },
  { "from": "C6",  "name": "High" },
  { "from": "G6",  "name": "Altissimo" }
]

The bands are half-open: each runs from its from up to — but not including — the next band's from, and the last runs to the top of the instrument. The first from is the lowest written note the layout fingers, so every fingering lands in exactly one band. Pitches are spelled as in ranges: Bb3, C#4, F7.

The bands follow the written octave, with the boundary at C — not the instrument's register break. Flute has low C (foot C), middle C (up the octave), then high C; alto saxophone has low D (all down), middle D (all down with the octave key), high D (palm key) and then altissimo D. A partial group below the instrument's first C — the notes under the bottom of its lowest full octave — is named Lowest; the full octaves above it run Low, Middle, High, Altissimo in order.

The reason is that a register name is rendered without the octave digit ("Low C", not "C4"). A band wider than an octave therefore holds two notes of the same pitch class and the name stops identifying a note: anchored at the register break, flute C4 and C5 both came out as "Low C", and alto sax, clarinet, trumpet and trombone each had several such pairs. Grouping by written octave keeps every band under an octave, so a register name plus a note letter is unique on every instrument.

Saxophone is the one deliberate exception to the boundary-on-C rule: Altissimo starts at G6, not C7, because that is exactly where saxophone-altissimo.json begins. The tier then means "needs an altissimo fingering" rather than "is in the seventh octave", which is what the word means to a player. G6–F7 spans eleven semitones, so it still holds each pitch class once and the names stay unique.

name comes from a fixed set — Lowest, Low, Middle, High, Altissimo — and not every instrument uses all five:

| Layout | Bands | | --- | --- | | saxophone.json | Lowest B♭3 · Low C4 · Middle C5 · High C6 · Altissimo G6 | | clarinet.json | Lowest E3 · Low C4 · Middle C5 · High C6 | | flute.json | Low C4 · Middle C5 · High C6 · Altissimo C7 | | recorder.json | Low C4 · Middle C5 · High C6 | | trumpet.json | Lowest F♯3 · Low C4 · Middle C5 · High C6 | | trombone.json | Lowest E2 · Low C3 · Middle C4 · High C5 | | nuvo-toot.json, nuvo-dood.json | Low C4 · High C5 | | tin-whistle.json | per horn — see below |

The tin whistle is the documented exception: it has no layout-level registers, because every whistle is written in its own range, and its bands are anchored on the horn's tonic rather than on C. Each horn carries its own list beside its own ranges, with the high band an octave above the bottom note — D whistle Low D5 · High D6, C whistle Low C5 · High C6, and so on. That is musically right for a two-register instrument whose bottom note is its tonic, and it is still collision-free: each band is exactly an octave wide.

verify.mjs checks the bands ascend, that every name is one of the five, that the first band starts on the lowest note in that layout's (or horn's) fingerings, and — the check that matters — that no band contains two fingerings of the same pitch class, walking the real fingerings of every instrument and every tin-whistle horn and naming the offending notes if it finds a pair.

Instruments

| Layout | Fingerings | | --- | --- | | saxophone.json | written B♭3–F♯6, altissimo G6–F7 in saxophone-altissimo.json | | flute.json | C4–C7 with alternates | | clarinet.json | written E3–A6 with alternates | | recorder.json (soprano, baroque) | written C4–C6 | | tin-whistle.json | one sheet per horn: D, C, B♭, F, E♭ | | nuvo-dood.json, nuvo-toot.json | full charts from the maker | | trumpet.json | written F♯3–C6 | | trombone.json (tenor) | E2–C5, slide positions 1–7 |

Sax altissimo sits in its own file: it varies by horn, mouthpiece and player.

Files

src/keys.js          shape primitives, states, bounding boxes
src/layout.js        clusters, flows, alignment, relative placement
src/render.js        resolved layout + fingering -> SVG
src/react.jsx        React wrappers, including a click-to-author editor
instruments/*.json   key layouts
fingerings/*.json    note data
build-simple.mjs     generates the hole-based layouts on a shared grid
build-preview.mjs    bundles everything into preview.html
verify.mjs           validates every layout and every fingering reference