Skip to content

Timeline: move the current time to a layer's own data from its row #407

Description

@sandesh-sp

Timeline: move the current time to a layer's own data from its row

Motivation

The timeline moves by clock time, not by data. The scrubber lands wherever the pointer does, and the step buttons advance by a fixed calendar unit. For a layer holding data on a scattered handful of days, that makes landing on one of those days a pixel-hunt across a domain spanning years, and stepping there costs one press per intervening day.

A global "snap to data" setting can't fix it, because it has to pick whose data it means — and one timeline routinely carries sparse layers, continuously covered layers, and layers that ignore time entirely, all at once. Every global answer is wrong for some mission.

How it should work

Each layer row in the timeline sidebar carries its own set of controls — first, previous, next, last — that move the timeline's current time to that layer's data. The layer under the cursor is the scope, so there's no mode to set and nothing to configure.

  • A sparse layer walks the days it holds data on. Previous and next step between them; first and last jump to the earliest and latest.
  • A continuous layer steps across its coverage. For now that's by the timeline's current granularity — year, month, day or hour — and later by the layer's own declared periodicity. First and last go to the ends of its coverage.
  • A layer that ignores time shows no controls, since it has nothing to navigate to.

Pressing back or forward on a layer whose data lies far from where you are now takes you to that data in one press, rather than stepping toward it. If a layer's data lies outside the timeline's window, using its controls widens the window to reach it.

The controls appear when a row is hovered or when they take keyboard focus, and are hidden otherwise, so a timeline with many layers stays legible and layer names keep their full width.

The scrubber itself is unchanged — it stays free, with no snapping.

Done when

  • Every visible layer that carries time information shows first/previous/next/last controls on its row; layers with time switched off show none.
  • On a sparse layer, next and previous land exactly on its data days, and first/last on its earliest and latest.
  • On a continuous layer, next and previous step by the timeline's granularity within its coverage, and first/last land on its ends.
  • Pressing back on a layer whose data ended months ago lands on its last data in one press.
  • A control whose target lies outside the timeline's window widens the window to reach it, rather than stopping at the edge.
  • A control with nowhere left to go is visibly inert rather than silently doing nothing.
  • The controls are reachable and operable by keyboard, and each is distinguishable by layer to a screen reader.
  • Navigating to a layer's day actually shows that day's data on the map.

Out of scope

  • Scrubber snapping or magnetism of any kind — considered and deliberately dropped.
  • Playback that walks a layer's data days rather than the global granularity.
  • The layer periodicity configuration field itself, which lands separately. Until it exists, continuous layers step by the timeline's granularity.
Draft implementation plan — written as of 73b3d8c on 2026-09-08. Rough guide; re-verify against latest code.

Current behavior

The Timeline plugin lives in src/essence/Tools/Timeline/. TimelineView.tsx owns the scrubber (free drag, click-to-seek, arrow keys), and TimelineAdapter.tsx owns stepping and playback — both via stepTime(current, timeMode, ±1) in lib/utils/timeUtils.ts.

resolveLayerTimeRanges in that same file already reads a layer's time block into drawable spans: a continuous layer becomes one bar across its configured extent, and a layer listing time.dataDates becomes one whole-day box per listed day. That parsing — trimmed, strict ISO 8601, UTC-bounded — is what the navigation model should reuse.

The sidebar row is built in TimelineView.tsx from .layer-item, styled in Timeline.css, and is currently a colour dot plus a name.

Where the change lands & rough plan

  1. A new lib/utils/layerNavigation.ts: resolveLayerNavigation(time) turning a layer's config into a small model (sparse stops, or a periodic extent with an optional cadence) and returning null for a layer with nothing to navigate; navigateLayer(nav, from, action, mode) answering where a button leads, or null to disable it. Keeping it out of timeUtils.ts keeps that file's job to formatting and stepping.
  2. A LayerNavControls component under lib/geo/, rendered inside .layer-item, using the transport glyphs already in PlaybackControls.tsx. Reveal via .layer-item:hover and :focus-within, with opacity rather than display so the buttons stay in the tab order.
  3. layerBarHeight in TimelineView.tsx grows from 15 to 20 to fit a button. It's the pitch of the sidebar row and the SVG row — they must stay equal or the columns desync — so LayerTimeline.tsx should derive its bar thickness from a constant instead of the hardcoded y + 4 / height - 6, keeping the chart from getting heavier.
  4. TimelineAdapter.tsx resolves a navigation model per visible layer alongside resolveLayerTimeRanges, and commits a target through a handler separate from handleCurrentTimeChange — that one clamps to the window, and reaching a layer may mean widening it.

⚠️ Gotcha: the current time is not a point a layer is sampled at — TimeControl.updateLayersTime in src/essence/Basics/TimeControl_/TimeControl.js gives every time-enabled layer layer.time.end = TimeControl.currentTime, making it the trailing edge of the layer's query window. So navigation must land on the end of a sparse layer's day, not its start; landing at T00:00:00Z closes the window before any of that day's data falls inside it, and the button walks you onto a data day that shows nothing.

⚠️ Gotcha: a mission configured with relative start/end has its window recomputed from the current time by TimeControl.setTime, so an absolute widening won't stick there. The current time still lands on target and the rolling window follows it — worth knowing before debugging it as a bug.

References

  • Design spec: docs/superpowers/specs/2026-09-08-per-layer-timeline-navigation-design.md
  • Task-by-task plan: docs/superpowers/plans/2026-09-08-per-layer-timeline-navigation.md
  • Existing sparse-layer parsing and its tests: lib/utils/timeUtils.ts, __tests__/layerTimeRanges.spec.ts

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions