geotimelapse 0.1.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/bounds.d.ts +5 -0
  4. package/dist/bounds.d.ts.map +1 -0
  5. package/dist/bounds.js +9 -0
  6. package/dist/bounds.js.map +1 -0
  7. package/dist/clock.d.ts +24 -0
  8. package/dist/clock.d.ts.map +1 -0
  9. package/dist/clock.js +108 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/counter.d.ts +18 -0
  12. package/dist/counter.d.ts.map +1 -0
  13. package/dist/counter.js +54 -0
  14. package/dist/counter.js.map +1 -0
  15. package/dist/duckdb-source.d.ts +24 -0
  16. package/dist/duckdb-source.d.ts.map +1 -0
  17. package/dist/duckdb-source.js +211 -0
  18. package/dist/duckdb-source.js.map +1 -0
  19. package/dist/geo-timelapse.d.ts +8 -0
  20. package/dist/geo-timelapse.d.ts.map +1 -0
  21. package/dist/geo-timelapse.js +143 -0
  22. package/dist/geo-timelapse.js.map +1 -0
  23. package/dist/glow-layer.d.ts +47 -0
  24. package/dist/glow-layer.d.ts.map +1 -0
  25. package/dist/glow-layer.js +129 -0
  26. package/dist/glow-layer.js.map +1 -0
  27. package/dist/hooks.d.ts +32 -0
  28. package/dist/hooks.d.ts.map +1 -0
  29. package/dist/hooks.js +142 -0
  30. package/dist/hooks.js.map +1 -0
  31. package/dist/index.d.ts +6 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/map-style.d.ts +3 -0
  36. package/dist/map-style.d.ts.map +1 -0
  37. package/dist/map-style.js +832 -0
  38. package/dist/map-style.js.map +1 -0
  39. package/dist/map.d.ts +11 -0
  40. package/dist/map.d.ts.map +1 -0
  41. package/dist/map.js +60 -0
  42. package/dist/map.js.map +1 -0
  43. package/dist/player-bar.d.ts +4 -0
  44. package/dist/player-bar.d.ts.map +1 -0
  45. package/dist/player-bar.js +118 -0
  46. package/dist/player-bar.js.map +1 -0
  47. package/dist/settings-menu.d.ts +17 -0
  48. package/dist/settings-menu.d.ts.map +1 -0
  49. package/dist/settings-menu.js +18 -0
  50. package/dist/settings-menu.js.map +1 -0
  51. package/dist/synthetic-source.d.ts +18 -0
  52. package/dist/synthetic-source.d.ts.map +1 -0
  53. package/dist/synthetic-source.js +192 -0
  54. package/dist/synthetic-source.js.map +1 -0
  55. package/dist/types.d.ts +62 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/utils.d.ts +9 -0
  60. package/dist/utils.d.ts.map +1 -0
  61. package/dist/utils.js +28 -0
  62. package/dist/utils.js.map +1 -0
  63. package/package.json +67 -0
  64. package/src/bounds.tsx +14 -0
  65. package/src/clock.tsx +130 -0
  66. package/src/counter.tsx +96 -0
  67. package/src/duckdb-source.ts +238 -0
  68. package/src/geo-timelapse.tsx +204 -0
  69. package/src/glow-layer.test.ts +51 -0
  70. package/src/glow-layer.ts +166 -0
  71. package/src/hooks.ts +167 -0
  72. package/src/index.ts +5 -0
  73. package/src/map-style.ts +833 -0
  74. package/src/map.tsx +107 -0
  75. package/src/player-bar.tsx +164 -0
  76. package/src/settings-menu.tsx +117 -0
  77. package/src/synthetic-source.test.ts +58 -0
  78. package/src/synthetic-source.ts +218 -0
  79. package/src/types.ts +66 -0
  80. package/src/utils.test.ts +60 -0
  81. package/src/utils.ts +32 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Franck Ernewein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # geotimelapse
2
+
3
+ Replay a day of geolocated events on a dark glowing map: deck.gl point lights
4
+ with fading trails over a Mapbox basemap, a waveform seek bar, running totals,
5
+ and a TV mode (autoplay, loop, self-hiding UI and cursor).
6
+
7
+ **[Live glow lab →](https://franckernewein.github.io/geotimelapse/)**
8
+
9
+ ## Why it looks the way it does
10
+
11
+ - **Additive light, not markers** — every event is a tiny point light with a
12
+ gaussian halo, blended additively: density builds the brightness, and
13
+ hotspots bloom toward white the way long-exposure photos do.
14
+ - **Ground-anchored radii** — point radii are meters, not pixels, so the glow
15
+ reads the same at every zoom level; heavy stacks approach a pixel ceiling
16
+ asymptotically instead of clipping into flat discs.
17
+ - **Weight-scaled brightness** — co-located events are aggregated into one
18
+ draw whose halo amplitude follows a film-exposure response on √count.
19
+ - **A trail that breathes** — each frame leaves ghosts that shrink and fade
20
+ with age, and the trail length adapts to the achieved frame rate (sheds
21
+ ghosts under 24 fps, grows them back above 58).
22
+ - **A full player** — per-minute activity waveform as the seek bar, running
23
+ totals, hour axis, fullscreen TV mode.
24
+ - **Data-agnostic** — the component talks to a small `GeoTimelapseSource`
25
+ contract; bring any backend, or use the bundled duckdb-wasm and synthetic
26
+ sources.
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ npm install geotimelapse
32
+ ```
33
+
34
+ Peer dependencies: `react`, `react-dom`, `deck.gl`, `mapbox-gl`,
35
+ `react-map-gl`. (`@duckdb/duckdb-wasm` is a regular dependency, pulled in only
36
+ when you import `geotimelapse/duckdb`.)
37
+
38
+ ## Usage
39
+
40
+ ```tsx
41
+ 'use client'; // browser-only (WebGL, workers) — in Next.js, use it from a client component
42
+
43
+ import 'mapbox-gl/dist/mapbox-gl.css';
44
+
45
+ import { GeoTimelapse } from 'geotimelapse';
46
+ import { createDuckDbSource } from 'geotimelapse/duckdb';
47
+
48
+ const source = createDuckDbSource({
49
+ parquetUrl: '/data/replay-2026-09-05.parquet',
50
+ // engine omitted: version-matched jsDelivr CDN. Self-hosting: pass the base
51
+ // URL you serve @duckdb/duckdb-wasm/dist under (the copy THIS package
52
+ // resolves, so the wasm matches the JS), or full DuckDBBundles.
53
+ valueColumn: 'spend_micros',
54
+ });
55
+
56
+ <div style={{ height: '100vh' }}>
57
+ <GeoTimelapse
58
+ source={source}
59
+ mapboxAccessToken={token}
60
+ dateLabel="Sat, Sep 5, 2026"
61
+ timeZoneLabel="EDT"
62
+ formatValue={(micros) => usd.format(micros / 1e6)}
63
+ formatCount={(count) => `${count.toLocaleString()} wins`}
64
+ />
65
+ </div>;
66
+ ```
67
+
68
+ The component fills its parent: give the wrapper an explicit size. The source
69
+ holds a dedicated worker and the full in-memory table: call `source.dispose()`
70
+ when you are done with it (e.g. leaving the page in an SPA).
71
+
72
+ ### Data contract
73
+
74
+ `GeoTimelapseSource` is the only coupling to your data: implement `load`,
75
+ `frame`, `totals`, `activity` and `setScope` over any backend (see
76
+ `src/types.ts`). Two sources ship with the package:
77
+
78
+ - `geotimelapse/duckdb` loads a day parquet (`day_second` INT sorted, a value
79
+ column, `lat`/`lon` FLOAT) into an in-browser duckdb-wasm table.
80
+ - `geotimelapse/synthetic` generates a deterministic day in memory (gaussian
81
+ clusters, uniform fog and single-spot stacks) — handy for demos and benches.
82
+
83
+ ### Tuning
84
+
85
+ The glow is calibrated through the `tuning` prop, merged over the defaults:
86
+
87
+ ```tsx
88
+ <GeoTimelapse source={source} tuning={{ pointAlpha: 50 }} ... />
89
+ ```
90
+
91
+ `pointAlpha` is the master per-point transparency (0-255). The playback trail
92
+ stacks 12 additive frames, so dense datasets (thousands of points per frame)
93
+ need it well below the single-frame value. Other knobs: `flashRadius`,
94
+ `ghostMaxRadius`, `ghostMinRadius` (meters), `minRadiusPx`, `maxRadiusPx`,
95
+ `haloTint`, `trailFrames` — see `GlowTuning`.
96
+
97
+ ### Styling
98
+
99
+ Components are styled with Tailwind CSS v4 classes. Add the package to your
100
+ Tailwind sources so they get generated:
101
+
102
+ ```css
103
+ @import 'tailwindcss';
104
+ @source '../node_modules/geotimelapse/dist';
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,5 @@
1
+ import type { MapBounds } from './types.js';
2
+ export declare const MapBoundsProvider: import("react").Provider<MapBounds | null>;
3
+ /** The map's current viewport bounds; null until the map has loaded. */
4
+ export declare function useMapBounds(): MapBounds | null;
5
+ //# sourceMappingURL=bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,eAAO,MAAM,iBAAiB,4CAAyB,CAAC;AAExD,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,SAAS,GAAG,IAAI,CAE/C"}
package/dist/bounds.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+ const BoundsContext = createContext(null);
4
+ export const MapBoundsProvider = BoundsContext.Provider;
5
+ /** The map's current viewport bounds; null until the map has loaded. */
6
+ export function useMapBounds() {
7
+ return useContext(BoundsContext);
8
+ }
9
+ //# sourceMappingURL=bounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.js","sourceRoot":"","sources":["../src/bounds.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAIlD,MAAM,aAAa,GAAG,aAAa,CAAmB,IAAI,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC,QAAQ,CAAC;AAExD,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { PropsWithChildren } from 'react';
2
+ export declare const DAY_SECONDS: number;
3
+ export interface TimelapseClock {
4
+ play: () => void;
5
+ pause: () => void;
6
+ toggle: () => void;
7
+ seek: (daySeconds: number) => void;
8
+ /** Seconds elapsed since the start of the replayed day, in [0, DAY_SECONDS]. */
9
+ getDaySeconds: () => number;
10
+ /** Increments on every time jump (seek, end-of-day restart): consumers use it to invalidate history. */
11
+ getEpoch: () => number;
12
+ isPlaying: () => boolean;
13
+ /** Notified up to MAX_TICKS_PER_SECOND times while playing, and on play/pause/seek. */
14
+ subscribe: (listener: () => void) => () => void;
15
+ }
16
+ export declare function TimelapseClockProvider({ children }: PropsWithChildren): import("react").JSX.Element;
17
+ export declare function useTimelapseClock(): TimelapseClock;
18
+ export declare function useIsPlaying(): boolean;
19
+ /**
20
+ * Current day time, quantized: the component only re-renders when the value
21
+ * crosses a quantum boundary, so each consumer picks its own refresh rate.
22
+ */
23
+ export declare function useDaySeconds(quantum?: number): number;
24
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAG/C,eAAO,MAAM,WAAW,QAAe,CAAC;AAcxC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,gFAAgF;IAChF,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,wGAAwG;IACxG,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,uFAAuF;IACvF,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CACjD;AAyED,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAIrE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAGtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAI,GAAG,MAAM,CAIjD"}
package/dist/clock.js ADDED
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useState, useSyncExternalStore } from 'react';
4
+ export const DAY_SECONDS = 24 * 60 * 60;
5
+ // Wall-clock duration of a full-day replay.
6
+ const PLAYBACK_SECONDS = 180;
7
+ const RATE = DAY_SECONDS / PLAYBACK_SECONDS;
8
+ // Ticks are capped below the display refresh rate: extra updates are invisible
9
+ // and would only waste re-renders on high-refresh screens.
10
+ const MAX_TICKS_PER_SECOND = 24;
11
+ const MIN_TICK_MS = 1000 / MAX_TICKS_PER_SECOND;
12
+ // Bounds on the wall-clock delta applied per tick. The upper bound keeps a
13
+ // hidden tab (rAF suspended) or a main-thread stall from fast-forwarding the
14
+ // replay; the lower bound guards against the first rAF timestamp predating the
15
+ // performance.now() sampled in play() (vsync time is stamped before input).
16
+ const MAX_TICK_MS = 100;
17
+ function createClock() {
18
+ let daySeconds = 0;
19
+ let epoch = 0;
20
+ let playing = false;
21
+ let rafId = 0;
22
+ let lastTick = 0;
23
+ // Next tick due date, advanced on its own grid so the effective rate stays
24
+ // at the cap instead of snapping down to a divisor of the refresh rate.
25
+ let nextTickAt = 0;
26
+ const listeners = new Set();
27
+ const notify = () => listeners.forEach((listener) => listener());
28
+ const tick = (now) => {
29
+ if (now < nextTickAt) {
30
+ rafId = requestAnimationFrame(tick);
31
+ return;
32
+ }
33
+ nextTickAt = Math.max(nextTickAt + MIN_TICK_MS, now);
34
+ daySeconds += (Math.min(Math.max(now - lastTick, 0), MAX_TICK_MS) / 1000) * RATE;
35
+ lastTick = now;
36
+ if (daySeconds >= DAY_SECONDS) {
37
+ daySeconds = DAY_SECONDS;
38
+ playing = false;
39
+ }
40
+ else {
41
+ rafId = requestAnimationFrame(tick);
42
+ }
43
+ notify();
44
+ };
45
+ const play = () => {
46
+ if (playing)
47
+ return;
48
+ if (daySeconds >= DAY_SECONDS) {
49
+ daySeconds = 0;
50
+ epoch += 1;
51
+ }
52
+ playing = true;
53
+ lastTick = performance.now();
54
+ nextTickAt = lastTick;
55
+ rafId = requestAnimationFrame(tick);
56
+ notify();
57
+ };
58
+ const pause = () => {
59
+ if (!playing)
60
+ return;
61
+ playing = false;
62
+ cancelAnimationFrame(rafId);
63
+ notify();
64
+ };
65
+ return {
66
+ play,
67
+ pause,
68
+ toggle: () => (playing ? pause() : play()),
69
+ seek: (value) => {
70
+ daySeconds = Math.min(Math.max(value, 0), DAY_SECONDS);
71
+ epoch += 1;
72
+ notify();
73
+ },
74
+ getDaySeconds: () => daySeconds,
75
+ getEpoch: () => epoch,
76
+ isPlaying: () => playing,
77
+ subscribe: (listener) => {
78
+ listeners.add(listener);
79
+ return () => listeners.delete(listener);
80
+ },
81
+ };
82
+ }
83
+ const ClockContext = createContext(null);
84
+ export function TimelapseClockProvider({ children }) {
85
+ const [clock] = useState(createClock);
86
+ useEffect(() => () => clock.pause(), [clock]);
87
+ return _jsx(ClockContext.Provider, { value: clock, children: children });
88
+ }
89
+ export function useTimelapseClock() {
90
+ const clock = useContext(ClockContext);
91
+ if (!clock)
92
+ throw new Error('useTimelapseClock must be used within a TimelapseClockProvider');
93
+ return clock;
94
+ }
95
+ export function useIsPlaying() {
96
+ const clock = useTimelapseClock();
97
+ return useSyncExternalStore(clock.subscribe, clock.isPlaying, () => false);
98
+ }
99
+ /**
100
+ * Current day time, quantized: the component only re-renders when the value
101
+ * crosses a quantum boundary, so each consumer picks its own refresh rate.
102
+ */
103
+ export function useDaySeconds(quantum = 1) {
104
+ const clock = useTimelapseClock();
105
+ const getSnapshot = useCallback(() => Math.floor(clock.getDaySeconds() / quantum) * quantum, [clock, quantum]);
106
+ return useSyncExternalStore(clock.subscribe, getSnapshot, () => 0);
107
+ }
108
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { GeoTimelapseSource } from './types.js';
2
+ interface CounterProps {
3
+ source: GeoTimelapseSource;
4
+ ready: boolean;
5
+ error: string | null;
6
+ progress: {
7
+ loadedBytes: number;
8
+ totalBytes: number;
9
+ } | null;
10
+ scopeVersion: number;
11
+ dateLabel?: string;
12
+ timeZoneLabel?: string;
13
+ formatValue: (value: number) => string;
14
+ formatCount: (count: number) => string;
15
+ }
16
+ export default function Counter({ source, ready, error, progress, scopeVersion, dateLabel, timeZoneLabel, formatValue, formatCount, }: CounterProps): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAU,MAAM,YAAY,CAAC;AAG7D,UAAU,YAAY;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAC9B,MAAM,EACN,KAAK,EACL,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,aAAa,EACb,WAAW,EACX,WAAW,GACZ,EAAE,YAAY,+BAiEd"}
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { useDaySeconds, useTimelapseClock } from './clock.js';
5
+ import { formatDayTime12 } from './utils.js';
6
+ export default function Counter({ source, ready, error, progress, scopeVersion, dateLabel, timeZoneLabel, formatValue, formatCount, }) {
7
+ const clock = useTimelapseClock();
8
+ const daySeconds = useDaySeconds(60);
9
+ const [totals, setTotals] = useState(null);
10
+ const [queryError, setQueryError] = useState(null);
11
+ useEffect(() => {
12
+ if (!ready)
13
+ return;
14
+ let disposed = false;
15
+ let inFlight = false;
16
+ let lastQueried = -1;
17
+ // One query at a time: each tick tries, and is skipped while the previous
18
+ // query is still running, so the counter refreshes as fast as the source
19
+ // can answer and never queues up.
20
+ const query = async () => {
21
+ const second = Math.floor(clock.getDaySeconds());
22
+ if (inFlight || second === lastQueried)
23
+ return;
24
+ inFlight = true;
25
+ try {
26
+ const result = await source.totals(second);
27
+ if (disposed)
28
+ return;
29
+ lastQueried = second;
30
+ setQueryError(null);
31
+ setTotals(result);
32
+ }
33
+ catch (totalsError) {
34
+ if (!disposed)
35
+ setQueryError(totalsError instanceof Error ? totalsError.message : String(totalsError));
36
+ }
37
+ finally {
38
+ inFlight = false;
39
+ }
40
+ };
41
+ void query();
42
+ const unsubscribe = clock.subscribe(() => void query());
43
+ return () => {
44
+ disposed = true;
45
+ unsubscribe();
46
+ };
47
+ // ready and scopeVersion re-run the effect so a re-scoped source is re-read.
48
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies
49
+ }, [clock, source, ready, scopeVersion]);
50
+ return (_jsx("div", { className: "absolute top-4 right-4 h-24 w-64 rounded-lg bg-black/60 px-4 py-3 text-right font-mono text-white backdrop-blur-sm", children: (error ?? queryError) ? (_jsx("div", { className: "max-w-xs overflow-hidden text-xs text-red-400", children: error ?? queryError })) : ready && totals ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-xs text-white/50 tabular-nums", children: [dateLabel && `${dateLabel}, `, formatDayTime12(daySeconds), timeZoneLabel && ` ${timeZoneLabel}`] }), _jsx("div", { className: "text-2xl tabular-nums", children: formatValue(totals.value) }), _jsx("div", { className: "text-xs text-white/70 tabular-nums", children: formatCount(totals.count) })] })) : (_jsx("div", { className: "text-xs text-white/70 tabular-nums", children: progress
51
+ ? `loading day… ${Math.round(progress.loadedBytes / 1e6)}/${Math.round(progress.totalBytes / 1e6)} MB`
52
+ : 'loading…' })) }));
53
+ }
54
+ //# sourceMappingURL=counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import * as duckdb from '@duckdb/duckdb-wasm';
2
+ import type { GeoTimelapseSource } from './types.js';
3
+ export interface DuckDbSourceOptions {
4
+ /** URL of the day's parquet: day_second INT (seconds since the day's
5
+ * midnight, sorted), a value column, lat/lon FLOAT. ~60M rows is fine. */
6
+ parquetUrl: string;
7
+ /**
8
+ * Where the duckdb-wasm engine files come from. Omitted: version-matched
9
+ * jsDelivr URLs (zero config, external CDN). A string: the base URL you
10
+ * serve `@duckdb/duckdb-wasm/dist` under — serve the copy THIS package
11
+ * resolves, so the wasm matches the JS. An object: full bundle control.
12
+ */
13
+ engine?: string | duckdb.DuckDBBundles;
14
+ /** Name of the parquet column summed into totals().value. */
15
+ valueColumn?: string;
16
+ }
17
+ /**
18
+ * A GeoTimelapseSource backed by duckdb-wasm: the parquet is downloaded once
19
+ * and loaded into an in-browser table, then every read is a local query.
20
+ * Each source owns its own engine instance (worker), so two components never
21
+ * share state — at the price of one table copy per instance.
22
+ */
23
+ export declare function createDuckDbSource({ parquetUrl, engine, valueColumn, }: DuckDbSourceOptions): GeoTimelapseSource;
24
+ //# sourceMappingURL=duckdb-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb-source.d.ts","sourceRoot":"","sources":["../src/duckdb-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,qBAAqB,CAAC;AAE9C,OAAO,KAAK,EAAe,kBAAkB,EAAqB,MAAM,YAAY,CAAC;AAErF,MAAM,WAAW,mBAAmB;IAClC;+EAC2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;IACvC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAoBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,EACV,MAAM,EACN,WAAqB,GACtB,EAAE,mBAAmB,GAAG,kBAAkB,CA8L1C"}
@@ -0,0 +1,211 @@
1
+ import * as duckdb from '@duckdb/duckdb-wasm';
2
+ function resolveBundles(engine) {
3
+ if (!engine)
4
+ return duckdb.getJsDelivrBundles();
5
+ if (typeof engine === 'string') {
6
+ const base = engine.replace(/\/$/, '');
7
+ return {
8
+ mvp: {
9
+ mainModule: `${base}/duckdb-mvp.wasm`,
10
+ mainWorker: `${base}/duckdb-browser-mvp.worker.js`,
11
+ },
12
+ eh: {
13
+ mainModule: `${base}/duckdb-eh.wasm`,
14
+ mainWorker: `${base}/duckdb-browser-eh.worker.js`,
15
+ },
16
+ };
17
+ }
18
+ return engine;
19
+ }
20
+ /**
21
+ * A GeoTimelapseSource backed by duckdb-wasm: the parquet is downloaded once
22
+ * and loaded into an in-browser table, then every read is a local query.
23
+ * Each source owns its own engine instance (worker), so two components never
24
+ * share state — at the price of one table copy per instance.
25
+ */
26
+ export function createDuckDbSource({ parquetUrl, engine, valueColumn = 'value', }) {
27
+ let dbPromise = null;
28
+ let connPromise = null;
29
+ let loadPromise = null;
30
+ // Mutable slot so a load() call after the first (e.g. a React StrictMode
31
+ // remount) still gets progress reports from the single in-flight download.
32
+ let progressListener;
33
+ let scopeFilter = 'TRUE';
34
+ let requestedFilter = 'TRUE';
35
+ let scopeChain = Promise.resolve();
36
+ const getDB = () => (dbPromise ??= (async () => {
37
+ const bundle = await duckdb.selectBundle(resolveBundles(engine));
38
+ const workerUrl = bundle.mainWorker;
39
+ // A same-origin worker loads directly (and its requests carry the
40
+ // page's cookies); cross-origin URLs (the jsDelivr default) need
41
+ // createWorker's same-origin blob wrapper or the browser blocks them.
42
+ const sameOrigin = new URL(workerUrl, globalThis.location.href).origin === globalThis.location.origin;
43
+ const worker = sameOrigin ? new Worker(workerUrl) : await duckdb.createWorker(workerUrl);
44
+ const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(duckdb.LogLevel.WARNING), worker);
45
+ await db.instantiate(bundle.mainModule);
46
+ return db;
47
+ })());
48
+ const getConn = () => (connPromise ??= getDB().then((db) => db.connect()));
49
+ // Running totals per second, so counters read one row per tick.
50
+ const CREATE_TOTALS = `
51
+ CREATE OR REPLACE TABLE totals_by_second AS
52
+ SELECT day_second,
53
+ sum(events) OVER w AS events,
54
+ sum(value) OVER w AS value
55
+ FROM scope_seconds
56
+ WINDOW w AS (ORDER BY day_second)
57
+ `;
58
+ const load = (onProgress) => {
59
+ if (onProgress)
60
+ progressListener = onProgress;
61
+ return (loadPromise ??= (async () => {
62
+ const response = await fetch(parquetUrl);
63
+ if (!response.ok || !response.body)
64
+ throw new Error(`parquet download failed (${response.status})`);
65
+ const totalBytes = Number(response.headers.get('content-length') ?? 0);
66
+ const chunks = [];
67
+ let loadedBytes = 0;
68
+ let lastReported = 0;
69
+ const reader = response.body.getReader();
70
+ for (;;) {
71
+ const { done, value } = await reader.read();
72
+ if (done)
73
+ break;
74
+ chunks.push(value);
75
+ loadedBytes += value.byteLength;
76
+ if (loadedBytes - lastReported >= 8 * 1024 * 1024) {
77
+ lastReported = loadedBytes;
78
+ progressListener?.(loadedBytes, totalBytes);
79
+ }
80
+ }
81
+ const buffer = new Uint8Array(loadedBytes);
82
+ let offset = 0;
83
+ for (const chunk of chunks) {
84
+ buffer.set(chunk, offset);
85
+ offset += chunk.byteLength;
86
+ }
87
+ progressListener?.(loadedBytes, totalBytes);
88
+ const db = await getDB();
89
+ await db.registerFileBuffer('replay.parquet', buffer);
90
+ const conn = await getConn();
91
+ // The value column is normalized at ingest so every later query is
92
+ // schema-agnostic. The source order (sorted by day_second) is what
93
+ // gives the in-memory table its zonemap pruning.
94
+ await conn.query(`
95
+ CREATE OR REPLACE TABLE events AS
96
+ SELECT day_second, ${valueColumn} AS value, lat, lon
97
+ FROM read_parquet('replay.parquet')
98
+ `);
99
+ // Per-second counts within the current scope, the single base both
100
+ // aggregates derive from — so consumers never rescan the raw rows.
101
+ await conn.query(`
102
+ CREATE OR REPLACE TABLE scope_seconds AS
103
+ SELECT day_second, count(*) AS events, sum(value) AS value
104
+ FROM events
105
+ GROUP BY day_second
106
+ `);
107
+ await conn.query(CREATE_TOTALS);
108
+ await db.dropFile('replay.parquet');
109
+ })());
110
+ };
111
+ const frame = async (fromSecond, toSecond) => {
112
+ const conn = await getConn();
113
+ // Integer bounds keep the comparison on the INTEGER column: a double
114
+ // literal would disable zonemap pruning and full-scan the table.
115
+ const result = await conn.query(`
116
+ SELECT lon, lat, count(*)::DOUBLE AS weight FROM events
117
+ WHERE day_second >= ${Math.floor(fromSecond)} AND day_second < ${Math.floor(toSecond)}
118
+ AND lat IS NOT NULL AND lon IS NOT NULL
119
+ GROUP BY lon, lat
120
+ `);
121
+ const lon = result.getChild('lon').toArray();
122
+ const lat = result.getChild('lat').toArray();
123
+ const weight = result.getChild('weight').toArray();
124
+ const positions = new Float32Array(lon.length * 2);
125
+ const weights = new Float32Array(lon.length);
126
+ for (let i = 0; i < lon.length; i++) {
127
+ positions[2 * i] = lon[i];
128
+ positions[2 * i + 1] = lat[i];
129
+ weights[i] = weight[i];
130
+ }
131
+ return { positions, weights, count: lon.length };
132
+ };
133
+ const totals = async (second) => {
134
+ const conn = await getConn();
135
+ const result = await conn.query(`
136
+ SELECT events::DOUBLE AS events, value::DOUBLE AS value
137
+ FROM totals_by_second
138
+ WHERE day_second < ${Math.floor(second)}
139
+ ORDER BY day_second DESC
140
+ LIMIT 1
141
+ `);
142
+ const row = result.numRows > 0 ? result.get(0) : null;
143
+ return { count: Number(row?.events ?? 0), value: Number(row?.value ?? 0) };
144
+ };
145
+ const activity = async () => {
146
+ const conn = await getConn();
147
+ const result = await conn.query(`
148
+ SELECT (day_second // 60)::INT AS minute, sum(events)::DOUBLE AS events
149
+ FROM scope_seconds
150
+ GROUP BY minute
151
+ `);
152
+ const minutes = result.getChild('minute').toArray();
153
+ const events = result.getChild('events').toArray();
154
+ const counts = new Float32Array(24 * 60);
155
+ for (let i = 0; i < minutes.length; i++)
156
+ counts[minutes[i]] = events[i];
157
+ return counts;
158
+ };
159
+ // The scan over the raw rows is chunked hour by hour (pruned on
160
+ // day_second) so playback tick queries interleave instead of queueing
161
+ // behind one long statement.
162
+ const setScope = (bounds) => {
163
+ const filter = bounds
164
+ ? `lat BETWEEN ${bounds.south} AND ${bounds.north} AND lon BETWEEN ${bounds.west} AND ${bounds.east}`
165
+ : 'TRUE';
166
+ requestedFilter = filter;
167
+ scopeChain = scopeChain
168
+ .then(async () => {
169
+ // Superseded while queued, or already active.
170
+ if (requestedFilter !== filter || filter === scopeFilter)
171
+ return;
172
+ // The rebuild reads the events table: wait for the day to be loaded.
173
+ await load();
174
+ const conn = await getConn();
175
+ await conn.query(`
176
+ CREATE OR REPLACE TABLE scope_seconds_build AS
177
+ SELECT day_second, count(*) AS events, sum(value) AS value
178
+ FROM events WHERE false GROUP BY day_second
179
+ `);
180
+ for (let hour = 0; hour < 24; hour++) {
181
+ if (requestedFilter !== filter)
182
+ return;
183
+ await conn.query(`
184
+ INSERT INTO scope_seconds_build
185
+ SELECT day_second, count(*), sum(value)
186
+ FROM events
187
+ WHERE day_second >= ${hour * 3600} AND day_second < ${(hour + 1) * 3600} AND ${filter}
188
+ GROUP BY day_second
189
+ `);
190
+ }
191
+ await conn.query(`CREATE OR REPLACE TABLE scope_seconds AS FROM scope_seconds_build`);
192
+ await conn.query(`DROP TABLE scope_seconds_build`);
193
+ await conn.query(CREATE_TOTALS);
194
+ scopeFilter = filter;
195
+ })
196
+ // A failed rebuild keeps the previous scope; never poison the chain.
197
+ .catch((error) => console.error('geotimelapse scope rebuild failed', error));
198
+ return scopeChain;
199
+ };
200
+ const dispose = async () => {
201
+ const conn = connPromise ? await connPromise.catch(() => null) : null;
202
+ connPromise = null;
203
+ await conn?.close().catch(() => { });
204
+ const db = dbPromise ? await dbPromise.catch(() => null) : null;
205
+ dbPromise = null;
206
+ loadPromise = null;
207
+ await db?.terminate().catch(() => { });
208
+ };
209
+ return { load, frame, totals, activity, setScope, dispose };
210
+ }
211
+ //# sourceMappingURL=duckdb-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb-source.js","sourceRoot":"","sources":["../src/duckdb-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { GeoTimelapseProps } from './types.js';
2
+ /**
3
+ * A full-day replay of geolocated events: dark glowing map, waveform seek
4
+ * bar, running totals, and a TV mode (autoplay, loop, self-hiding UI).
5
+ * Fills its parent — give the wrapping element an explicit size.
6
+ */
7
+ export declare function GeoTimelapse(props: GeoTimelapseProps): import("react").JSX.Element;
8
+ //# sourceMappingURL=geo-timelapse.d.ts.map