@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 +21 -0
- package/README.md +172 -0
- package/dist/fisheye-nav.esm.js +4477 -0
- package/dist/fisheye-nav.js +4505 -0
- package/dist/fisheye-nav.min.js +1 -0
- package/dist/react.esm.js +4548 -0
- package/package.json +98 -0
- package/src/fisheyeNav.js +305 -0
- package/src/hierarchy.js +259 -0
- package/src/index.js +18 -0
- package/src/layout/bands.js +135 -0
- package/src/layout/index.js +202 -0
- package/src/layout/position.js +142 -0
- package/src/layout/scales.js +41 -0
- package/src/layout/select.js +256 -0
- package/src/react/index.js +81 -0
- package/src/render/controls.js +282 -0
- package/src/render/index.js +215 -0
- package/src/render/style.js +135 -0
- package/src/svelte/FisheyeNav.svelte +72 -0
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
|
+
[](https://www.npmjs.com/package/@john-guerra/fisheye-nav)
|
|
4
|
+
[](https://github.com/john-guerra/fisheye-nav/actions/workflows/ci.yml)
|
|
5
|
+
[](./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)
|