@john-guerra/fisheye-nav 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Alexis Guerra Gómez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # @john-guerra/fisheye-nav
2
+
3
+ [![npm](https://img.shields.io/npm/v/@john-guerra/fisheye-nav.svg)](https://www.npmjs.com/package/@john-guerra/fisheye-nav)
4
+ [![CI](https://github.com/john-guerra/fisheye-nav/actions/workflows/ci.yml/badge.svg)](https://github.com/john-guerra/fisheye-nav/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@john-guerra/fisheye-nav.svg)](./LICENSE)
6
+
7
+ A **focus+context navigator for hierarchies**: an icicle whose leaf axis is fisheyed, or a
8
+ flat outline indented by level. Hover to magnify; click any band to select at that depth.
9
+
10
+ **[→ Live demo](https://john-guerra.github.io/fisheye-nav/)**
11
+
12
+ Built for lists too long to scroll — a photo library with 3,000 days in it, a log tree, a
13
+ file system — where you need to see _where you are_ and _what's around you_ at the same
14
+ time.
15
+
16
+ Vanilla [reactivewidgets](https://reactivewidgets.org)-style DOM widget. Svelte and React
17
+ wrappers included.
18
+
19
+ ```bash
20
+ npm install @john-guerra/fisheye-nav
21
+ ```
22
+
23
+ ```js
24
+ import fisheyeNav from "@john-guerra/fisheye-nav";
25
+
26
+ const nav = fisheyeNav({
27
+ data: rows, // flat, pre-sorted rows
28
+ keys: ["year", "month", "day"], // the levels
29
+ count: (d) => d.count, // mass (drives the histogram)
30
+ });
31
+ document.querySelector("#sidebar").appendChild(nav);
32
+
33
+ nav.addEventListener("input", () => {
34
+ console.log(nav.value);
35
+ // [{key:"year", value:"2024"}, {key:"month", value:"06"}, {key:"day", value:"2024-06-13"}]
36
+ });
37
+
38
+ nav.value = [{ key: "year", value: "2024" }]; // set silently — no `input` fires
39
+ ```
40
+
41
+ ## The two axes
42
+
43
+ `style` is how it's **drawn**. `layout` is how vertical space is **allocated**. They are
44
+ independent — all eight combinations are valid.
45
+
46
+ | | `uniform` | `fisheye` |
47
+ | ---------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
48
+ | **all leaves** | plain icicle | **`fisheye`** — every leaf, distorted around the focus |
49
+ | **DOI-selected** | **`doi`** — full-height rows, uninteresting subtrees collapsed | **`hybrid`** — _default_ |
50
+
51
+ - **`fisheye`** — no decimation. Every leaf gets a band, magnified near the focus and
52
+ compressed away from it. Honest about density; on thousands of leaves the compressed
53
+ tail is sub-pixel, so use it when the _silhouette_ is the point.
54
+ - **`doi`** — Furnas [degree of interest](https://dl.acm.org/doi/10.1145/22339.22342).
55
+ Leaves are scored by importance minus distance-from-focus; the best ones keep a full,
56
+ readable row and everything else collapses into its ancestors (`2019 · 1,204`) or into an
57
+ elision row (`⋯ 4 more`). No magnification, nothing sub-pixel.
58
+ - **`hybrid`** (default) — DOI picks _which_ leaves survive, the fisheye _positions_ them.
59
+ Bounded row count and continuous magnification. This is the one you want for a real
60
+ library.
61
+
62
+ `style: "flat"` gives internal nodes their own row (indented by depth), so you can always
63
+ see what level you're on. `style: "icicle"` gives each depth a column instead.
64
+
65
+ ## Options
66
+
67
+ | | |
68
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `data`, `keys` | flat pre-sorted rows + the level names. Order is preserved — the widget groups, it never re-sorts. |
70
+ | `root`, `children` | …or a nested tree instead. |
71
+ | `count` | leaf mass. Default `d => d.count ?? 1`. |
72
+ | `label` | `(value, key, node) => string`. |
73
+ | `style` | `"flat"` \| `"icicle"` |
74
+ | `layout` | `"hybrid"` \| `"fisheye"` \| `"doi"` \| `"uniform"` |
75
+ | `sizeBy` | what a band's **height** encodes: `"slots"` (default — interest, every row equal) or `"count"` (mass, a true value-encoded partition). |
76
+ | `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"`. |
77
+ | `distortion` | lens strength. Higher = gentler. Default `4`. |
78
+ | `minRowPx` | target minimum row height; sets the DOI row budget. Default `16`. |
79
+ | `apiWeight`, `distanceWeight`, `treeWeight`, `countWeight` | DOI scoring knobs. |
80
+ | `bars` | draw the count histogram inside each band (length = photo mass). Default `true`. |
81
+ | `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. |
82
+ | `controls` | show the ⚙ settings popover. Default `true`. |
83
+ | `persistKey` | `localStorage` key to remember the user's settings under. Default `null` (don't persist). |
84
+ | `width`, `height` | default: measured from the container (with a `ResizeObserver`). |
85
+
86
+ Methods: `nav.update(opts)`, `nav.getRows()`, `nav.getRoot()`, `nav.getLeafCount()`,
87
+ `nav.getSettings()`, `nav.setSettings(s)`, `nav.resetSettings()`, `nav.destroy()`.
88
+ Styling: override the `--fn-*` custom properties on `.fisheye-nav`.
89
+
90
+ ## Settings, built in
91
+
92
+ The widget ships its own ⚙ popover — view, lens, band size, bar scale, distortion, row
93
+ size, and the DOI weights. **A consumer should never hand-roll a gear.** Give it a
94
+ `persistKey` and the user's choices survive a reload; listen for `settings` if you want to
95
+ react to them.
96
+
97
+ ```js
98
+ const nav = fisheyeNav({ data, keys, persistKey: "myapp.fisheye" });
99
+ nav.addEventListener("settings", (e) => console.log(e.detail)); // {style, layout, …}
100
+ ```
101
+
102
+ An explicit option always beats a persisted one, so `fisheyeNav({style: "icicle", …})`
103
+ still opens as an icicle even if the user last left it flat. Pass `controls: false` to hide
104
+ the gear entirely.
105
+
106
+ ## Svelte
107
+
108
+ ```svelte
109
+ <script>
110
+ import FisheyeNav from "@john-guerra/fisheye-nav/svelte";
111
+ </script>
112
+
113
+ <FisheyeNav
114
+ {data}
115
+ keys={["year", "month", "day"]}
116
+ style="icicle"
117
+ bind:selected
118
+ on:select={(e) => jumpTo(e.detail)}
119
+ />
120
+ ```
121
+
122
+ ## React
123
+
124
+ ```jsx
125
+ import FisheyeNav from "@john-guerra/fisheye-nav/react";
126
+
127
+ <FisheyeNav
128
+ data={rows}
129
+ keys={["year", "month", "day"]}
130
+ selected={path}
131
+ onSelect={setPath}
132
+ />;
133
+ ```
134
+
135
+ ## How it holds together
136
+
137
+ Every node owns a **leaf interval**, and the slots the layout produces **partition that
138
+ same leaf axis**. So a node's band is the pixel span of the slots its interval covers — and
139
+ because a child's interval is a subset of its parent's, its band is necessarily inside its
140
+ parent's. _Parents contain their children_ is arithmetic, not diligence.
141
+
142
+ That one idea is what lets `selection` and `positioning` vary independently, lets both
143
+ renderers stay ignorant of which algorithm ran, and lets the test suite state its
144
+ invariants once and sweep them across every strategy:
145
+
146
+ - every band is contained in its parent's;
147
+ - rows tile the column with no gaps and no overlaps;
148
+ - leaf order is preserved;
149
+ - photo mass is conserved through every elision;
150
+ - the focused row's band contains the focus pixel — so the row you click is the row you
151
+ pointed at;
152
+ - the focused leaf is never elided away, whatever the budget.
153
+
154
+ ```bash
155
+ npm test # property tests (fast-check) + DOM tests (jsdom)
156
+ npm run dev # the demo: the full style x layout matrix
157
+ ```
158
+
159
+ The demo comes two ways. `npm run dev` serves `example/index.html` straight off
160
+ `src/` through Vite — use this while hacking on the widget. `example/standalone.html`
161
+ runs off the bundled `dist/`, so it works under any plain static server (a bare
162
+ `http-server` cannot load `example/index.html`, because `src/` imports d3 by bare
163
+ specifier and only a bundler can resolve that):
164
+
165
+ ```bash
166
+ npm run build && npx http-server . -p 5190
167
+ # -> http://localhost:5190/example/standalone.html
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT © [John Alexis Guerra Gómez](https://johnguerra.co)