Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,17 @@ The `document` `mousemove` fallback returns early when cursor is over `.sidebar-

## Graph search filter

`#graph-search` text input in topbar. On `input` event: if query non-empty, adds `.filter-active` to `#results` and `.filter-match` to each `.anime-circle` whose `mediaStore[idx]` title (romaji or english) includes the query (case-insensitive). CSS:
`#graph-search` text input in topbar. On `input` event: if query non-empty, adds `.filter-active` to `#results` and `.filter-match` to each `.anime-circle` that matches all terms. CSS:

- `.filter-active .anime-circle` → `opacity: 0.15`
- `.filter-active .anime-circle.filter-match` → `opacity: 1`, `border-width: 7px`

**Query parsing:** Raw input split on whitespace. Tokens starting with `#` are tag filters — `#` stripped, underscores replaced with spaces, lowercased. Optional `:N` suffix (last colon in token) sets a minimum rank threshold (`minRank`); parsed as integer, ignored if not a valid number. Remaining tokens rejoined as a single title query string.

**Match logic (all must pass — AND):**
- *Title:* `titleQuery` non-empty → romaji or english title must include it (case-insensitive). Vacuously true if no non-`#` tokens.
- *Tags:* Each `{ name, minRank }` filter: a `media.tags` entry whose lowercased name includes `name` and whose `rank >= minRank` (or `minRank === null`) satisfies the filter. If no tag matches, falls back to `media.genres` substring match — only when `minRank === null` (rank threshold disables genre fallback).

On clear: removes `.filter-active` + all `.filter-match`. Cleared automatically on new Search. Independent of hover highlights — both can coexist.

## Media type
Expand All @@ -111,6 +117,8 @@ Affects all fetch paths:
| Popular | `type: ANIME` | `type: MANGA` | `type: MANGA, format: NOVEL` |
| Profile | `MediaListCollection(type: ANIME)` | `MediaListCollection(type: MANGA)` | `MediaListCollection(type: MANGA)` + client filter `format === 'NOVEL'` |

**Profile status filter:** Six checkboxes in `#input-profile` (Current, Completed, Paused, Dropped, Planning, Repeating — all checked by default). On search, checked values are collected into a `Set<string>` of AniList status enums (`CURRENT`, `COMPLETED`, `PAUSED`, `DROPPED`, `PLANNING`, `REPEATING`). `fetchProfile` receives this set and filters `collection.lists` by `l.status` before flattening entries. The query requests `status` on each list object. If no checkboxes are checked, search is blocked with a feedback message.

AniList popup link uses `/anime/${id}` for ANIME, `/manga/${id}` for MANGA and NOVEL (AniList stores novels under `/manga/`).

Stats label ("N anime") reflects the active media type. Feedback strings likewise. All graph features (clusters, connections, physics, visuals) are media-type-agnostic — same code path after fetch.
Expand Down Expand Up @@ -155,6 +163,11 @@ Virtual camera state: `camX, camY` (world-space center) and `camZoom`. Initializ

**Zoom:** `wheel` event inside `.center` only (non-passive, `preventDefault`). Zoom-to-pointer: record world position under cursor before zoom, recompute `camX/Y` to keep that world point under the cursor after zoom. Clamped to `[0.1, 10]`.

**Touch (mobile):** `.center` has `touch-action: none` (prevents browser scroll interference). Three `{ passive: false }` touch listeners on `.center`:
- `touchstart` — 1 finger: start pan (same variables as mouse pan). 2 fingers: start pinch — record `pinchStartDist`, `pinchStartZoom`, midpoint, and world point under midpoint.
- `touchmove` — 1 finger + `isPanning`: update `camX/Y` like mouse pan. 2 fingers + `isPinching`: compute `camZoom = pinchStartZoom × (dist / pinchStartDist)`, then recompute `camX/Y` to keep pinch midpoint's world point fixed.
- `touchend` — 0 fingers: clear both flags. 1 finger remaining after pinch: transition to pan from remaining touch position.

**Projection** (world → screen, per frame in `updateSimDOM`):
```
screenX = (worldX - camX) * camZoom + rect.width/2
Expand All @@ -176,6 +189,17 @@ Constants: `SPRING_REST=130`, `REPEL_K=5000`, `CENTER_K=0.002`, `DRAG=0.82`. `sp

Loop runs via `requestAnimationFrame`. Stopped/restarted on new search. Selection mode changes rebuild spring topology without resetting node positions.

## Retractable sidebars

Each sidebar has a `.sidebar-toggle` button as its first child. Clicking toggles `.collapsed` on the sidebar and `left-collapsed` / `right-collapsed` on `.content-area`.

**Grid:** Three class variants on `.content-area` narrow the relevant column to `--sidebar-collapsed-w` (24px):
- `.left-collapsed` → `24px 1fr 220px`
- `.right-collapsed` → `220px 1fr 24px`
- Both → `24px 1fr 24px`

**Collapsed sidebar:** `padding:0`, `gap:0`, `overflow:hidden`. All direct children except `.sidebar-toggle` get `display:none`. The toggle button expands to fill the full 24×100% strip and acts as a clickable handle. Arrow flips: `◀`↔`▶` (left), `▶`↔`◀` (right).

## Z-index layering

| Layer | z-index |
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Open `index.html` in a browser — no build step required.
Then:

1. Use the **media type buttons** in the topbar to select Anime, Manga, or Novel. All input modes and features work for each type.
Use the **filter bar** (topbar, between type buttons and AniList link) to highlight matching circles by title — non-matching entries dim while typing.
2. Use the **right sidebar** to import a `.txt` file (one title per line) and click **Search**.
Use the **filter bar** (topbar, between type buttons and AniList link) to highlight matching circles — non-matching entries dim while typing. Supports combined queries: plain text matches titles, `#tag` matches tags or genres (case-insensitive, use `_` for spaces, e.g. `#slice_of_life`), `#tag:N` requires the tag to have community acceptance ≥ N% (e.g. `#action:70`). Multiple terms are ANDed — e.g. `naruto #action:80 #shounen` shows only entries whose title contains "naruto" and which have the "action" tag at ≥ 80% acceptance and the "shounen" genre/tag. A rank threshold on a tag token disables genre matching for that token.
2. Use the **right sidebar** to choose an input mode: **Local** (import a `.txt` file, one title per line), **Popular** (top N by popularity), or **Profile** (AniList username). In Profile mode, use the status checkboxes to select which list statuses to include (Current, Completed, Paused, Dropped, Planning, Repeating — all checked by default). Click **Search**.
3. Matching entries appear as **circles** in a live physics simulation — nodes repel each other, shared-selection connections act as springs.
4. **Drag** the center area to pan. **Scroll** over the center area to zoom.
4. **Drag** the center area to pan. **Scroll** over the center area to zoom. On touch devices, **one-finger drag** to pan and **pinch** to zoom.
5. **Hover** a circle to see a popup with genres and community-rated tags; its connection lines highlight.
6. **Hover** a connection line (in the center area) to see shared selection items (tags or genres); its two endpoint circles highlight.
7. Click **↗** in the popup to open the entry's AniList page.
8. The **left sidebar** shows total entry and connection counts, plus a bar chart of selection items sorted by how many connections they appear in. **Hover** a bar to highlight all connections, circles, and the matching cluster polygon.
8. The **left sidebar** shows total entry and connection counts, plus a bar chart of selection items sorted by how many connections they appear in. **Hover** a bar to highlight all connections, circles, and the matching cluster polygon. Both sidebars can be collapsed via the **◀ / ▶** toggle button at the top of each sidebar; click again to expand.
9. **Cluster polygons** are drawn around groups sharing a selection item. **Hover** a polygon to highlight it and its connections. Labels sit outside the polygon border, placed to avoid overlapping other labels.
10. The controls in the **right sidebar** can be used to change selection criteria for connections and clusters, such as community acceptance of a tag or number of connections in a cluster.
11. Use the **Visuals** checkboxes in the right sidebar to toggle rendering of connections, covers, cluster polygons, and cluster labels independently.
Expand Down
80 changes: 80 additions & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
--sep: 1px solid #fff;
--fg: #fff;
--sidebar-w: 220px;
--sidebar-collapsed-w: 24px;
--topbar-h: 56px;
}

Expand Down Expand Up @@ -57,6 +58,18 @@ body {
min-height: 0;
}

.content-area.left-collapsed {
grid-template-columns: var(--sidebar-collapsed-w) 1fr var(--sidebar-w);
}

.content-area.right-collapsed {
grid-template-columns: var(--sidebar-w) 1fr var(--sidebar-collapsed-w);
}

.content-area.left-collapsed.right-collapsed {
grid-template-columns: var(--sidebar-collapsed-w) 1fr var(--sidebar-collapsed-w);
}

.sidebar-left {
position: relative;
z-index: 10;
Expand Down Expand Up @@ -121,6 +134,7 @@ body {
position: relative;
overflow: visible;
padding: 0;
touch-action: none;
}

#results {
Expand Down Expand Up @@ -476,3 +490,69 @@ input[type="range"] {
.text-input:focus {
border-color: #fff;
}

.status-checks {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-top: 0.4rem;
}

/* Sidebar toggle */

.sidebar-toggle {
flex-shrink: 0;
background: transparent;
border: 1px solid #333;
border-radius: 3px;
color: #555;
cursor: pointer;
font-size: 0.6rem;
padding: 0.2rem 0.3rem;
line-height: 1;
}

.sidebar-toggle:hover {
border-color: #fff;
color: #fff;
}

.sidebar-left .sidebar-toggle {
align-self: flex-end;
}

.sidebar-right .sidebar-toggle {
align-self: flex-start;
}

.sidebar-left.collapsed,
.sidebar-right.collapsed {
padding: 0;
gap: 0;
overflow: hidden;
}

.sidebar-left.collapsed > :not(.sidebar-toggle),
.sidebar-right.collapsed > :not(.sidebar-toggle) {
display: none;
}

.sidebar-left.collapsed .sidebar-toggle,
.sidebar-right.collapsed .sidebar-toggle {
width: 100%;
height: 100%;
border: none;
border-radius: 0;
align-self: auto;
font-size: 0.65rem;
color: #444;
display: flex;
align-items: center;
justify-content: center;
}

.sidebar-left.collapsed .sidebar-toggle:hover,
.sidebar-right.collapsed .sidebar-toggle:hover {
color: #fff;
background: #1a1a1a;
}
12 changes: 11 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ <h1 class="site-title"><img src="favicon.svg" class="site-icon" alt="" />anigrap
<button class="mode-btn media-type-btn" data-type="MANGA">Manga</button>
<button class="mode-btn media-type-btn" data-type="NOVEL">Novel</button>
</div>
<input id="graph-search" type="text" class="graph-search" placeholder="Filter results…" />
<input id="graph-search" type="text" class="graph-search" placeholder="title #tag #tag:minpct" />
<a class="link-btn anilist-home" href="https://anilist.co" target="_blank" rel="noopener">AniList ↗</a>
</header>
<div class="content-area">
<aside class="sidebar-left">
<button class="sidebar-toggle" id="toggle-left" title="Collapse sidebar">◀</button>
<div id="stats"></div>
</aside>
<main class="center">
<section id="results"></section>
</main>
<aside class="sidebar-right">
<button class="sidebar-toggle" id="toggle-right" title="Collapse sidebar">▶</button>
<div class="settings">
<p class="label">Input</p>
<div class="mode-switch">
Expand All @@ -53,6 +55,14 @@ <h1 class="site-title"><img src="favicon.svg" class="site-icon" alt="" />anigrap
<div id="input-profile" class="setting-panel" style="display:none">
<input id="profile-username" type="text" placeholder="AniList username" class="text-input" />
<span class="file-hint">Profile must be public</span>
<div class="status-checks">
<label class="vis-row"><input type="checkbox" class="status-check" value="CURRENT" checked> Current</label>
<label class="vis-row"><input type="checkbox" class="status-check" value="COMPLETED" checked> Completed</label>
<label class="vis-row"><input type="checkbox" class="status-check" value="PAUSED" checked> Paused</label>
<label class="vis-row"><input type="checkbox" class="status-check" value="DROPPED" checked> Dropped</label>
<label class="vis-row"><input type="checkbox" class="status-check" value="PLANNING" checked> Planning</label>
<label class="vis-row"><input type="checkbox" class="status-check" value="REPEATING" checked> Repeating</label>
</div>
</div>
</div>
<button id="search-btn" type="button" disabled>Search</button>
Expand Down
Loading
Loading