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

@john-guerra/fisheye-nav

v0.1.2

Published

A focus+context navigator for hierarchies: an icicle whose leaf axis is fisheyed, or a flat list indented by level. Vanilla reactive widget, with Svelte and React wrappers.

Readme

@john-guerra/fisheye-nav

npm CI license

A focus+context navigator for hierarchies: an icicle whose leaf axis is fisheyed, or a flat outline indented by level. Hover to magnify; click any band to select at that depth.

→ Live demo

Built for lists too long to scroll — a photo library with 3,000 days in it, a log tree, a file system — where you need to see where you are and what's around you at the same time.

Vanilla reactivewidgets-style DOM widget. Svelte and React wrappers included.

npm install @john-guerra/fisheye-nav
import fisheyeNav from "@john-guerra/fisheye-nav";

const nav = fisheyeNav({
  data: rows, // flat, pre-sorted rows
  keys: ["year", "month", "day"], // the levels
  count: (d) => d.count, // mass (drives the histogram)
});
document.querySelector("#sidebar").appendChild(nav);

nav.addEventListener("input", () => {
  console.log(nav.value);
  // [{key:"year", value:"2024"}, {key:"month", value:"06"}, {key:"day", value:"2024-06-13"}]
});

nav.value = [{ key: "year", value: "2024" }]; // set silently — no `input` fires

The two axes

style is how it's drawn. layout is how vertical space is allocated. They are independent — all eight combinations are valid.

| | uniform | fisheye | | ---------------- | -------------------------------------------------------------- | ------------------------------------------------------ | | all leaves | plain icicle | fisheye — every leaf, distorted around the focus | | DOI-selected | doi — full-height rows, uninteresting subtrees collapsed | hybriddefault |

  • fisheye — no decimation. Every leaf gets a band, magnified near the focus and compressed away from it. Honest about density; on thousands of leaves the compressed tail is sub-pixel, so use it when the silhouette is the point.
  • doi — Furnas degree of interest. Leaves are scored by importance minus distance-from-focus; the best ones keep a full, readable row and everything else collapses into its ancestors (2019 · 1,204) or into an elision row (⋯ 4 more). No magnification, nothing sub-pixel.
  • hybrid (default) — DOI picks which leaves survive, the fisheye positions them. Bounded row count and continuous magnification. This is the one you want for a real library.

style: "flat" gives internal nodes their own row (indented by depth), so you can always see what level you're on. style: "icicle" gives each depth a column instead.

Options

| | | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | data, keys | flat pre-sorted rows + the level names. Order is preserved — the widget groups, it never re-sorts. | | root, children | …or a nested tree instead. | | count | leaf mass. Default d => d.count ?? 1. | | label | (value, key, node) => string. | | style | "flat" | "icicle" | | layout | "hybrid" | "fisheye" | "doi" | "uniform" | | sizeBy | what a band's height encodes: "slots" (default — interest, every row equal) or "count" (mass, a true value-encoded partition). | | sizeFloor | under sizeBy: "count", the share of the column split equally regardless of mass, so a zero-count group is still visible and clickable. Default 0.25; 1 collapses back to "slots". | | distortion | lens strength. Higher = gentler. Default 4. | | minRowPx | target minimum row height; sets the DOI row budget. Default 16. | | apiWeight, distanceWeight, treeWeight, countWeight | DOI scoring knobs. | | bars | draw the count histogram inside each band (length = photo mass). Default true. | | barScale | "log" (default) | "sqrt" | "linear". Log, because real hierarchies have pathological count distributions — one bucket holding 99% of the mass flattens every other bar to the minimum under linear or sqrt. | | controls | show the ⚙ settings popover. Default true. | | persistKey | localStorage key to remember the user's settings under. Default null (don't persist). | | width, height | default: measured from the container (with a ResizeObserver). |

Methods: nav.update(opts), nav.getRows(), nav.getRoot(), nav.getLeafCount(), nav.getSettings(), nav.setSettings(s), nav.resetSettings(), nav.destroy(). Styling: override the --fn-* custom properties on .fisheye-nav.

Settings, built in

The widget ships its own ⚙ popover — view, lens, band size, bar scale, distortion, row size, and the DOI weights. A consumer should never hand-roll a gear. Give it a persistKey and the user's choices survive a reload; listen for settings if you want to react to them.

const nav = fisheyeNav({ data, keys, persistKey: "myapp.fisheye" });
nav.addEventListener("settings", (e) => console.log(e.detail)); // {style, layout, …}

An explicit option always beats a persisted one, so fisheyeNav({style: "icicle", …}) still opens as an icicle even if the user last left it flat. Pass controls: false to hide the gear entirely.

Svelte

<script>
  import FisheyeNav from "@john-guerra/fisheye-nav/svelte";
</script>

<FisheyeNav
  {data}
  keys={["year", "month", "day"]}
  style="icicle"
  bind:selected
  on:select={(e) => jumpTo(e.detail)}
/>

React

import FisheyeNav from "@john-guerra/fisheye-nav/react";

<FisheyeNav
  data={rows}
  keys={["year", "month", "day"]}
  selected={path}
  onSelect={setPath}
/>;

How it holds together

Every node owns a leaf interval, and the slots the layout produces partition that same leaf axis. So a node's band is the pixel span of the slots its interval covers — and because a child's interval is a subset of its parent's, its band is necessarily inside its parent's. Parents contain their children is arithmetic, not diligence.

That one idea is what lets selection and positioning vary independently, lets both renderers stay ignorant of which algorithm ran, and lets the test suite state its invariants once and sweep them across every strategy:

  • every band is contained in its parent's;
  • rows tile the column with no gaps and no overlaps;
  • leaf order is preserved;
  • photo mass is conserved through every elision;
  • the focused row's band contains the focus pixel — so the row you click is the row you pointed at;
  • the focused leaf is never elided away, whatever the budget.
npm test          # property tests (fast-check) + DOM tests (jsdom)
npm run dev       # the demo: the full style x layout matrix

The demo comes two ways. npm run dev serves example/index.html straight off src/ through Vite — use this while hacking on the widget. example/standalone.html runs off the bundled dist/, so it works under any plain static server (a bare http-server cannot load example/index.html, because src/ imports d3 by bare specifier and only a bundler can resolve that):

npm run build && npx http-server . -p 5190
# -> http://localhost:5190/example/standalone.html

License

MIT © John Alexis Guerra Gómez