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. For lists too long to scroll.
A synthetic photo library, year β month β day. Hover the big panel to move the lens; click a band to select it. The β in its corner is the widget's own settings β every consumer gets it for free.
style is how it's drawn. layout is
how vertical space is allocated. They're orthogonal β the
renderers never learn which algorithm ran, which is why every
combination above is valid.
No decimation. Every leaf gets a band, magnified near the focus and compressed away from it. Honest about density β use it when the silhouette is the point.
Furnas degree-of-interest. Leaves are scored by importance minus
distance-from-focus; the best keep a full row, the rest collapse
into their ancestors or an β― 4 more row. Nothing
sub-pixel.
DOI picks which leaves survive; the fisheye positions them. Bounded row count and continuous magnification. The one you want for a real library.
slots makes a band's height encode interest.
count makes it encode mass, so a
40,000-photo year visibly dwarfs a 400-photo one β a true
value-encoded partition diagram.
A vanilla
reactivewidgets-style DOM
node: a .value getter/setter and an
input event. Svelte and React wrappers ship in the same
package.
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)
});
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
Every node owns a leaf interval, and the layout's slots partition that same 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, and what lets the test suite state its invariants once and sweep them across every strategy:
| Containment | every band is inside its parent's |
|---|---|
| Tiling | rows tile the column with no gaps and no overlaps |
| Order | leaf order is preserved |
| Mass | counts are conserved through every elision |
| Focus | the focused row's band contains the focus pixel β the row you click is the row you pointed at |
| Budget | the focused leaf is never elided away, whatever the row budget |