Skip to content
Draft
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
108 changes: 108 additions & 0 deletions examples/timeline-layers/timeline-layer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Timeline Layer - Prototype

An interactive timeline visualization layer for deck.gl. This is a **working prototype** demonstrating core timeline functionality for video editing, animation sequencing, and temporal data visualization.

## Current Status

This prototype successfully demonstrates:

- Multi-track timeline rendering with clip visualization
- Interactive scrubber with drag support
- Zoom and pan controls for temporal navigation
- Clip and track selection with hover states
- Collision detection for overlapping clips (subtrack assignment)
- Multiple time formatting options
- Performance-optimized rendering with memoization

## Remaining Work

### 1. Critical Bugs to Fix

- **Scrubber Position After Window Resize**: When the window is resized and the user immediately drags the scrubber, the scrubber position jumps incorrectly. The coordinate system calculation needs investigation - likely related to how mouse coordinates are translated to timeline coordinates after dimension changes.

### 2. API Design & Internal Interactivity

**Critical Priority**: The current implementation exposes significant internal complexity to users. A production-ready timeline layer requires:

- **Simplified Props Interface**: Reduce the number of exposed props while maintaining flexibility
- **Internal State Management**: Move interaction state (panning, zooming, scrubber dragging) inside the layer
- **Hover State Management**: Currently hover states (hoveredClipId, hoveredTrackId) are managed externally. The layer should handle hover state internally and only expose necessary callbacks.
- **Callback Consolidation**: Streamline the event callback system (currently: onClipClick, onClipHover, onTrackClick, onTrackHover, onScrubberDrag, onViewportChange, onZoomChange)
- **Declarative Configuration**: Users should specify what they want, not how to achieve it
- **Default Behaviors**: Provide sensible defaults for common use cases (video editor vs. analytics timeline)

The goal: Users should be able to create a functional timeline with 5-10 props, not 30+.

### 3. Official Documentation

This layer needs comprehensive documentation matching the standards of other deck.gl layers:

- **API Reference**: Complete PropTypes documentation with descriptions, types, and defaults
- **Usage Guide**: Common patterns and best practices
- **Examples**: Multiple real-world use cases (video editing, analytics, scheduling)
- **Architecture Overview**: How the layer works internally (collision detection, sublayers, viewport management)
- **Migration Guide**: How to integrate into existing deck.gl projects

Reference: [deck.gl Layer Catalog](https://deck.gl/docs/api-reference/layers)

### 4. Testing & Validation

- Unit tests for collision detection and time calculations
- Integration tests for interaction behaviors
- Performance benchmarks with large datasets (1000+ clips)
- Cross-browser compatibility testing

### 5. Additional Features

- Clip resizing (drag edges to extend/trim)
- Multi-selection support
- Keyboard shortcuts (arrow keys, space for play/pause)
- Clip drag-and-drop between tracks
- Undo/redo support
- Timeline markers and regions
- Customizable track heights
- Nested timeline groups

## Running the Demo

```bash
yarn install
yarn start
```

Open [http://localhost:5173](http://localhost:5173)

## Architecture

```
timeline-layer.ts # Main CompositeLayer implementation
timeline-types.ts # TypeScript type definitions
timeline-utils.ts # Time formatting and position calculations
timeline-collision.ts # Subtrack assignment algorithm
timeline-hooks.ts # React hooks for interaction state
demo-controls.tsx # Demo control panel
app.tsx # Demo application
```

## Core Interactions Implemented

- **Mouse Wheel**: Zoom timeline in/out
- **Click + Drag**: Pan when zoomed (zoom level > 1.0)
- **Click Clip**: Select clip and show details
- **Click Track Background**: Deselect clip, select track
- **Click Empty Space**: Deselect both clip and track
- **Drag Scrubber**: Scrub through timeline
- **Hover**: Clips and tracks lighten on hover

## Contributing

This prototype demonstrates feasibility and core functionality. To make it production-ready:

1. Review the "Remaining Work" section above
2. Start with API simplification - gather feedback on ideal developer experience
3. Create comprehensive documentation following deck.gl standards
4. Add tests and validate performance at scale

---

**Note**: This is experimental code intended for evaluation and feedback. Not recommended for production use without addressing the items in "Remaining Work".
213 changes: 213 additions & 0 deletions examples/timeline-layers/timeline-layer/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors


import React, {ReactElement, useMemo} from 'react';
import {createRoot} from 'react-dom/client';


import DeckGL from '@deck.gl/react';
import {OrthographicView} from '@deck.gl/core';
import {TimelineLayer} from '@deck.gl-community/timeline-layers';
import {useTimelineControls, TimelineControls} from './demo-controls';

import {
useTimelineInteractionState,
useTimelineRefs,
useGlobalMouseUpCleanup,
useWheelZoom,
useTimelineCallbacks,
useContainerHandlers,
useDeckGLHandlers,
useCursorGetter,
useTimelineResize
} from './timeline-hooks';

const canvasWidth = (typeof window !== 'undefined' ? window.innerWidth : 1280) - 320;
const canvasHeight = typeof window !== 'undefined' ? window.innerHeight : 720;

const INITIAL_VIEW_STATE: OrthographicViewState = {
target: [canvasWidth / 2, canvasHeight / 2, 0],
zoom: 0
};

// Memoized constants
const ORTHOGRAPHIC_VIEW = new OrthographicView();
const CONTROLLER_CONFIG = {
scrollZoom: false,
doubleClickZoom: false,
touchZoom: false,
dragPan: false,
dragRotate: false,
keyboard: false
} as const;

const CONTAINER_OUTER_STYLE = {
display: 'flex',
width: '100vw',
height: '100vh',
overflow: 'hidden'
} as const;

const CONTAINER_INNER_STYLE = {
flex: 1,
height: '100vh',
position: 'relative' as const,
overflow: 'hidden',
border: '3px solid black'
} as const;

export default function App(): ReactElement {
const {state, controls, trackCount, clipsPerTrack, labelFormatterType} = useTimelineControls();

const {state: interactionState, setState} = useTimelineInteractionState();
const {timelineLayerRef, containerRef} = useTimelineRefs();

useGlobalMouseUpCleanup(
setState.setIsDraggingScrubber,
setState.setIsPanning,
setState.setPanStartViewport
);

useWheelZoom(containerRef, timelineLayerRef, state.zoomLevel);
useTimelineResize(controls.setTimelineWidth);

const timelineCallbacks = useTimelineCallbacks(controls);

const containerHandlers = useContainerHandlers(
interactionState,
setState,
timelineLayerRef,
controls,
state
);

const deckGLHandlers = useDeckGLHandlers(setState, timelineLayerRef, controls, state);

const getCursor = useCursorGetter(interactionState, state.zoomLevel);

// Memoize viewport object
const viewport = useMemo(
() => ({startMs: state.viewportStartMs, endMs: state.viewportEndMs}),
[state.viewportStartMs, state.viewportEndMs]
);

// Memoize selectionStyle object
const selectionStyle = useMemo(
() => ({
selectedClipColor: [255, 200, 0, 255] as [number, number, number, number],
hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number],
selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number],
hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number],
selectedLineWidth: state.selectedLineWidth,
hoveredLineWidth: state.hoveredLineWidth
}),
[state.selectedLineWidth, state.hoveredLineWidth]
);

const timelineLayerProps = useMemo(
() => ({
id: 'timeline',
data: state.tracks,
timelineStart: state.timelineStart,
timelineEnd: state.timelineEnd,
x: state.timelineX,
y: state.timelineY,
width: state.timelineWidth,
trackHeight: state.trackHeight,
trackSpacing: state.trackSpacing,
currentTimeMs: state.currentTimeMs,
viewport,
timeFormatter: state.labelFormatter,
selectedClipId: state.selectedClip?.id,
hoveredClipId: state.hoveredClip?.id,
selectedTrackId: state.selectedTrack?.id,
hoveredTrackId: state.hoveredTrack?.id,
showTrackLabels: true,
showClipLabels: true,
showScrubber: true,
showAxis: true,
showSubtrackSeparators: state.showSubtrackSeparators,
selectionStyle,
onClipClick: timelineCallbacks.handleClipClick,
onClipHover: timelineCallbacks.handleClipHover,
onTrackClick: timelineCallbacks.handleTrackClick,
onTrackHover: timelineCallbacks.handleTrackHover,
onScrubberDrag: timelineCallbacks.handleScrubberDrag,
onTimelineClick: timelineCallbacks.handleTimelineClick,
onViewportChange: timelineCallbacks.handleViewportChange,
onZoomChange: timelineCallbacks.handleZoomChange
}),
[
state.tracks,
state.timelineStart,
state.timelineEnd,
state.timelineX,
state.timelineY,
state.timelineWidth,
state.trackHeight,
state.trackSpacing,
state.currentTimeMs,
viewport,
state.labelFormatter,
state.selectedClip?.id,
state.hoveredClip?.id,
state.selectedTrack?.id,
state.hoveredTrack?.id,
state.showSubtrackSeparators,
selectionStyle,
timelineCallbacks
]
);

const timelineLayer = useMemo(
() => new TimelineLayer(timelineLayerProps),
[timelineLayerProps]
);

// Update ref when layer changes
React.useEffect(() => {
timelineLayerRef.current = timelineLayer;
}, [timelineLayer]);

const layers = useMemo(() => [timelineLayer], [timelineLayer]);

return (
<div style={CONTAINER_OUTER_STYLE}>
<TimelineControls
state={state}
controls={controls}
trackCount={trackCount}
clipsPerTrack={clipsPerTrack}
labelFormatterType={labelFormatterType}
/>

<div
ref={containerRef}
data-timeline-container="true"
style={CONTAINER_INNER_STYLE}
onMouseDown={containerHandlers.handleContainerMouseDown}
onMouseMove={containerHandlers.handleContainerMouseMove}
onMouseUp={containerHandlers.handleContainerMouseUp}
>
<DeckGL
views={ORTHOGRAPHIC_VIEW}
initialViewState={INITIAL_VIEW_STATE}
controller={CONTROLLER_CONFIG}
layers={layers}
pickingRadius={10}
useDevicePixels={false}
onHover={deckGLHandlers.handleDeckGLHover}
onClick={deckGLHandlers.handleDeckGLClick}
getCursor={getCursor}
/>
</div>
</div>
);
}


const root = createRoot(document.getElementById('app')!);
root.render(<App />);

Loading
Loading