Skip to content

Say where you are: a rail for rows, a dial for the disc - #49

Merged
Paururo merged 4 commits into
mainfrom
feat/tree-minimap
Aug 30, 2026
Merged

Say where you are: a rail for rows, a dial for the disc#49
Paururo merged 4 commits into
mainfrom
feat/tree-minimap

Conversation

@Paururo

@Paururo Paururo commented Aug 30, 2026

Copy link
Copy Markdown
Member

On a tree of two million tips a wheel notch will take a reader to seven rows and
nothing on the canvas says so. Fit is the only way back, and it throws away where
they were. This adds the rail: the whole tree, at the height of the canvas, down
the right hand side, with the rows on screen marked on it.

It cannot disagree with the tree

The rail is drawn from the same select the main view is drawn from, over the
whole row range instead of the window, at the canvas's own height. That is the
point of the first commit here, which lifts the choice of what to draw out of the
middle of drawing it: with one selector there is no second opinion to have. It is
built once per load and again only if the canvas changes height, which on
2,097,151 nodes is 9,822 branches and about 1.5 ms.

Per frame it costs a re-stroke of those branches. Measured on a 500,000 node tree
at 900 by 520: a paint with the rail is 3.0 ms and panning holds under a
millisecond a frame.

What a hand can do with it

Click, drag or wheel on the rail and the window goes to those rows, keeping how
many rows are in it. Measured on the real page against the million tip demo:
clicking at 5, 35, 65 and 95 percent down the rail lands on tips 47,242, 347,242,
647,242 and 947,242 of a million, and dragging from 20 to 80 percent walks
through 197,242, 397,242, 597,242 and 797,242. Wheeling down moves the mark down
the rail and wheeling back up returns it to the pixel it started on.

A wheel on the rail scrolls rather than zooms, because a zoom there would be
anchored on the wrong row: the two sides of the canvas are at different scales.
Which side a gesture began on decides what it is for its whole life, so a drag
that starts on the rail and wanders onto the tree goes on moving the window.

The small print

Seven rows in two million is a thousandth of a pixel, so the mark is held at four
pixels and kept on the rail rather than allowed to hang off either end. Below 420
pixels of canvas width there is no rail at all and the tree gets the width back,
which is what a phone gets: verified at 375 px, where the pixels where the rail
would be come back fully transparent.

panBy grew a dx it drops on purpose, and now says so: the depth axis is
fitted to what is drawn on every paint, so nudging it sideways would be undone
before the frame reached the screen.

Checks

Eight new cases in tests/tree-canvas.test.js, which CI runs. Two of them are
there to stop the others passing vacuously: one asserts that a rail built from
the window would follow a zoom where the real one does not, and one asserts that
scrubbing to the top and to the bottom of the rail land more than 700 px apart,
which a dropped or constant mapping would fail. The fake 2d context gained
fillRect and strokeRect and now records them, so the mark's size, position
and colours are checked rather than assumed.

And the same idea, bent round

The crate has three projections and the page had one. The rail does not fit a
circle, because a circle has no top and bottom to scroll between, so the
circular view gets a dial instead: the whole disc, small, in the corner, with
the window drawn on it. Same idea, shape following the projection.

The disc is the crate's own arithmetic and not another one that resembles it.
radial.rs says a radial tree is "the rectangular one in polar coordinates: the
same rows, with depth becoming radius and row becoming angle", and the canvas
uses that formula with the crate's own defaults: the sweep starts at the top of
the circle, goes all the way round, and leaves a hole eight percent of the way
out. A check writes that formula out again from the crate's source and compares
it against where(node): the two agree to a thousandth of a pixel, and moving
the hole from 0.08 to 0.10 or the start from -90 to 0 degrees fails it.

Switching projection asks the program for nothing. The rows and depths are the
same numbers read a different way, so there is no round trip to the worker and
a check pins that the layout arrays come back untouched.

Two bounds, because a circle needs them

A million tips draws at fourteen milliseconds a frame at full extent and under
two milliseconds anywhere in a zoom. Getting there took two rules, and the first
version of the first one was wrong in a way worth recording.

Every walk heads inward, so a walk that has gone further in than the window can
see has nothing left to draw. Stopping instead on "off the canvas" seemed
equivalent and is not: a tip outside the window walking inward was cut off
before it reached the part of its own lineage that is inside it, and the picture
came out with 1,423 of its 1,430 branches off screen. The rule is a radius, not
a box.

And a branch further out than the window can see is walked past rather than
drawn. On the worst gesture, zooming into the middle of a two million node disc,
that takes 24,051 branches down to 2. Rendered pixel by pixel against the same
view without the rule, at four zooms, the ink is identical: 544,112 pixels
against 544,112 at full extent, 41,623 against 41,623 zoomed in, one pixel
different out of 5,555 at the deepest.

The sample budget is the circumference of the widest circle the window can see
rather than of the rim, which is not the same thing once the rim is off the
canvas: measuring against the rim asked for six thousand samples of a region
that holds a few hundred.

Export says --projection circular, which is a flag the crate already had.

Which nodes a run of rows comes down to, and the span in x that holds them,
were written inline in the middle of painting them. Both are now named
functions the paint calls, so a second drawing pass over the same tree
cannot quietly pick a different set and disagree with the first about the
shape of it. Nothing about the picture changes: the same 9,822 branches
come out of a two million node tree in the same 1.7 ms.
A wheel notch will take a reader to seven rows out of two million and
nothing on the canvas said so, with Fit the only way back and it throwing
away where they were. The rail down the right is the whole tree at the
height of the canvas, drawn once from the same selection the main view is
drawn from so it cannot disagree with it, and the block on it is the rows
on screen: clicking, dragging or wheeling there goes to those rows and
keeps how many of them there are. It holds its mark at four pixels because
seven rows in two million is a thousandth of one, stands down on a canvas
too narrow to spare the width, and is checked by eight new cases that fail
when the rail is built from the window instead of the tree.
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests The test suites and the property net labels Aug 30, 2026
A review of the rail found five things and measurement backed all of them.
The walk up to the root has no bound on a ladder, where every tip hangs off
one spine: a hundred and twenty thousand tip caterpillar cost a hundred and
fifty eight milliseconds a frame and stayed expensive when zoomed into, so
the walk now steps over an ancestor that would be drawn inside the same
pixel as the branch below it and that tree costs nine. Compared against the
old selection pixel by pixel the ink does not move: at a deep zoom the two
draw the same number of branches and every differing pixel is one of the
tip names that used to run under the rail.

The rail is drawn once onto a canvas of its own and stamped from there
rather than re-stroked, which took a frame with fifty branches on it from
ten milliseconds to none. Clicks measured the box around the canvas rather
than the canvas, and that box has a one pixel border, so every one of them
landed three thousand rows low at two million tips. The rail's ink was
1.96 to 1 on white, which is a silhouette nobody can see, and its mark was
a fourth hue on a three hue page: they are now 3.75 to 1 and the page's own
accent. A gesture belongs to the pointer that began it and ends on lost
capture as well, and a canvas with no size in the stylesheet can no longer
grow without limit.
The page had one projection where the crate has three, and the rail only
fits the one it had: a circle has no top and bottom to scroll between. So
the circular view draws the same layout in the crate's own polar arithmetic,
checked against the formula in radial.rs rather than against itself, and the
small picture of the whole tree becomes a dial in the corner with the window
drawn on it. Nothing is asked of the program to switch: the rows and depths
are the same numbers, read as angles and radii.

A million tips comes out at fourteen milliseconds a frame at full extent and
under two anywhere in a zoom, which took two bounds. A walk that has gone
further in than the window can see has nothing left to draw, and a branch
further out than the window can see is walked past rather than drawn: the
second one takes twenty four thousand branches down to two on the worst
gesture and, compared pixel by pixel against the same view without it, does
not change the picture at all.
@Paururo Paururo changed the title Give the tree a rail that says where you are Say where you are: a rail for rows, a dial for the disc Aug 30, 2026
@Paururo
Paururo merged commit d9edcc2 into main Aug 30, 2026
13 checks passed
@Paururo
Paururo deleted the feat/tree-minimap branch August 30, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation tests The test suites and the property net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant