- JavaScript 85.7%
- CSS 8.1%
- HTML 6.2%
| docs | ||
| lib | ||
| prompts | ||
| test | ||
| .gitignore | ||
| app.js | ||
| CLAUDE.md | ||
| embed.js | ||
| embedding-example.html | ||
| index.html | ||
| LICENSE | ||
| README.md | ||
| sample-graph.json | ||
| search.js | ||
| style.css | ||
Tree Crawl Graph
An editor built on the D3 force graph.
Disclaimer
This code, in it's entirety, was automatically generated by Anthropic's Claude Opus 5. In being so, I feel that I may have perhaps cheated the process. However, this idea has been very clear in my mind, but I have been unable to build it to a satisfactory standard. I hope that it's usefulness is sufficient to justistify it's existence.
There are some concerns with automatically generated code. To address these, the following measures have been taken:
- The prompts used are numbered and available under the
promptsfolder. The initial prompt contains the specification, with subsequent prompts describing changes after testing the generated output. - The entire chat transcript is available at: https://files.kistu.dev/projects/2026-07-28-tree-crawl-graph.
- The application is completely local and makes no network requests.
- The application does not access or store any files, apart from accessing uploaded files and storing a cached copy of the current data file in the browser storage.
d3.js is vendored under
librather than fetched. There is no CDN fallback: if the library is missing, the page says so instead of reaching for the network. Acquiring it in the first place is a manual step — see Usage.
Please maintain a backup and version control your data file to prevent unexpected data loss.
If execution of JavaScript is a concern, run this application in a virtual machine.
Project Structure
.
|- index.html defines webpage structure and imports styles/scripts
|- style.css defines the theme and style
|- app.js defines the force graph, editor and related actions
|- search.js manages searching
|- lib/d3.v7.min.js d3 javascript library
Usage
Simply clone this repository and open index.html in a web browser. JavaScript must be enabled for d3 to work.
The repository ships with lib/d3.v7.min.js. If it is ever missing, fetch it once and reload:
curl -L -o lib/d3.v7.min.js https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js
Alternatively, the application is hosted on CloudFlare pages: https://treecrawl.pages.dev/.
Behaviour
Graph
| Action | Function |
|---|---|
| Scroll | Zoom |
| Drag canvas | Pan |
| Drag node | Move the node with it's children |
| Click | Select a node or a link |
| Shift + click | Select a second node |
| Hover on link | Show link's label |
| Double click | Expand one layer, or collapse the whole subtree |
| Shift + double click | Expand the children, or collapse if expanded |
| Press and hold | Same as shift + double click |
| Ctrl + double click | Expand the ancestors, or collapse if expanded |
| Tripple tap | Same as ctrl + double click |
| Drag gripper | Move the graph/editor boundary (horizontal when stacked) |
| Drag lower gripper | Move the markdown/event-editor boundary |
| Click '+' button | Adds a child node, or links the two selected nodes |
| Hold '+' button | The same, but the link it makes is weak |
For an ordered selection on touch, use the multi-select button. Double taps wait a moment before acting so a third tap can override them.
Timeline
Thin bars along the bottom of the canvas, one per bucket of time, as tall as the number of nodes created in that bucket. Ids are creation timestamps, so this is the graph's own history rather than anything extra kept for it.
| Action | Function |
|---|---|
| Scroll | Change the resolution, a second per bar up to a year |
| Drag | Select every node created in that stretch, and draw round them |
| Shift + drag | Pan along the timeline |
| Click | Open the event under the cursor in the editor below |
| Double click | Back to the default day window |
| Hover | Show the event at that moment, if there is one |
Scrolling holds the instant under the pointer still. The window shows about a
day by default and slides so new work stays in view; once you scroll, drag or
pan it stays where you put it, and a double click (or r) gives the day back.
The strip rests at a low opacity so it stays out of the way of the graph underneath, and comes fully forward while the pointer is over it. How far back it sits is a setting.
Events mark a stretch of that time — a start, an end, a title and some
markdown. The editor below the markdown field records them: press the record
button (or e) to open an event, and again to close it. An event holds:
- every node created while it ran, worked out from the creation timestamps rather than stored on the node, so moving an event's start or end changes what it holds;
- every node edited while it ran — a label, markdown or colour change, even on a node made long before. Nothing about an edit shows in a timestamp, so these are the ids the event writes down. Moving a node does not count, or tidying the layout would sweep half the graph into the event.
An event that has not been stopped is still recording, even after a reload.
Clicking an event on the strip opens it in the editor and selects the nodes it holds, drawing a solid curve round them. Anything the timeline selects is wrapped in pink rather than the lasso's blue, so it is clear at a glance which one drew the loop; within that, a dragged stretch stays dashed like the lasso it shares a gesture with, and an event — a boundary that already exists — is solid. Selecting a single node the other way round shows the events it belongs to. A node can be in more than one — nested events, or one it was made in and one it was later edited during — and they appear as tabs above the editor. Deselecting empties the editor again, unless an event is still recording.
Pressing r with a node selected frames every event that node is part of and
opens them all; with nothing selected it goes back to the default day.
Keyboard
Shortcuts only fire when focus is outside the label and markdown fields.
Keyboard shortcuts can be disabled in settings.
n |
Add a child node, or link the two selected nodes |
Enter |
Same as n |
p |
Select previous node or link source |
Shift + n |
Same as n, but the link it makes is weak |
dd |
Remove the selected node or link |
Delete |
Same as dd; Backspace too |
s |
Focus the search box |
r |
Recenter and reset zoom, and the timeline with it |
l |
Lasso select |
m |
Multi-select (tap nodes in order) |
e |
Start or stop a timeline event |
k |
Show the keybindings panel |
Ctrl + S |
Download the current file |
Esc |
Close panels, leave the editor or clear search |
In the label field, Enter hands focus back to the graph, while Tab moves to the markdown field.
dd is a two-key sequence: the first d stays armed for 700 ms.
In the search box, Enter steps through matches.
Search
Search covers nodes, links and timeline events, over labels and markdown.
It supports a small query language:
foo bar |
Both words — adjacency means AND |
a || b |
Either. && is explicit AND, ! negates |
(a || b) && !c |
Parentheses group |
node::foo |
Nodes only. link:: for links, event:: for timeline events, all three by default |
event::standup |
Timeline events, by title or notes |
node::(a || b) |
A scope can wrap a group |
color == red |
Exact match on a palette name |
date > 2025-01-01 |
By creation date; id compares raw timestamps |
label : plan |
Field contains. == and != are exact |
"two words" |
Quoted phrase |
Fields:
idordate/created— on an event,dateis when it began;idis the raw idcolor/colourlabel, ortitle— the same field; an event stores its name as a titletextkind,weak,collapsed
Comparisons take >, <, >=, <=, ==, != and :.
Anything that doesn't parse falls back to a plain substring match, so a half-typed query never breaks the box.
The search bar border turns red when a valid query matches nothing.
A matching node wears a ring of small orange ticks; a matching link turns thick and dashed. Neither repaints the item, so a node you have coloured keeps its colour while it matches. A matching event lights its band on the timeline in the same orange, and pressing Enter on it opens the event: the strip scrolls to it, the editor loads it, and everything it holds is selected and wrapped in the selection curve.
Settings
- Show link labels — When disabled, link labels are only shown on hover.
- Show rings — When disabled, no rings will be used to show that a node has been collapsed.
- Show timeline — When disabled, both the strip over the canvas and the event editor are hidden.
- Timeline opacity — Where the strip rests when the pointer is elsewhere; it is always fully opaque on hover.
- Link length — The resting distance of a link, and the radius at which a new child is spawned.
- Node repulsion — The force with which nodes push each other apart.
- Colours — Named palette that nodes can reference. Renaming a colour updates nodes using it.
- Hold to simulate — While held, everything except the root and hidden nodes nodes are unpinned; this eases crowded nodes apart.
- Show info popup — When disabled, the transient message in the bottom-left corner is hidden. Warnings are not affected: a failed autosave, or a file whose ids had to be repaired, appear at the top of the graph and stay until dismissed.
- Keyboard shortcuts — When disabled, all keybindings defined in the keyboard section will not work.
Data
All data is stored in one JSON file. The application data is included first, follwed by the graph data.
Positions are relative to the parent node, IDs are creation timestamps, and empty fields are omitted.
Example data:
{
"app": "Tree Crawl Graph",
"version": 1,
"saved": "2026-07-27T09:12:44.101Z",
"settings": {
"showLinkLabels": false, "showRings": true, "shortcuts": true,
"showStatus": true, "linkDistance": 120, "repulsion": 200, "split": 70
},
"palette": [
{ "name": "red", "value": "#ea6962" }
],
"nodes": [
{ "id": 1750000000000, "x": 0, "y": 0, "label": "Root", "text": "# notes" },
{ "id": 1750000000001, "x": 130, "y": 0, "label": "Child", "color": "red" }
],
"links": [
{ "id": 1750000000002, "source": 1750000000000, "target": 1750000000001 }
],
"events": [
{ "id": 1750000000003, "start": 1750000000000, "end": 1750000000900,
"title": "First pass", "text": "what these notes came out of",
"nodes": [1740000000000] }
]
}
An event with no end is one that is still recording. Which nodes it holds is
mostly not written down — it is whichever ones were created between its start
and its end. nodes lists only the exceptions: ids of nodes that were edited
while it ran but made before it started. A subgraph export carries no events,
because appending a subgraph reissues every id it brings and the window would
arrive holding nothing.
Opening a file restores its settings and palette along with the graph.
The current graph is mirrored to localStorage as you work, so a refresh doesn't cause data loss. Saving is still required to download the file.
Theming
All colours and metrics are CSS variables at the top of style.css — Gruvbox Material dark, with two accents (--accent for selection, --accent-2 for search and the second selected node), --warn, and --ring-child / --ring-parent for the two kinds of nesting ring.
Graph geometry constants (node radius, ring spacing, force constants) are in CFG at the top of app.js.
Both pane splits — graph against editor, and markdown against the event editor
— are dragged with their grippers and remembered between sessions.
Notes on behaviour
This section has not been reviewed yet.
-
Positions are fixed. Every node is pinned (
fx/fy); only dragging moves one. The forces act on a node just after it's created, while it finds a spot near its parent — about a second — and then it's pinned too. The force constants live inCFGinapp.js; they aren't user-facing. -
A node's first incoming link is its parent for positioning and for the collapse walk. Extra links are cross-links.
-
Weak links are cross-links — drawn very faint, since they're associative rather than structural. They come to full strength when hovered, selected, or when either end is selected. Dragging a node does not carry anything joined only by a weak link. Links created with a new child node are structural and always solid.
-
The lasso (button, or
l) draws a freehand loop and selects every visible node inside it. With several nodes selected the label and markdown fields go quiet but the colour field still applies to all of them. -
Multi-select (button, or
m) accumulates taps until you switch it off. Unlike the lasso it keeps the order you tapped in, which is what a link needs — the first two selected become source and destination. While it's armed a tap only ever selects, so the double and triple tap gestures stay out of the way. Tapping a selected node again drops it. -
A link needs two nodes selected. With two selected,
+andnlink them; with one, they add a child. Shift only ever changes what kind of link is made, never which of those two things happens — soShift + n, shift-clicking+and holding+all make the link weak, whether it joins two existing nodes or attaches a new child.On touch, select the two nodes with the multi-select button, which keeps them in the order you tapped; that order is what gives the link its direction.
-
pwalks back through the nodes you've selected, up to fifty of them. Creating a node records its parent, son, type a label, Enter,preturns you to where you branched from. Links are deliberately not recorded:pon a link already means "go to its source", so recording it would bounce you straight back. Entries pointing at deleted nodes are skipped. On a link,pandnstep to its source and destination instead. -
Colour is a palette name, not a raw value, so recolouring the palette restyles every node at once. A literal
#rrggbbin a hand-edited file also renders. -
Recenter targets the selection.
rcentres the selected node if there is one, otherwise the whole visible graph. -
Collapsed nodes wear rings — one thin ring per level of hidden depth, up to three, fading toward the rim. A focused node also wears rings in a second colour for the levels of parents the focus is hiding above it. When a node has both, the parent rings sit outside the child rings, and a search hit's ticks sit outside everything.
-
Collapsing is always recursive, expanding is one layer at a time (or the whole subtree with shift). So you can fold a branch away and then crawl back down it one level per double click.
-
Ctrl + double click focuses a node: the graph redraws from that node as if it were the root, hiding its parents and their other branches. The viewport itself is left where it is. This is view state only — nothing above it is modified, so ctrl + double clicking the same node restores the previous view exactly. It isn't written to the file.
-
New nodes pull the view along. If a node spawns or settles past the edge of the viewport the graph pans the minimum needed to keep it on screen.
-
Orphans are temporary. Deleting a node's only incoming link marks it orphaned (dashed red outline). Link it to something and the change sticks; do anything else and the deleted link comes back.
-
Removing a node reattaches any children that would be left parentless to its parent, so every node keeps at least one.
-
The root node can't be removed.
-
The id in the editor reads as
YYYY-MM-DD HH:mm:ss; hover it for the raw timestamp. Links show no position, since they don't have one.
collapsed: true is written on collapsed nodes so the view survives a reload, weak: true on cross-links, and color holds a palette name.
On import, destination is accepted as an alias for target and loose for weak.
Saving opens the browser's native save dialog where it's supported (Chromium's showSaveFilePicker, so you pick both name and folder).
Elsewhere it asks for a filename and drops the file in your downloads folder.