@kteneyck/cesium-timeline-angular 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,869 @@
1
+ # @kteneyck/cesium-timeline
2
+
3
+ A canvas-based timeline component for **React** and **Angular** with Cesium Clock integration. Provides interactive time scrubbing, smooth edge-scroll, Netflix/Hulu-style playback controls, a LIVE indicator, and a flexible token-based datetime format system.
4
+
5
+ ## Packages
6
+
7
+ | Package | Description | npm |
8
+ |---|---|---|
9
+ | `@kteneyck/cesium-timeline-core` | Framework-agnostic types, utils, canvas engine | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-core)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-core) |
10
+ | `@kteneyck/cesium-timeline-react` | React components (thin wrappers around core) | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-react)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-react) |
11
+ | `@kteneyck/cesium-timeline-angular` | Angular standalone components (Angular 17+) | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-angular)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-angular) |
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ### React
18
+
19
+ ```bash
20
+ npm install @kteneyck/cesium-timeline-react @kteneyck/cesium-timeline-core
21
+ ```
22
+
23
+ Peer dependencies: `react` ≥ 19, `cesium` ≥ 1.100
24
+
25
+ ### Angular
26
+
27
+ ```bash
28
+ npm install @kteneyck/cesium-timeline-angular @kteneyck/cesium-timeline-core
29
+ ```
30
+
31
+ Peer dependencies: `@angular/core` ≥ 17, `cesium` ≥ 1.100
32
+
33
+ ---
34
+
35
+ ## Basic Usage
36
+
37
+ ### React
38
+
39
+ ```tsx
40
+ import { Timeline } from '@kteneyck/cesium-timeline-react';
41
+
42
+ const MyComponent = () => {
43
+ const viewer = /* your Cesium Viewer */;
44
+
45
+ return (
46
+ <Timeline
47
+ clock={viewer.clock}
48
+ height={120}
49
+ onTimeChange={(t) => { viewer.clock.currentTime = t; }}
50
+ onPlayPause={(playing) => { viewer.clock.shouldAnimate = playing; }}
51
+ onMultiplierChange={(m) => { viewer.clock.multiplier = m; }}
52
+ />
53
+ );
54
+ };
55
+ ```
56
+
57
+ When `clock` is provided the component subscribes to `clock.onTick` and stays in sync automatically. All `onTimeChange`, `onPlayPause`, and `onMultiplierChange` callbacks are optional — the controls still work without them if you only pass `clock`.
58
+
59
+ ### Without a Cesium Clock
60
+
61
+ ```tsx
62
+ <Timeline
63
+ startTime={new Date('2026-01-01T00:00:00')}
64
+ endTime={new Date('2026-12-31T23:59:59')}
65
+ height={80}
66
+ showControls={false}
67
+ onTimeChange={(t) => console.log(Cesium.JulianDate.toIso8601(t))}
68
+ />
69
+ ```
70
+
71
+ When no `clock` is provided the component falls back to `setInterval` and tracks real wall-clock time.
72
+
73
+ ### Angular
74
+
75
+ ```typescript
76
+ import { Component } from '@angular/core';
77
+ import { TimelineComponent } from '@kteneyck/cesium-timeline-angular';
78
+ import * as Cesium from 'cesium';
79
+
80
+ @Component({
81
+ selector: 'app-root',
82
+ standalone: true,
83
+ imports: [TimelineComponent],
84
+ template: `
85
+ <ct-timeline
86
+ [clock]="viewer.clock"
87
+ [height]="120"
88
+ (timeChange)="onTimeChange($event)"
89
+ (playPause)="onPlayPause($event)"
90
+ (multiplierChange)="onMultiplierChange($event)"
91
+ />
92
+ `,
93
+ })
94
+ export class AppComponent {
95
+ viewer!: Cesium.Viewer;
96
+
97
+ onTimeChange(t: Cesium.JulianDate) {
98
+ this.viewer.clock.currentTime = t;
99
+ }
100
+
101
+ onPlayPause(playing: boolean) {
102
+ this.viewer.clock.shouldAnimate = playing;
103
+ }
104
+
105
+ onMultiplierChange(m: number) {
106
+ this.viewer.clock.multiplier = m;
107
+ }
108
+ }
109
+ ```
110
+
111
+ Angular components use standalone imports — no NgModule required. Selectors: `ct-timeline`, `ct-timeline-canvas`, `ct-timeline-controls`.
112
+
113
+ ---
114
+
115
+ ## Features
116
+
117
+ - **Canvas rendering** — zero framework re-renders during playback or drag; all mutable state lives in refs/class properties.
118
+ - **Shared core engine** — identical visual output in React and Angular via `@kteneyck/cesium-timeline-core`.
119
+ - **Cesium Clock sync** — subscribes to `clock.onTick`; respects `shouldAnimate`, `multiplier`, and `currentTime`.
120
+ - **Draggable needle** — grab and drag the current-time indicator to scrub; cursor changes to `grab`/`grabbing`.
121
+ - **Click-to-seek** — click anywhere on the timeline to jump to that time.
122
+ - **Edge scroll** — drag the needle within 8% of either edge and the visible window scrolls smoothly underneath. The needle stays pinned to the cursor position as the window shifts.
123
+ - **Auto-scroll during playback** — visible window pans automatically when the needle reaches 10% from either edge.
124
+ - **Infinite scrolling window** — timeline is not clamped to `startTime`/`endTime`; the window can pan anywhere.
125
+ - **Adaptive tick labels** — label granularity adapts to zoom level: milliseconds → seconds → HH:MM:SS → HH:MM → Month Day → Month Year → Year. Tick dates are shown only when the visible window spans more than 24 hours.
126
+ - **Local time labels** — ticks and dates reflect the user's local timezone, not UTC.
127
+ - **Netflix/Hulu-style controls** — transport buttons (⏮ ◀◀ ▶/⏸ ▶▶ ⏭) always stay centered; speed badge and LIVE button in the left column never cause layout shift.
128
+ - **Conditional start/end buttons** — ⏮ and ⏭ are only rendered when `startTime` and `endTime` props are explicitly provided.
129
+ - **Speed cycling** — FF cycles through `ffSpeeds` (default `2×→4×→8×→16×→32×→1×`); RW cycles through `rwSpeeds` (default `−1×→−2×→−4×→−8×→−16×→−32×`). Both arrays are fully configurable.
130
+ - **LIVE button** — filled `● LIVE` when within 10 s of wall clock; dim outline `LIVE` otherwise; clicking jumps to `Date.now()` and resets speed to 1×.
131
+ - **Speed badge** — shown in the left column when multiplier ≠ 1×; click to reset to 1×.
132
+ - **Two-line datetime display** — time displayed large/bold; date displayed smaller in the theme's active color.
133
+ - **Clickable datetime** — pass `onDateTimeClick` to open your own date picker; pass the result back via `jumpToTime` to pan the canvas and set the time.
134
+ - **Token-based datetime format** — built-in presets plus custom format strings with 17 supported tokens.
135
+ - **Max tick limit** — `maxTicks` prop prevents the canvas from becoming overloaded at wide zoom levels by coarsening the tick scale automatically.
136
+ - **Swim lanes** — display time intervals and instants as horizontal rows inside the canvas. Supports customizable styling, click/hover/double-click event hooks, drag-to-reorder, and vertical scrolling when lanes overflow.
137
+ - **Fully themeable** — 16 theme properties cover every color, size, and font setting, including swim lane item border defaults.
138
+ - **Responsive** — fills container width; `ResizeObserver` redraws on resize.
139
+
140
+ ---
141
+
142
+ ## Props
143
+
144
+ ### `TimelineProps`
145
+
146
+ | Prop | Type | Default | Description |
147
+ |------|------|---------|-------------|
148
+ | `clock` | `Cesium.Clock` | — | Cesium clock to sync with. Falls back to `setInterval` if omitted. |
149
+ | `startTime` | `JulianDate \| Date` | now − 12 h | Left bound of initial visible window. Also shows the ⏮ button when provided. |
150
+ | `endTime` | `JulianDate \| Date` | now + 12 h | Right bound of initial visible window. Also shows the ⏭ button when provided. |
151
+ | `currentTime` | `JulianDate \| Date` | `startTime` | Initial needle position |
152
+ | `height` | `number` | `120` | Canvas height in pixels |
153
+ | `showControls` | `boolean` | `true` | Show/hide the control bar |
154
+ | `enableDrag` | `boolean` | `true` | Show/hide the canvas (drag/seek area) |
155
+ | `showLabels` | `boolean` | — | Show/hide tick labels on the canvas |
156
+ | `snapToTicks` | `boolean` | — | Snap needle to nearest tick on drag |
157
+ | `tickInterval` | `TickInterval \| number` | auto | Override automatic tick interval |
158
+ | `maxTicks` | `number` | unlimited | Maximum number of major ticks on the canvas at once. When exceeded the tick scale is automatically coarsened. |
159
+ | `ffSpeeds` | `number[]` | `[2,4,8,16,32,1]` | Speed steps cycled by the ▶▶ button. Last entry wraps back to first. |
160
+ | `rwSpeeds` | `number[]` | `[1,2,4,8,16,32]` | Absolute-value speed steps cycled by the ◀◀ button (negated internally). |
161
+ | `dateTimeFormat` | `string` | `'MMM DD YYYY HH:mm:ss'` | Token-based format string for the controls datetime display |
162
+ | `onDateTimeClick` | `() => void` | — | Called when the user clicks the datetime display. Use to open your own date picker. |
163
+ | `jumpToTime` | `JulianDate \| Date` | — | Set to programmatically jump the timeline to a moment (pans canvas + sets time). |
164
+ | `theme` | `Partial<TimelineTheme>` | `defaultTheme` | Theme overrides (merged with defaults) |
165
+ | `className` | `string` | — | CSS class applied to the root div |
166
+ | `onTimeChange` | `(t: JulianDate) => void` | — | Fires when needle moves (drag, click, or clock tick) |
167
+ | `onPlayPause` | `(playing: boolean) => void` | — | Fires on play/pause toggle |
168
+ | `onMultiplierChange` | `(m: number) => void` | — | Fires when speed changes |
169
+ | `swimLanes` | `SwimLane[]` | — | Array of swim lane definitions to render on the canvas |
170
+ | `showSwimLanes` | `boolean` | `true` | Show or hide the swim lanes |
171
+ | `onSwimLaneItemClick` | `(info: SwimLaneEventInfo) => void` | — | Fires when a swim lane item is clicked |
172
+ | `onSwimLaneItemHover` | `(info: SwimLaneEventInfo \| null) => void` | — | Fires when mouse enters/leaves a swim lane item |
173
+ | `onSwimLaneItemDoubleClick` | `(info: SwimLaneEventInfo) => void` | — | Fires when a swim lane item is double-clicked |
174
+ | `onSwimLaneReorder` | `(orderedIds: string[]) => void` | — | Fires when swim lanes are reordered via drag. Receives the new lane id order. |
175
+
176
+ ---
177
+
178
+ ## Theme
179
+
180
+ Pass a partial `TimelineTheme` object to the `theme` prop. Any omitted properties fall back to `defaultTheme`.
181
+
182
+ ```tsx
183
+ <Timeline
184
+ clock={viewer.clock}
185
+ theme={{
186
+ backgroundColor: '#111',
187
+ indicatorColor: '#ffd54f',
188
+ buttonActiveColor: '#ffd54f',
189
+ }}
190
+ />
191
+ ```
192
+
193
+ ### `TimelineTheme` Properties
194
+
195
+ | Property | Default | Description |
196
+ |----------|-----------|-------------|
197
+ | `backgroundColor` | `#1a1a1a` | Canvas background colour |
198
+ | `tickColor` | `#666` | Minor tick stroke colour |
199
+ | `majorTickColor` | `#999` | Major tick stroke colour |
200
+ | `labelColor` | `#ccc` | Tick label and datetime text colour |
201
+ | `indicatorColor` | `#d69826` | Needle (current-time line) colour |
202
+ | `indicatorLineWidth` | `5` | Needle stroke width in px |
203
+ | `majorTickHeight` | `10` | Major tick height in px |
204
+ | `minorTickHeight` | `5` | Minor tick height in px |
205
+ | `fontSize` | `12` | Tick label font size in px |
206
+ | `controlBarBackground` | `#242424` | Control bar background colour |
207
+ | `controlBarBorder` | `#333` | Control bar bottom border colour |
208
+ | `buttonColor` | `#666` | Normal button colour |
209
+ | `buttonHoverColor` | `#888` | Button hover colour |
210
+ | `buttonActiveColor` | `#d69826` | Active buttons, LIVE, speed badge, and date line colour |
211
+ | `swimLaneItemBorderColor` | `#666666` | Default border colour for swim lane interval bars. Can be overridden per-lane or per-item. |
212
+ | `swimLaneItemBorderWidth` | `0` | Default border width (px) for swim lane interval bars. Set to `0` to remove borders globally. Can be overridden per-lane or per-item. |
213
+
214
+ > **Note:** Theme colours must be resolved hex/rgb values. CSS variables like `var(--primary-color)` do **not** work in canvas `ctx.fillStyle`. Use `getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim()` to resolve them first.
215
+
216
+ ### Resolving CSS Variables
217
+
218
+ ```tsx
219
+ const theme = useMemo(() => {
220
+ const style = getComputedStyle(document.documentElement);
221
+ return {
222
+ indicatorColor: style.getPropertyValue('--primary-color').trim() || '#ffd54f',
223
+ backgroundColor: style.getPropertyValue('--surface-ground').trim() || '#1a1a1a',
224
+ };
225
+ }, []);
226
+
227
+ <Timeline clock={viewer.clock} theme={theme} />
228
+ ```
229
+
230
+ ---
231
+
232
+ ## DateTime Format
233
+
234
+ The `dateTimeFormat` prop controls the two-line datetime display in the control bar. It accepts a token-based format string.
235
+
236
+ ### Built-in Presets (`DateTimeFormats`)
237
+
238
+ ```tsx
239
+ import { DateTimeFormats } from '@kteneyck/cesium-timeline-react';
240
+
241
+ <Timeline dateTimeFormat={DateTimeFormats.TWELVE_HR} ... />
242
+ ```
243
+
244
+ | Key | Format string | Example output |
245
+ |-----|--------------|----------------|
246
+ | `DEFAULT` | `MMM DD YYYY HH:mm:ss` | Feb 24 2026 14:04:07 |
247
+ | `TWELVE_HR` | `MMM DD YYYY hh:mm:ss A` | Feb 24 2026 02:04:07 PM |
248
+ | `ISO` | `YYYY-MM-DD HH:mm:ss` | 2026-02-24 14:04:07 |
249
+ | `US` | `MM/DD/YYYY HH:mm` | 02/24/2026 14:04 |
250
+ | `EU` | `DD/MM/YYYY HH:mm` | 24/02/2026 14:04 |
251
+ | `TIME_ONLY` | `HH:mm:ss` | 14:04:07 |
252
+ | `TIME_12` | `hh:mm:ss A` | 02:04:07 PM |
253
+
254
+ ### Supported Tokens
255
+
256
+ | Token | Example | Description |
257
+ |-------|---------|-------------|
258
+ | `YYYY` | 2026 | 4-digit year |
259
+ | `YY` | 26 | 2-digit year |
260
+ | `MMMM` | February | Full month name |
261
+ | `MMM` | Feb | Abbreviated month name |
262
+ | `MM` | 02 | Zero-padded month number |
263
+ | `M` | 2 | Month number |
264
+ | `DD` | 05 | Zero-padded day |
265
+ | `D` | 5 | Day |
266
+ | `HH` | 14 | 24-hour, zero-padded |
267
+ | `H` | 14 | 24-hour |
268
+ | `hh` | 02 | 12-hour, zero-padded |
269
+ | `h` | 2 | 12-hour |
270
+ | `mm` | 04 | Minutes, zero-padded |
271
+ | `ss` | 07 | Seconds, zero-padded |
272
+ | `SSS` | 042 | Milliseconds, zero-padded |
273
+ | `A` | PM | AM/PM uppercase |
274
+ | `a` | pm | AM/PM lowercase |
275
+
276
+ The control bar automatically splits the format string into a **time line** (large/bold) and a **date line** (smaller, in `buttonActiveColor`) using the `splitForDisplay` utility.
277
+
278
+ ### `formatDateTime` Utility
279
+
280
+ ```tsx
281
+ import { formatDateTime, DateTimeFormats } from '@kteneyck/cesium-timeline-react';
282
+
283
+ const label = formatDateTime(julianDate, DateTimeFormats.ISO);
284
+ // → "2026-02-24 14:04:07"
285
+
286
+ const label2 = formatDateTime(new Date(), 'DD/MM/YYYY HH:mm');
287
+ // → "24/02/2026 14:04"
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Playback Controls
293
+
294
+ The control bar uses a 3-column CSS grid so the transport buttons are always centered regardless of the content in the left (datetime/LIVE/badge) or right (empty spacer) columns.
295
+
296
+ ### Transport Buttons
297
+
298
+ | Button | Action |
299
+ |--------|--------|
300
+ | ⏮ | Jump to `startTime` — **only rendered when `startTime` prop is provided** |
301
+ | ◀◀ | Cycle reverse speeds through `rwSpeeds` (wraps) |
302
+ | ▶ / ⏸ | Play / Pause. If coming out of reverse, resets to 1× forward. |
303
+ | ▶▶ | Cycle forward speeds through `ffSpeeds` (wraps) |
304
+ | ⏭ | Jump to `endTime` — **only rendered when `endTime` prop is provided** |
305
+
306
+ ### LIVE Button
307
+
308
+ - Shows `● LIVE` (filled background) when the current time is within 10 seconds of `Date.now()`.
309
+ - Shows `LIVE` (dim outline) otherwise.
310
+ - Clicking jumps to `Date.now()`, centers the visible window ±12 h, and resets speed to 1×.
311
+
312
+ ### Speed Badge
313
+
314
+ - Appears to the right of LIVE when multiplier ≠ 1×.
315
+ - Shows `◀ N×` for reverse, `N× ▶` for fast-forward.
316
+ - Clicking resets to 1× speed.
317
+
318
+ ### Configuring Playback Speeds
319
+
320
+ ```tsx
321
+ // Gentle: 2× and 4× only
322
+ <Timeline ffSpeeds={[2, 4, 1]} rwSpeeds={[1, 2, 4]} ... />
323
+
324
+ // Scientific: fine-grained control
325
+ <Timeline ffSpeeds={[10, 100, 1000, 10000, 1]} rwSpeeds={[1, 10, 100, 1000]} ... />
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Date Picker Integration
331
+
332
+ Pass `onDateTimeClick` to make the datetime display clickable. When clicked, open your own picker. Pass the result back as `jumpToTime` to pan the canvas to the selected time.
333
+
334
+ ```tsx
335
+ const [jumpToTime, setJumpToTime] = useState<Date | undefined>();
336
+ const [pickerOpen, setPickerOpen] = useState(false);
337
+
338
+ <Timeline
339
+ clock={viewer.clock}
340
+ onDateTimeClick={() => setPickerOpen(true)}
341
+ jumpToTime={jumpToTime}
342
+ ...
343
+ />
344
+
345
+ {/* Your picker — any library works */}
346
+ {pickerOpen && (
347
+ <MyDatePicker
348
+ onSelect={(date) => {
349
+ setJumpToTime(date);
350
+ setPickerOpen(false);
351
+ }}
352
+ />
353
+ )}
354
+ ```
355
+
356
+ > `jumpToTime` is edge-triggered: the timeline reacts whenever the value changes. Wrap updates in a new `Date` instance (or use state) to ensure React detects the change.
357
+
358
+ ---
359
+
360
+ ## Exports
361
+
362
+ ### React
363
+
364
+ ```tsx
365
+ import {
366
+ Timeline, // Main component
367
+ TimelineCanvas, // Canvas component (imperative handle)
368
+ TimelineControls, // Transport controls
369
+ TimelineSVG, // SVG-based alternative renderer
370
+ } from '@kteneyck/cesium-timeline-react';
371
+ ```
372
+
373
+ ### Angular
374
+
375
+ ```typescript
376
+ import {
377
+ TimelineComponent, // <ct-timeline>
378
+ TimelineCanvasComponent, // <ct-timeline-canvas>
379
+ TimelineControlsComponent, // <ct-timeline-controls>
380
+ } from '@kteneyck/cesium-timeline-angular';
381
+ ```
382
+
383
+ ### Core (re-exported by both React and Angular packages)
384
+
385
+ ```tsx
386
+ import {
387
+ DateTimeFormats, // Format string presets
388
+ formatDateTime, // Token-based date formatter
389
+ splitForDisplay, // Split format string into time/date parts
390
+ toJulianDate, // Convert Date | JulianDate → JulianDate
391
+ toDate, // Convert Date | JulianDate → Date
392
+ toMilliseconds, // Convert ms → number
393
+ fromMilliseconds, // Convert ms → JulianDate
394
+ getDurationMs, // Duration between two dates in ms
395
+ TickInterval, // Enum: FIFTEEN_MIN | THIRTY_MIN | HOURLY | CUSTOM
396
+ defaultTheme, // Default theme values
397
+ defaultSwimLaneStyle,
398
+ DEFAULT_LANE_HEIGHT,
399
+ } from '@kteneyck/cesium-timeline-core';
400
+
401
+ // TypeScript types
402
+ import type {
403
+ TimelineTheme,
404
+ SwimLane,
405
+ SwimLaneItem,
406
+ SwimLaneItemStyle,
407
+ SwimLaneStyle,
408
+ SwimLaneEventInfo,
409
+ } from '@kteneyck/cesium-timeline-core';
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Examples
415
+
416
+ ### Full Cesium Integration
417
+
418
+ ```tsx
419
+ import { useRef, useEffect, useMemo, useState } from 'react';
420
+ import * as Cesium from 'cesium';
421
+ import { Timeline, DateTimeFormats } from '@kteneyck/cesium-timeline-react';
422
+
423
+ const CesiumWithTimeline = () => {
424
+ const containerRef = useRef<HTMLDivElement>(null);
425
+ const [clock, setClock] = useState<Cesium.Clock | undefined>();
426
+
427
+ useEffect(() => {
428
+ if (!containerRef.current) return;
429
+ const viewer = new Cesium.Viewer(containerRef.current);
430
+ viewer.clock.shouldAnimate = false;
431
+ setClock(viewer.clock);
432
+ return () => viewer.destroy();
433
+ }, []);
434
+
435
+ const theme = useMemo(() => ({
436
+ indicatorColor: '#ffd54f',
437
+ buttonActiveColor: '#ffd54f',
438
+ backgroundColor: '#1a1a1a',
439
+ }), []);
440
+
441
+ return (
442
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
443
+ <div ref={containerRef} style={{ flex: 1 }} />
444
+ {clock && (
445
+ <Timeline
446
+ clock={clock}
447
+ height={120}
448
+ showControls={true}
449
+ showLabels={true}
450
+ snapToTicks={false}
451
+ dateTimeFormat={DateTimeFormats.DEFAULT}
452
+ theme={theme}
453
+ onTimeChange={(t) => { clock.currentTime = t; }}
454
+ onPlayPause={(playing) => { clock.shouldAnimate = playing; }}
455
+ onMultiplierChange={(m) => { clock.multiplier = m; }}
456
+ />
457
+ )}
458
+ </div>
459
+ );
460
+ };
461
+ ```
462
+
463
+ ### With Start / End Bounds
464
+
465
+ Providing `startTime` and `endTime` shows the ⏮ and ⏭ jump buttons.
466
+
467
+ ```tsx
468
+ const start = Cesium.JulianDate.fromIso8601('2026-01-01T00:00:00Z');
469
+ const end = Cesium.JulianDate.fromIso8601('2026-12-31T23:59:59Z');
470
+
471
+ <Timeline
472
+ clock={viewer.clock}
473
+ startTime={start}
474
+ endTime={end}
475
+ height={120}
476
+ onTimeChange={(t) => { viewer.clock.currentTime = t; }}
477
+ onPlayPause={(playing) => { viewer.clock.shouldAnimate = playing; }}
478
+ onMultiplierChange={(m) => { viewer.clock.multiplier = m; }}
479
+ />
480
+ ```
481
+
482
+ ### Configuring Max Ticks
483
+
484
+ Useful when the timeline is shown at a small height or in a compact layout.
485
+
486
+ ```tsx
487
+ <Timeline
488
+ clock={viewer.clock}
489
+ height={40}
490
+ maxTicks={10}
491
+ />
492
+ ```
493
+
494
+ ### Configuring Playback Speeds
495
+
496
+ ```tsx
497
+ // Slow-motion / real-time / time-lapse only
498
+ <Timeline
499
+ clock={viewer.clock}
500
+ ffSpeeds={[0.5, 1, 2, 10, 100]}
501
+ rwSpeeds={[0.5, 1, 2, 10, 100]}
502
+ />
503
+ ```
504
+
505
+ ### Timeline-Only (No Controls)
506
+
507
+ ```tsx
508
+ <Timeline
509
+ clock={viewer.clock}
510
+ height={35}
511
+ showControls={false}
512
+ theme={{ indicatorColor: '#ff6b6b' }}
513
+ />
514
+ ```
515
+
516
+ ### Custom Date Range
517
+
518
+ ```tsx
519
+ const start = new Date();
520
+ start.setHours(0, 0, 0, 0);
521
+ const end = new Date();
522
+ end.setHours(23, 59, 59, 999);
523
+
524
+ <Timeline
525
+ startTime={start}
526
+ endTime={end}
527
+ height={80}
528
+ dateTimeFormat="HH:mm:ss"
529
+ />
530
+ ```
531
+
532
+ ### Without Clock (Standalone)
533
+
534
+ ```tsx
535
+ import { useState } from 'react';
536
+ import * as Cesium from 'cesium';
537
+ import { Timeline } from '@kteneyck/cesium-timeline-react';
538
+
539
+ const StandaloneTimeline = () => {
540
+ const [time, setTime] = useState(new Date());
541
+
542
+ return (
543
+ <>
544
+ <p>Selected: {time.toLocaleTimeString()}</p>
545
+ <Timeline
546
+ onTimeChange={(t) => setTime(Cesium.JulianDate.toDate(t))}
547
+ height={80}
548
+ showControls={false}
549
+ />
550
+ </>
551
+ );
552
+ };
553
+ ```
554
+
555
+ ---
556
+
557
+ ## Swim Lanes
558
+
559
+ Swim lanes render time intervals (bars) and instants (markers) as horizontal rows directly on the timeline canvas, aligned with the ticks and needle. They are ideal for visualizing satellite passes, ground contacts, scheduled events, or any temporal data.
560
+
561
+ Lanes are rendered in the upper portion of the canvas. The tick area remains fixed at the bottom. As you increase the timeline `height`, more lanes become visible. When lanes overflow the available space, a vertical scrollbar appears and you can scroll with the mouse wheel.
562
+
563
+ ### Basic Swim Lane Example
564
+
565
+ ```tsx
566
+ import * as Cesium from 'cesium';
567
+ import { Timeline } from '@kteneyck/cesium-timeline-react';
568
+ import type { SwimLane } from '@kteneyck/cesium-timeline-react';
569
+
570
+ const now = Cesium.JulianDate.now();
571
+ const later = Cesium.JulianDate.addHours(now, 3, new Cesium.JulianDate());
572
+
573
+ const swimLanes: SwimLane[] = [
574
+ {
575
+ id: 'passes',
576
+ label: 'Passes',
577
+ items: [
578
+ {
579
+ id: 'pass-1',
580
+ interval: new Cesium.TimeInterval({ start: now, stop: later }),
581
+ },
582
+ ],
583
+ },
584
+ ];
585
+
586
+ <Timeline
587
+ clock={viewer.clock}
588
+ height={150}
589
+ swimLanes={swimLanes}
590
+ onTimeChange={(t) => { viewer.clock.currentTime = t; }}
591
+ />
592
+ ```
593
+
594
+ ### Intervals and Instants
595
+
596
+ Each `SwimLaneItem` can have an `interval` (rendered as a horizontal bar), an `instant` (rendered as a marker), or both.
597
+
598
+ ```tsx
599
+ const lanes: SwimLane[] = [
600
+ {
601
+ id: 'events',
602
+ label: 'Events',
603
+ items: [
604
+ // Interval — rendered as a bar spanning start to stop
605
+ {
606
+ id: 'meeting',
607
+ interval: new Cesium.TimeInterval({
608
+ start: Cesium.JulianDate.fromIso8601('2026-03-05T09:00:00Z'),
609
+ stop: Cesium.JulianDate.fromIso8601('2026-03-05T10:30:00Z'),
610
+ }),
611
+ },
612
+ // Instant — rendered as a marker at a single point in time
613
+ {
614
+ id: 'alert',
615
+ instant: Cesium.JulianDate.fromIso8601('2026-03-05T12:00:00Z'),
616
+ },
617
+ ],
618
+ },
619
+ ];
620
+ ```
621
+
622
+ ### Customizing Styles
623
+
624
+ Styles cascade: `defaultSwimLaneStyle` → `theme` → `lane.style` → `item.style`. Each level is a partial override.
625
+
626
+ > **Border shortcut:** The `swimLaneItemBorderColor` and `swimLaneItemBorderWidth` theme properties let you control item borders globally without touching individual lane or item styles. Set `swimLaneItemBorderWidth: 0` in your theme to remove all borders at once.
627
+
628
+ ```tsx
629
+ const lanes: SwimLane[] = [
630
+ {
631
+ id: 'maintenance',
632
+ label: 'Maint.',
633
+ // Lane-level style: all items in this lane default to grey
634
+ style: {
635
+ color: '#78909c',
636
+ backgroundColor: 'rgba(120,144,156,0.1)',
637
+ },
638
+ items: [
639
+ {
640
+ id: 'window-1',
641
+ interval: new Cesium.TimeInterval({ start: h(-2), stop: h(0) }),
642
+ // Item-level override: this specific item is red
643
+ style: { color: '#f44336', opacity: 1.0 },
644
+ },
645
+ {
646
+ id: 'window-2',
647
+ interval: new Cesium.TimeInterval({ start: h(4), stop: h(6) }),
648
+ // Inherits lane style (grey)
649
+ },
650
+ ],
651
+ },
652
+ ];
653
+ ```
654
+
655
+ #### `SwimLaneItemStyle` Properties
656
+
657
+ | Property | Type | Default | Description |
658
+ |----------|------|---------|-------------|
659
+ | `color` | `string` | `#4da6ff` | Fill colour for interval bars and instant markers |
660
+ | `borderColor` | `string` | `#2980b9` | Border colour for interval bars |
661
+ | `borderWidth` | `number` | `1` | Border width in px for interval bars |
662
+ | `opacity` | `number` | `0.8` | Opacity (0–1) |
663
+ | `markerShape` | `'diamond' \| 'circle' \| 'line'` | `'diamond'` | Shape used to render instant markers |
664
+ | `markerSize` | `number` | `10` | Size in px for instant markers |
665
+
666
+ #### `SwimLaneStyle` Properties (extends `SwimLaneItemStyle`)
667
+
668
+ | Property | Type | Default | Description |
669
+ |----------|------|---------|-------------|
670
+ | `labelColor` | `string` | `#cccccc` | Colour of the lane label text |
671
+ | `backgroundColor` | `string` | `transparent` | Background colour of the lane row |
672
+
673
+ ### Instant Marker Shapes
674
+
675
+ Three marker shapes are available for instants:
676
+
677
+ ```tsx
678
+ // Diamond (default)
679
+ { id: 'a', instant: someDate, style: { markerShape: 'diamond' } }
680
+
681
+ // Circle
682
+ { id: 'b', instant: someDate, style: { markerShape: 'circle' } }
683
+
684
+ // Vertical line
685
+ { id: 'c', instant: someDate, style: { markerShape: 'line' } }
686
+ ```
687
+
688
+ ### Event Hooks
689
+
690
+ Swim lane items support click, hover, and double-click events. Each callback receives a `SwimLaneEventInfo` object.
691
+
692
+ ```tsx
693
+ import type { SwimLaneEventInfo } from '@kteneyck/cesium-timeline-react';
694
+
695
+ const handleClick = (info: SwimLaneEventInfo) => {
696
+ console.log(`Clicked item ${info.item.id} in lane ${info.laneId}`);
697
+ console.log('Custom data:', info.item.data);
698
+ };
699
+
700
+ const handleHover = (info: SwimLaneEventInfo | null) => {
701
+ if (info) {
702
+ showTooltip(`${info.item.id} in ${info.laneId}`);
703
+ } else {
704
+ hideTooltip();
705
+ }
706
+ };
707
+
708
+ const handleDoubleClick = (info: SwimLaneEventInfo) => {
709
+ openDetailPanel(info.item.data);
710
+ };
711
+
712
+ <Timeline
713
+ clock={viewer.clock}
714
+ height={150}
715
+ swimLanes={lanes}
716
+ onSwimLaneItemClick={handleClick}
717
+ onSwimLaneItemHover={handleHover}
718
+ onSwimLaneItemDoubleClick={handleDoubleClick}
719
+ />
720
+ ```
721
+
722
+ #### `SwimLaneEventInfo`
723
+
724
+ | Property | Type | Description |
725
+ |----------|------|-------------|
726
+ | `laneId` | `string` | The `id` of the lane containing the item |
727
+ | `item` | `SwimLaneItem` | The item that was interacted with |
728
+ | `originalEvent` | `MouseEvent` | The native DOM mouse event |
729
+
730
+ ### Attaching Custom Data
731
+
732
+ Use the `data` field on `SwimLaneItem` to attach arbitrary metadata. It's passed through in event callbacks.
733
+
734
+ ```tsx
735
+ const lanes: SwimLane[] = [
736
+ {
737
+ id: 'contacts',
738
+ label: 'Contacts',
739
+ items: [
740
+ {
741
+ id: 'c-1',
742
+ interval: new Cesium.TimeInterval({ start: t1, stop: t2 }),
743
+ data: { satellite: 'ISS', groundStation: 'Goldstone', snr: 42.5 },
744
+ },
745
+ ],
746
+ },
747
+ ];
748
+
749
+ const handleClick = (info: SwimLaneEventInfo) => {
750
+ const { satellite, groundStation, snr } = info.item.data as ContactData;
751
+ console.log(`${satellite} → ${groundStation} (SNR: ${snr})`);
752
+ };
753
+ ```
754
+
755
+ ### Drag-to-Reorder
756
+
757
+ Pass `onSwimLaneReorder` to enable drag-to-reorder. Grab a lane by its label (left side) and drag up or down. An insertion indicator shows where the lane will be placed.
758
+
759
+ ```tsx
760
+ const [lanes, setLanes] = useState<SwimLane[]>(initialLanes);
761
+
762
+ const handleReorder = (orderedIds: string[]) => {
763
+ // orderedIds is the new order of lane IDs
764
+ setLanes(prev => orderedIds.map(id => prev.find(l => l.id === id)!));
765
+ };
766
+
767
+ <Timeline
768
+ clock={viewer.clock}
769
+ height={150}
770
+ swimLanes={lanes}
771
+ onSwimLaneReorder={handleReorder}
772
+ />
773
+ ```
774
+
775
+ ### Show / Hide Swim Lanes
776
+
777
+ Toggle visibility without removing the data:
778
+
779
+ ```tsx
780
+ const [showSwimLanes, setShowSwimLanes] = useState(true);
781
+
782
+ <Timeline
783
+ clock={viewer.clock}
784
+ height={150}
785
+ swimLanes={lanes}
786
+ showSwimLanes={showSwimLanes}
787
+ />
788
+
789
+ <button onClick={() => setShowSwimLanes(v => !v)}>
790
+ {showSwimLanes ? 'Hide' : 'Show'} Swim Lanes
791
+ </button>
792
+ ```
793
+
794
+ When hidden, the full canvas height is used for the tick/label area as normal.
795
+
796
+ ### Lane Height and Scrolling
797
+
798
+ Each lane defaults to 24px tall. Override per-lane with the `height` property:
799
+
800
+ ```tsx
801
+ const lanes: SwimLane[] = [
802
+ { id: 'big', label: 'Important', height: 40, items: [...] },
803
+ { id: 'small', label: 'Minor', height: 16, items: [...] },
804
+ ];
805
+ ```
806
+
807
+ When the total lane height exceeds the available space (canvas height minus the 36px tick area), a scrollbar appears and the mouse wheel scrolls vertically in the swim lane region. Outside the lane region, the mouse wheel zooms the timeline as usual.
808
+
809
+ ### Imperative API
810
+
811
+ The `TimelineCanvasHandle` (accessible via a ref on `Timeline`) exposes methods for programmatic swim lane management:
812
+
813
+ ```tsx
814
+ const timelineRef = useRef<TimelineCanvasHandle>(null);
815
+
816
+ // Append a new lane
817
+ timelineRef.current?.appendSwimLane(newLane);
818
+
819
+ // Update an existing lane
820
+ timelineRef.current?.updateSwimLane('lane-id', updatedLane);
821
+
822
+ // Remove a lane
823
+ timelineRef.current?.removeSwimLane('lane-id');
824
+
825
+ // Reorder lanes
826
+ timelineRef.current?.reorderSwimLanes(['lane-b', 'lane-a', 'lane-c']);
827
+ ```
828
+
829
+ ---
830
+
831
+ ## Building
832
+
833
+ ```bash
834
+ cd cesium-timeline
835
+ npm install
836
+
837
+ # Build all packages (core → react → angular)
838
+ npm run build
839
+
840
+ # Build individually
841
+ npm run build:core
842
+ npm run build:react
843
+ npm run build:angular
844
+
845
+ # Development
846
+ npm run dev:demo # React demo with hot reload
847
+ npm run typecheck # TypeScript check (core + react)
848
+ npm run clean # Remove all build artifacts
849
+ ```
850
+
851
+ ### Project Structure
852
+
853
+ ```
854
+ cesium-timeline/
855
+ ├── packages/
856
+ │ ├── core/ → @kteneyck/cesium-timeline-core (types, utils, canvas engine)
857
+ │ ├── react/ → @kteneyck/cesium-timeline-react (React components)
858
+ │ └── angular/ → @kteneyck/cesium-timeline-angular (Angular standalone components)
859
+ ├── demo-react/ → React demo app (npm run dev:demo)
860
+ ├── src/ → Original source (preserved, not used by packages)
861
+ └── package.json → npm workspace root
862
+ ```
863
+
864
+ ---
865
+
866
+ ## License
867
+
868
+ MIT
869
+