From 8e1b215226288e4ebbcf6c2b766a88fddcb9365b Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Sun, 1 Mar 2026 17:24:19 -0500 Subject: [PATCH 01/16] fix(website): remove three/wild-forest from sidebar until doc page exists Co-Authored-By: Claude Sonnet 4.6 --- website/src/examples-sidebar.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js index 2cd284d8c..c073f9b6c 100644 --- a/website/src/examples-sidebar.js +++ b/website/src/examples-sidebar.js @@ -81,14 +81,6 @@ const sidebars = { // ] // }, - { - type: 'category', - label: '@deck.gl-community/three', - items: [ - "three/wild-forest" - ] - }, - // TODO - need BING map key // { // type: 'category', From f854ecd159fbae2cca3ed7b283bb06d6492e5540 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 00:20:29 -0500 Subject: [PATCH 02/16] feat(ci): add PR test workflow, vitest coverage, and unit tests Closes a degree of #82 by establishing an automated test baseline for every PR. Applies reviewer feedback from #512 re: dev/ directory for WIP/RFC content. ## CI workflow (.github/workflows/test.yml) - Triggers on push to master and all pull_request events - lint job: runs yarn lint (no build required) - test-node job: builds, runs vitest node project with v8 coverage, uploads to Coveralls - test-headless job: installs Playwright Chromium, runs headless browser tests Modeled after deck.gl's test.yml (see visgl/deck.gl vitest-migration-rfc) using @vitest/browser + Playwright as first-class visgl testing utilities. ## Coverage (vitest.config.ts + package.json) - Added test.coverage block: provider v8, reporters text + lcov (for Coveralls) - Scoped to modules/*/src/**, excludes basemap-props* and template - Added test-ci script: vitest run --project node --coverage - Added @vitest/coverage-v8 devDependency ## New unit tests (all 308 pass, 60 test files) - modules/editable-layers/test/immutable-feature-collection.node.spec.ts ImmutableFeatureCollection: add/delete/replace/removePosition, polygon ring wrapping, 3D elevation preservation, immutability guarantees (17 tests) - modules/editable-layers/test/imports.node.spec.ts Public API surface smoke test: draw, alter, and measurement mode exports (6 tests) - modules/layers/test/imports.node.spec.ts (2 tests) - modules/widgets/test/imports.node.spec.ts (4 tests) - modules/geo-layers/test/imports.node.spec.ts (3 tests) ## dev/ directory Per ibgreen's feedback in #512: establishes dev/ as the canonical home for RFCs and WIP content that should NOT be indexed by the docs site search. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 125 ++++++++ dev/README.md | 17 ++ .../immutable-feature-collection.node.spec.ts | 272 ++++++++++++++++++ .../editable-layers/test/imports.node.spec.ts | 61 ++++ modules/geo-layers/test/imports.node.spec.ts | 21 ++ modules/layers/test/imports.node.spec.ts | 14 + modules/widgets/test/imports.node.spec.ts | 22 ++ package.json | 2 + vitest.config.ts | 11 + yarn.lock | 69 ++++- 10 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 dev/README.md create mode 100644 modules/editable-layers/test/immutable-feature-collection.node.spec.ts create mode 100644 modules/editable-layers/test/imports.node.spec.ts create mode 100644 modules/geo-layers/test/imports.node.spec.ts create mode 100644 modules/layers/test/imports.node.spec.ts create mode 100644 modules/widgets/test/imports.node.spec.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..f4943e6ba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,125 @@ +name: test + +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Lint + run: yarn lint + + test-node: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Build packages + run: yarn build + + - name: Run node tests with coverage + run: yarn test-ci + + - name: Coveralls + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fail-on-error: false + + test-headless: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Build packages + run: yarn build + + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + + - name: Run headless browser tests + run: yarn test-headless diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 000000000..811ac0638 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,17 @@ +# dev/ + +This directory holds content that is **not** part of production builds or the public documentation site: + +- **`rfc/`** — Design RFCs and architectural proposals +- **`wip/`** — Work-in-progress module stubs, experiments, and half-finished features +- **`examples-wip/`** — Exploratory examples not yet ready for publication + +## Why a separate `dev/` directory? + +Files inside `docs/` are indexed by the documentation site's search engine, which can surface outdated or incomplete content to users. Keeping WIP and RFC content here avoids that noise while still preserving it in the source tree for maintainers and contributors. + +## RFCs + +Design RFCs for individual modules live in `rfc/`. Prefer using GitHub Issues or Pull Requests for new proposals so that commenting and linking is easier, but historical RFCs with architectural rationale can be preserved here. + +See also: [deck.gl-community issues](https://github.com/visgl/deck.gl-community/issues) for active roadmap discussions. diff --git a/modules/editable-layers/test/immutable-feature-collection.node.spec.ts b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts new file mode 100644 index 000000000..fa9c6f413 --- /dev/null +++ b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts @@ -0,0 +1,272 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {describe, it, expect} from 'vitest'; +import {ImmutableFeatureCollection} from '../src/edit-modes/immutable-feature-collection'; +import type {SimpleFeatureCollection} from '../src/utils/geojson-types'; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +function makeCollection(features: SimpleFeatureCollection['features']): ImmutableFeatureCollection { + return new ImmutableFeatureCollection({type: 'FeatureCollection', features}); +} + +const POINT_FEATURE = { + type: 'Feature' as const, + geometry: {type: 'Point' as const, coordinates: [1, 2]}, + properties: {} +}; + +const LINE_FEATURE = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + }, + properties: {} +}; + +const POLYGON_FEATURE = { + type: 'Feature' as const, + geometry: { + type: 'Polygon' as const, + // outer ring (closed) + coordinates: [ + [ + [0, 0], + [1, 0], + [1, 1], + [0, 1], + [0, 0] + ] + ] + }, + properties: {} +}; + +// --------------------------------------------------------------------------- +// addFeature / addFeatures +// --------------------------------------------------------------------------- + +describe('addFeature', () => { + it('appends a feature and returns a new collection', () => { + const fc = makeCollection([POINT_FEATURE]); + const updated = fc.addFeature(LINE_FEATURE); + + expect(updated.featureCollection.features).toHaveLength(2); + // original is unchanged + expect(fc.featureCollection.features).toHaveLength(1); + }); + + it('addFeatures appends multiple features', () => { + const fc = makeCollection([]); + const updated = fc.addFeatures([POINT_FEATURE, LINE_FEATURE]); + + expect(updated.featureCollection.features).toHaveLength(2); + }); +}); + +// --------------------------------------------------------------------------- +// deleteFeature / deleteFeatures +// --------------------------------------------------------------------------- + +describe('deleteFeature', () => { + it('removes a feature by index', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]); + const updated = fc.deleteFeature(1); + + expect(updated.featureCollection.features).toHaveLength(2); + expect(updated.featureCollection.features[0].geometry.type).toBe('Point'); + expect(updated.featureCollection.features[1].geometry.type).toBe('Polygon'); + }); + + it('deleteFeatures removes multiple features in correct order', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]); + const updated = fc.deleteFeatures([0, 2]); + + expect(updated.featureCollection.features).toHaveLength(1); + expect(updated.featureCollection.features[0].geometry.type).toBe('LineString'); + }); + + it('ignores out-of-range indexes', () => { + const fc = makeCollection([POINT_FEATURE]); + const updated = fc.deleteFeature(99); + + expect(updated.featureCollection.features).toHaveLength(1); + }); +}); + +// --------------------------------------------------------------------------- +// replaceGeometry +// --------------------------------------------------------------------------- + +describe('replaceGeometry', () => { + it('replaces the geometry of a feature', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE]); + const newGeom = {type: 'Point' as const, coordinates: [99, 99]}; + const updated = fc.replaceGeometry(0, newGeom); + + expect((updated.featureCollection.features[0].geometry as any).coordinates).toEqual([99, 99]); + // feature 1 is unchanged + expect(updated.featureCollection.features[1].geometry.type).toBe('LineString'); + // original is unchanged + expect((fc.featureCollection.features[0].geometry as any).coordinates).toEqual([1, 2]); + }); +}); + +// --------------------------------------------------------------------------- +// replacePosition — LineString +// --------------------------------------------------------------------------- + +describe('replacePosition (LineString)', () => { + it('replaces a coordinate at a given index', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.replacePosition(0, [1], [9, 9]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords[1]).toEqual([9, 9]); + // other positions unchanged + expect(coords[0]).toEqual([0, 0]); + expect(coords[2]).toEqual([2, 2]); + }); + + it('preserves elevation when updating 2D position over a 3D point', () => { + const feature3D = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0, 10], + [1, 1, 20] + ] + }, + properties: {} + }; + const fc = makeCollection([feature3D]); + const updated = fc.replacePosition(0, [0], [5, 5]); // no elevation given + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords[0]).toEqual([5, 5, 10]); // elevation copied from original + }); + + it('is immutable — original collection unchanged', () => { + const fc = makeCollection([LINE_FEATURE]); + fc.replacePosition(0, [0], [99, 99]); + + const coords = (fc.featureCollection.features[0].geometry as any).coordinates; + expect(coords[0]).toEqual([0, 0]); + }); +}); + +// --------------------------------------------------------------------------- +// replacePosition — Polygon (wraps first/last) +// --------------------------------------------------------------------------- + +describe('replacePosition (Polygon)', () => { + it('updates both first and last coordinate when replacing index 0', () => { + const fc = makeCollection([POLYGON_FEATURE]); + const updated = fc.replacePosition(0, [0, 0], [5, 5]); + + const ring = (updated.featureCollection.features[0].geometry as any).coordinates[0]; + expect(ring[0]).toEqual([5, 5]); + expect(ring[ring.length - 1]).toEqual([5, 5]); + }); +}); + +// --------------------------------------------------------------------------- +// addPosition +// --------------------------------------------------------------------------- + +describe('addPosition (LineString)', () => { + it('inserts a position at the given index', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.addPosition(0, [1], [0.5, 0.5]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords).toHaveLength(4); + expect(coords[1]).toEqual([0.5, 0.5]); + expect(coords[2]).toEqual([1, 1]); + }); + + it('throws when adding a position to a Point', () => { + const fc = makeCollection([POINT_FEATURE]); + expect(() => fc.addPosition(0, [0], [1, 1])).toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +// removePosition +// --------------------------------------------------------------------------- + +describe('removePosition (LineString)', () => { + it('removes a position from a LineString', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.removePosition(0, [1]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords).toHaveLength(2); + expect(coords[0]).toEqual([0, 0]); + expect(coords[1]).toEqual([2, 2]); + }); + + it('throws when removing from a Point', () => { + const fc = makeCollection([POINT_FEATURE]); + expect(() => fc.removePosition(0, [0])).toThrow(); + }); + + it('throws when LineString would have fewer than 2 positions', () => { + const twoPointLine = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0], + [1, 1] + ] + }, + properties: {} + }; + const fc = makeCollection([twoPointLine]); + expect(() => fc.removePosition(0, [0])).toThrow(); + }); + + it('throws when Polygon outer ring would have fewer than 4 positions', () => { + const triangle = { + type: 'Feature' as const, + geometry: { + type: 'Polygon' as const, + coordinates: [ + [ + [0, 0], + [1, 0], + [0.5, 1], + [0, 0] + ] + ] + }, + properties: {} + }; + const fc = makeCollection([triangle]); + expect(() => fc.removePosition(0, [0, 0])).toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +// getObject +// --------------------------------------------------------------------------- + +describe('getObject', () => { + it('returns the underlying feature collection', () => { + const raw = {type: 'FeatureCollection' as const, features: [POINT_FEATURE]}; + const fc = new ImmutableFeatureCollection(raw); + + expect(fc.getObject()).toBe(raw); + }); +}); diff --git a/modules/editable-layers/test/imports.node.spec.ts b/modules/editable-layers/test/imports.node.spec.ts new file mode 100644 index 000000000..c9fdff2cc --- /dev/null +++ b/modules/editable-layers/test/imports.node.spec.ts @@ -0,0 +1,61 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// Smoke test: verify the public API surface exports resolve without errors. +// This runs in Node (no WebGL) to catch broken re-exports early. + +import {it, expect} from 'vitest'; +import * as EditableLayers from '../src/index'; + +it('exports ImmutableFeatureCollection', () => { + expect(EditableLayers.ImmutableFeatureCollection).toBeDefined(); +}); + +it('exports GeoJsonEditMode', () => { + expect(EditableLayers.GeoJsonEditMode).toBeDefined(); +}); + +it('exports draw modes', () => { + const drawModes = [ + 'DrawPointMode', + 'DrawLineStringMode', + 'DrawPolygonMode', + 'DrawRectangleMode', + 'DrawCircleByDiameterMode', + 'DrawCircleFromCenterMode' + ] as const; + + for (const name of drawModes) { + expect(EditableLayers[name], `${name} should be exported`).toBeDefined(); + } +}); + +it('exports alter modes', () => { + const alterModes = [ + 'ModifyMode', + 'TranslateMode', + 'ScaleMode', + 'RotateMode', + 'DeleteMode', + 'DuplicateMode', + 'SplitPolygonMode', + 'TransformMode' + ] as const; + + for (const name of alterModes) { + expect(EditableLayers[name], `${name} should be exported`).toBeDefined(); + } +}); + +it('exports measurement modes', () => { + expect(EditableLayers.MeasureDistanceMode).toBeDefined(); + expect(EditableLayers.MeasureAreaMode).toBeDefined(); + expect(EditableLayers.MeasureAngleMode).toBeDefined(); +}); + +it('exports composite modes', () => { + expect(EditableLayers.CompositeMode).toBeDefined(); + expect(EditableLayers.SnappableMode).toBeDefined(); + expect(EditableLayers.ViewMode).toBeDefined(); +}); diff --git a/modules/geo-layers/test/imports.node.spec.ts b/modules/geo-layers/test/imports.node.spec.ts new file mode 100644 index 000000000..16c0e3e66 --- /dev/null +++ b/modules/geo-layers/test/imports.node.spec.ts @@ -0,0 +1,21 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as GeoLayers from '../src/index'; + +it('exports TileSourceLayer', () => { + expect(GeoLayers.TileSourceLayer).toBeDefined(); +}); + +it('exports GlobalGridLayer', () => { + expect(GeoLayers.GlobalGridLayer).toBeDefined(); +}); + +it('exports grid systems', () => { + expect(GeoLayers.H3Grid).toBeDefined(); + expect(GeoLayers.S2Grid).toBeDefined(); + expect(GeoLayers.GeohashGrid).toBeDefined(); + expect(GeoLayers.QuadkeyGrid).toBeDefined(); +}); diff --git a/modules/layers/test/imports.node.spec.ts b/modules/layers/test/imports.node.spec.ts new file mode 100644 index 000000000..0feba5afc --- /dev/null +++ b/modules/layers/test/imports.node.spec.ts @@ -0,0 +1,14 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as Layers from '../src/index'; + +it('exports PathOutlineLayer', () => { + expect(Layers.PathOutlineLayer).toBeDefined(); +}); + +it('exports PathMarkerLayer', () => { + expect(Layers.PathMarkerLayer).toBeDefined(); +}); diff --git a/modules/widgets/test/imports.node.spec.ts b/modules/widgets/test/imports.node.spec.ts new file mode 100644 index 000000000..6d8c2410d --- /dev/null +++ b/modules/widgets/test/imports.node.spec.ts @@ -0,0 +1,22 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as Widgets from '../src/index'; + +it('exports PanWidget', () => { + expect(Widgets.PanWidget).toBeDefined(); +}); + +it('exports ZoomRangeWidget', () => { + expect(Widgets.ZoomRangeWidget).toBeDefined(); +}); + +it('exports HtmlOverlayWidget', () => { + expect(Widgets.HtmlOverlayWidget).toBeDefined(); +}); + +it('exports HtmlTooltipWidget', () => { + expect(Widgets.HtmlTooltipWidget).toBeDefined(); +}); diff --git a/package.json b/package.json index 817d91417..249ab0942 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "publish-prod": "ocular-publish version-only-prod", "test": "yarn test-node", "test-node": "vitest --project node", + "test-ci": "vitest run --project node --coverage", "test-browser": "vitest --project browser", "test-headless": "vitest run --project headless", "metrics": "ocular-metrics", @@ -49,6 +50,7 @@ "@vitejs/plugin-react": "^5.1.4", "@vitest/browser": "^4.0.18", "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "@vitest/eslint-plugin": "^1.6.9", "bin-pack": "^1.0.2", "datauri": "^4.1.0", diff --git a/vitest.config.ts b/vitest.config.ts index 1b923c0ce..2348553de 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,17 @@ const CONFIG = defineConfig({ conditions: ['node'] // prefer node resolution }, test: { + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['modules/*/src/**/*.{ts,tsx,js,jsx}'], + exclude: [ + 'modules/basemap-props/**', + 'modules/basemap-props-wip/**', + 'modules/template/**', + 'modules/*/src/**/*.d.ts' + ] + }, projects: [ { test: { diff --git a/yarn.lock b/yarn.lock index 66046489e..bca23f258 100644 --- a/yarn.lock +++ b/yarn.lock @@ -262,6 +262,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3 + languageName: node + linkType: hard + "@carto/api-client@npm:^0.5.19": version: 0.5.24 resolution: "@carto/api-client@npm:0.5.24" @@ -1781,7 +1788,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -4964,6 +4971,30 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^4.0.18": + version: 4.0.18 + resolution: "@vitest/coverage-v8@npm:4.0.18" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.0.18" + ast-v8-to-istanbul: "npm:^0.3.10" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.1" + obug: "npm:^2.1.1" + std-env: "npm:^3.10.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + "@vitest/browser": 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + languageName: node + linkType: hard + "@vitest/eslint-plugin@npm:^1.6.9": version: 1.6.9 resolution: "@vitest/eslint-plugin@npm:1.6.9" @@ -5561,6 +5592,17 @@ __metadata: languageName: node linkType: hard +"ast-v8-to-istanbul@npm:^0.3.10": + version: 0.3.12 + resolution: "ast-v8-to-istanbul@npm:0.3.12" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10c0/bad6ba222b1073c165c8d65dbf366193d4a90536dabe37f93a3df162269b1c9473975756e4c048f708c235efccc26f8e5321c547b7e9563b64b21b2e0f27cbc9 + languageName: node + linkType: hard + "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -7003,6 +7045,7 @@ __metadata: "@vitejs/plugin-react": "npm:^5.1.4" "@vitest/browser": "npm:^4.0.18" "@vitest/browser-playwright": "npm:^4.0.18" + "@vitest/coverage-v8": "npm:^4.0.18" "@vitest/eslint-plugin": "npm:^1.6.9" bin-pack: "npm:^1.0.2" datauri: "npm:^4.1.0" @@ -10505,14 +10548,14 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -10523,7 +10566,7 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.4": +"istanbul-reports@npm:^3.1.4, istanbul-reports@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-reports@npm:3.2.0" dependencies: @@ -10608,6 +10651,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -11316,6 +11366,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.1": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 + languageName: node + linkType: hard + "make-dir@npm:4.0.0, make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" From ac06d499a3d59643b39d6de74df6f6a22be9914d Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 00:34:20 -0500 Subject: [PATCH 03/16] =?UTF-8?q?chore:=20remove=20dev/=20directory=20?= =?UTF-8?q?=E2=80=94=20RFCs=20move=20to=20GitHub=20Issues/Discussions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per discussion: RFCs are better tracked as GitHub Issues or Discussions where commenting and cross-linking are native. The docs tree stays focused on true usability docs and basic rationale only. Co-Authored-By: Claude Sonnet 4.6 --- dev/README.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 dev/README.md diff --git a/dev/README.md b/dev/README.md deleted file mode 100644 index 811ac0638..000000000 --- a/dev/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# dev/ - -This directory holds content that is **not** part of production builds or the public documentation site: - -- **`rfc/`** — Design RFCs and architectural proposals -- **`wip/`** — Work-in-progress module stubs, experiments, and half-finished features -- **`examples-wip/`** — Exploratory examples not yet ready for publication - -## Why a separate `dev/` directory? - -Files inside `docs/` are indexed by the documentation site's search engine, which can surface outdated or incomplete content to users. Keeping WIP and RFC content here avoids that noise while still preserving it in the source tree for maintainers and contributors. - -## RFCs - -Design RFCs for individual modules live in `rfc/`. Prefer using GitHub Issues or Pull Requests for new proposals so that commenting and linking is easier, but historical RFCs with architectural rationale can be preserved here. - -See also: [deck.gl-community issues](https://github.com/visgl/deck.gl-community/issues) for active roadmap discussions. From a39314cd12a46219e2719736673093991f8c4835 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 00:42:06 -0500 Subject: [PATCH 04/16] refactor: flat dev/ for WIP modules and examples, published modules stay in modules/ Separates in-progress work from the published modules tree. All npm-published 9.2.8 packages remain untouched in modules/. Moved to dev/ (flat): - modules/timeline-layers -> dev/timeline-layers (in progress, refs #517 #379) - examples/timeline-layers/horizon-graph-layer -> dev/timeline-layers/examples/ - examples/arrow-layers-wip -> dev/arrow-layers (renamed, drop -wip suffix) Config: - package.json + lerna.json: add dev/* to workspaces/packages - tsconfig.json: add dev/ to include, add timeline-layers path alias - vitest.config.ts: include dev/ in node/browser/headless globs and coverage - keep modules/basemap-props/** excluded from node tests (uses global describe) Rationale from #512: dev/ keeps WIP visible without polluting docs search or the published modules tree. Design proposals live in GitHub Issues/Discussions. Co-Authored-By: Claude Sonnet 4.6 --- dev/README.md | 10 +++++++++ .../arrow-layers}/linestring/README.md | 0 .../arrow-layers}/linestring/app.tsx | 0 .../arrow-layers}/linestring/index.html | 0 .../arrow-layers}/linestring/package.json | 0 .../arrow-layers}/multilinestring/app.tsx | 0 .../arrow-layers}/multilinestring/index.html | 0 .../multilinestring/package.json | 0 .../arrow-layers}/multipoint/app.tsx | 0 .../arrow-layers}/multipoint/index.html | 0 .../arrow-layers}/multipoint/package.json | 0 .../arrow-layers}/multipolygon/README.md | 0 .../arrow-layers}/multipolygon/app.tsx | 0 .../multipolygon/generate_data.py | 0 .../arrow-layers}/multipolygon/index.html | 0 .../arrow-layers}/multipolygon/package.json | 0 .../poetry.lock.disable-dependabot | 0 .../arrow-layers}/multipolygon/pyproject.toml | 0 .../arrow-layers}/point/README.md | 0 .../arrow-layers}/point/app.tsx | 0 .../arrow-layers}/point/generate_data.py | 0 .../arrow-layers}/point/index.html | 0 .../arrow-layers}/point/package.json | 0 .../point/poetry.lock.disable-dependabot | 0 .../arrow-layers}/point/pyproject.toml | 0 .../arrow-layers}/polygon/README.md | 0 .../arrow-layers}/polygon/app.tsx | 0 .../arrow-layers}/polygon/generate_data.py | 0 .../arrow-layers}/polygon/index.html | 0 .../arrow-layers}/polygon/package.json | 0 .../polygon/poetry.lock.disable-dependabot | 0 .../arrow-layers}/polygon/pyproject.toml | 0 .../arrow-layers}/text/app.tsx | 0 .../arrow-layers}/text/generate_data.py | 0 .../arrow-layers}/text/index.html | 0 .../arrow-layers}/text/package.json | 0 .../text/poetry.lock.disable-dependabot | 0 .../arrow-layers}/text/pyproject.toml | 0 .../arrow-layers}/trips/README.md | 0 .../arrow-layers}/trips/app.tsx | 0 .../arrow-layers}/trips/generate_data.py | 0 .../arrow-layers}/trips/index.html | 0 .../arrow-layers}/trips/package.json | 0 .../trips/poetry.lock.disable-dependabot | 0 .../arrow-layers}/trips/pyproject.toml | 0 {modules => dev}/timeline-layers/LICENSE | 0 {modules => dev}/timeline-layers/README.md | 0 .../examples}/horizon-graph-layer/app.tsx | 0 .../examples}/horizon-graph-layer/index.html | 0 .../examples}/horizon-graph-layer/index.tsx | 0 .../horizon-graph-layer/package.json | 0 .../horizon-graph-layer/tsconfig.json | 0 {modules => dev}/timeline-layers/package.json | 0 {modules => dev}/timeline-layers/src/index.ts | 0 .../horizon-graph-layer-uniforms.ts | 0 .../horizon-graph-layer.fs.ts | 0 .../horizon-graph-layer.ts | 0 .../horizon-graph-layer.vs.ts | 0 .../multi-horizon-graph-layer.ts | 0 .../src/layers/time-axis-layer.ts | 0 .../src/layers/vertical-grid-layer.ts | 0 .../timeline-layers/src/utils/format-utils.ts | 0 .../timeline-layers/src/utils/tick-utils.ts | 0 .../timeline-layers/test/index.spec.ts | 0 .../timeline-layers/tsconfig.json | 0 lerna.json | 3 ++- package.json | 1 + tsconfig.json | 4 +++- vitest.config.ts | 22 +++++++++---------- 69 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 dev/README.md rename {examples/arrow-layers-wip => dev/arrow-layers}/linestring/README.md (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/linestring/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/linestring/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/linestring/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multilinestring/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multilinestring/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multilinestring/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipoint/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipoint/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipoint/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/README.md (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/generate_data.py (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/poetry.lock.disable-dependabot (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/multipolygon/pyproject.toml (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/README.md (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/generate_data.py (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/poetry.lock.disable-dependabot (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/point/pyproject.toml (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/README.md (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/generate_data.py (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/poetry.lock.disable-dependabot (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/polygon/pyproject.toml (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/generate_data.py (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/poetry.lock.disable-dependabot (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/text/pyproject.toml (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/README.md (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/app.tsx (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/generate_data.py (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/index.html (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/package.json (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/poetry.lock.disable-dependabot (100%) rename {examples/arrow-layers-wip => dev/arrow-layers}/trips/pyproject.toml (100%) rename {modules => dev}/timeline-layers/LICENSE (100%) rename {modules => dev}/timeline-layers/README.md (100%) rename {examples/timeline-layers => dev/timeline-layers/examples}/horizon-graph-layer/app.tsx (100%) rename {examples/timeline-layers => dev/timeline-layers/examples}/horizon-graph-layer/index.html (100%) rename {examples/timeline-layers => dev/timeline-layers/examples}/horizon-graph-layer/index.tsx (100%) rename {examples/timeline-layers => dev/timeline-layers/examples}/horizon-graph-layer/package.json (100%) rename {examples/timeline-layers => dev/timeline-layers/examples}/horizon-graph-layer/tsconfig.json (100%) rename {modules => dev}/timeline-layers/package.json (100%) rename {modules => dev}/timeline-layers/src/index.ts (100%) rename {modules => dev}/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts (100%) rename {modules => dev}/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts (100%) rename {modules => dev}/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts (100%) rename {modules => dev}/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts (100%) rename {modules => dev}/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts (100%) rename {modules => dev}/timeline-layers/src/layers/time-axis-layer.ts (100%) rename {modules => dev}/timeline-layers/src/layers/vertical-grid-layer.ts (100%) rename {modules => dev}/timeline-layers/src/utils/format-utils.ts (100%) rename {modules => dev}/timeline-layers/src/utils/tick-utils.ts (100%) rename {modules => dev}/timeline-layers/test/index.spec.ts (100%) rename {modules => dev}/timeline-layers/tsconfig.json (100%) diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 000000000..e8d88f4de --- /dev/null +++ b/dev/README.md @@ -0,0 +1,10 @@ +# dev/ + +Work-in-progress modules and examples. Not published to npm or indexed by the docs site. + +| Directory | Type | Status | +|---|---|---| +| `timeline-layers/` | module + examples | in progress — see #517, #379 | +| `arrow-layers/` | examples | arrow-layers WIP examples | + +Design proposals and RFCs belong in [GitHub Issues/Discussions](https://github.com/visgl/deck.gl-community/issues). diff --git a/examples/arrow-layers-wip/linestring/README.md b/dev/arrow-layers/linestring/README.md similarity index 100% rename from examples/arrow-layers-wip/linestring/README.md rename to dev/arrow-layers/linestring/README.md diff --git a/examples/arrow-layers-wip/linestring/app.tsx b/dev/arrow-layers/linestring/app.tsx similarity index 100% rename from examples/arrow-layers-wip/linestring/app.tsx rename to dev/arrow-layers/linestring/app.tsx diff --git a/examples/arrow-layers-wip/linestring/index.html b/dev/arrow-layers/linestring/index.html similarity index 100% rename from examples/arrow-layers-wip/linestring/index.html rename to dev/arrow-layers/linestring/index.html diff --git a/examples/arrow-layers-wip/linestring/package.json b/dev/arrow-layers/linestring/package.json similarity index 100% rename from examples/arrow-layers-wip/linestring/package.json rename to dev/arrow-layers/linestring/package.json diff --git a/examples/arrow-layers-wip/multilinestring/app.tsx b/dev/arrow-layers/multilinestring/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multilinestring/app.tsx rename to dev/arrow-layers/multilinestring/app.tsx diff --git a/examples/arrow-layers-wip/multilinestring/index.html b/dev/arrow-layers/multilinestring/index.html similarity index 100% rename from examples/arrow-layers-wip/multilinestring/index.html rename to dev/arrow-layers/multilinestring/index.html diff --git a/examples/arrow-layers-wip/multilinestring/package.json b/dev/arrow-layers/multilinestring/package.json similarity index 100% rename from examples/arrow-layers-wip/multilinestring/package.json rename to dev/arrow-layers/multilinestring/package.json diff --git a/examples/arrow-layers-wip/multipoint/app.tsx b/dev/arrow-layers/multipoint/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multipoint/app.tsx rename to dev/arrow-layers/multipoint/app.tsx diff --git a/examples/arrow-layers-wip/multipoint/index.html b/dev/arrow-layers/multipoint/index.html similarity index 100% rename from examples/arrow-layers-wip/multipoint/index.html rename to dev/arrow-layers/multipoint/index.html diff --git a/examples/arrow-layers-wip/multipoint/package.json b/dev/arrow-layers/multipoint/package.json similarity index 100% rename from examples/arrow-layers-wip/multipoint/package.json rename to dev/arrow-layers/multipoint/package.json diff --git a/examples/arrow-layers-wip/multipolygon/README.md b/dev/arrow-layers/multipolygon/README.md similarity index 100% rename from examples/arrow-layers-wip/multipolygon/README.md rename to dev/arrow-layers/multipolygon/README.md diff --git a/examples/arrow-layers-wip/multipolygon/app.tsx b/dev/arrow-layers/multipolygon/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multipolygon/app.tsx rename to dev/arrow-layers/multipolygon/app.tsx diff --git a/examples/arrow-layers-wip/multipolygon/generate_data.py b/dev/arrow-layers/multipolygon/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/multipolygon/generate_data.py rename to dev/arrow-layers/multipolygon/generate_data.py diff --git a/examples/arrow-layers-wip/multipolygon/index.html b/dev/arrow-layers/multipolygon/index.html similarity index 100% rename from examples/arrow-layers-wip/multipolygon/index.html rename to dev/arrow-layers/multipolygon/index.html diff --git a/examples/arrow-layers-wip/multipolygon/package.json b/dev/arrow-layers/multipolygon/package.json similarity index 100% rename from examples/arrow-layers-wip/multipolygon/package.json rename to dev/arrow-layers/multipolygon/package.json diff --git a/examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot b/dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot rename to dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/multipolygon/pyproject.toml b/dev/arrow-layers/multipolygon/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/multipolygon/pyproject.toml rename to dev/arrow-layers/multipolygon/pyproject.toml diff --git a/examples/arrow-layers-wip/point/README.md b/dev/arrow-layers/point/README.md similarity index 100% rename from examples/arrow-layers-wip/point/README.md rename to dev/arrow-layers/point/README.md diff --git a/examples/arrow-layers-wip/point/app.tsx b/dev/arrow-layers/point/app.tsx similarity index 100% rename from examples/arrow-layers-wip/point/app.tsx rename to dev/arrow-layers/point/app.tsx diff --git a/examples/arrow-layers-wip/point/generate_data.py b/dev/arrow-layers/point/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/point/generate_data.py rename to dev/arrow-layers/point/generate_data.py diff --git a/examples/arrow-layers-wip/point/index.html b/dev/arrow-layers/point/index.html similarity index 100% rename from examples/arrow-layers-wip/point/index.html rename to dev/arrow-layers/point/index.html diff --git a/examples/arrow-layers-wip/point/package.json b/dev/arrow-layers/point/package.json similarity index 100% rename from examples/arrow-layers-wip/point/package.json rename to dev/arrow-layers/point/package.json diff --git a/examples/arrow-layers-wip/point/poetry.lock.disable-dependabot b/dev/arrow-layers/point/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/point/poetry.lock.disable-dependabot rename to dev/arrow-layers/point/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/point/pyproject.toml b/dev/arrow-layers/point/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/point/pyproject.toml rename to dev/arrow-layers/point/pyproject.toml diff --git a/examples/arrow-layers-wip/polygon/README.md b/dev/arrow-layers/polygon/README.md similarity index 100% rename from examples/arrow-layers-wip/polygon/README.md rename to dev/arrow-layers/polygon/README.md diff --git a/examples/arrow-layers-wip/polygon/app.tsx b/dev/arrow-layers/polygon/app.tsx similarity index 100% rename from examples/arrow-layers-wip/polygon/app.tsx rename to dev/arrow-layers/polygon/app.tsx diff --git a/examples/arrow-layers-wip/polygon/generate_data.py b/dev/arrow-layers/polygon/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/polygon/generate_data.py rename to dev/arrow-layers/polygon/generate_data.py diff --git a/examples/arrow-layers-wip/polygon/index.html b/dev/arrow-layers/polygon/index.html similarity index 100% rename from examples/arrow-layers-wip/polygon/index.html rename to dev/arrow-layers/polygon/index.html diff --git a/examples/arrow-layers-wip/polygon/package.json b/dev/arrow-layers/polygon/package.json similarity index 100% rename from examples/arrow-layers-wip/polygon/package.json rename to dev/arrow-layers/polygon/package.json diff --git a/examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot b/dev/arrow-layers/polygon/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot rename to dev/arrow-layers/polygon/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/polygon/pyproject.toml b/dev/arrow-layers/polygon/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/polygon/pyproject.toml rename to dev/arrow-layers/polygon/pyproject.toml diff --git a/examples/arrow-layers-wip/text/app.tsx b/dev/arrow-layers/text/app.tsx similarity index 100% rename from examples/arrow-layers-wip/text/app.tsx rename to dev/arrow-layers/text/app.tsx diff --git a/examples/arrow-layers-wip/text/generate_data.py b/dev/arrow-layers/text/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/text/generate_data.py rename to dev/arrow-layers/text/generate_data.py diff --git a/examples/arrow-layers-wip/text/index.html b/dev/arrow-layers/text/index.html similarity index 100% rename from examples/arrow-layers-wip/text/index.html rename to dev/arrow-layers/text/index.html diff --git a/examples/arrow-layers-wip/text/package.json b/dev/arrow-layers/text/package.json similarity index 100% rename from examples/arrow-layers-wip/text/package.json rename to dev/arrow-layers/text/package.json diff --git a/examples/arrow-layers-wip/text/poetry.lock.disable-dependabot b/dev/arrow-layers/text/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/text/poetry.lock.disable-dependabot rename to dev/arrow-layers/text/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/text/pyproject.toml b/dev/arrow-layers/text/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/text/pyproject.toml rename to dev/arrow-layers/text/pyproject.toml diff --git a/examples/arrow-layers-wip/trips/README.md b/dev/arrow-layers/trips/README.md similarity index 100% rename from examples/arrow-layers-wip/trips/README.md rename to dev/arrow-layers/trips/README.md diff --git a/examples/arrow-layers-wip/trips/app.tsx b/dev/arrow-layers/trips/app.tsx similarity index 100% rename from examples/arrow-layers-wip/trips/app.tsx rename to dev/arrow-layers/trips/app.tsx diff --git a/examples/arrow-layers-wip/trips/generate_data.py b/dev/arrow-layers/trips/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/trips/generate_data.py rename to dev/arrow-layers/trips/generate_data.py diff --git a/examples/arrow-layers-wip/trips/index.html b/dev/arrow-layers/trips/index.html similarity index 100% rename from examples/arrow-layers-wip/trips/index.html rename to dev/arrow-layers/trips/index.html diff --git a/examples/arrow-layers-wip/trips/package.json b/dev/arrow-layers/trips/package.json similarity index 100% rename from examples/arrow-layers-wip/trips/package.json rename to dev/arrow-layers/trips/package.json diff --git a/examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot b/dev/arrow-layers/trips/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot rename to dev/arrow-layers/trips/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/trips/pyproject.toml b/dev/arrow-layers/trips/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/trips/pyproject.toml rename to dev/arrow-layers/trips/pyproject.toml diff --git a/modules/timeline-layers/LICENSE b/dev/timeline-layers/LICENSE similarity index 100% rename from modules/timeline-layers/LICENSE rename to dev/timeline-layers/LICENSE diff --git a/modules/timeline-layers/README.md b/dev/timeline-layers/README.md similarity index 100% rename from modules/timeline-layers/README.md rename to dev/timeline-layers/README.md diff --git a/examples/timeline-layers/horizon-graph-layer/app.tsx b/dev/timeline-layers/examples/horizon-graph-layer/app.tsx similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/app.tsx rename to dev/timeline-layers/examples/horizon-graph-layer/app.tsx diff --git a/examples/timeline-layers/horizon-graph-layer/index.html b/dev/timeline-layers/examples/horizon-graph-layer/index.html similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/index.html rename to dev/timeline-layers/examples/horizon-graph-layer/index.html diff --git a/examples/timeline-layers/horizon-graph-layer/index.tsx b/dev/timeline-layers/examples/horizon-graph-layer/index.tsx similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/index.tsx rename to dev/timeline-layers/examples/horizon-graph-layer/index.tsx diff --git a/examples/timeline-layers/horizon-graph-layer/package.json b/dev/timeline-layers/examples/horizon-graph-layer/package.json similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/package.json rename to dev/timeline-layers/examples/horizon-graph-layer/package.json diff --git a/examples/timeline-layers/horizon-graph-layer/tsconfig.json b/dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/tsconfig.json rename to dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json diff --git a/modules/timeline-layers/package.json b/dev/timeline-layers/package.json similarity index 100% rename from modules/timeline-layers/package.json rename to dev/timeline-layers/package.json diff --git a/modules/timeline-layers/src/index.ts b/dev/timeline-layers/src/index.ts similarity index 100% rename from modules/timeline-layers/src/index.ts rename to dev/timeline-layers/src/index.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts diff --git a/modules/timeline-layers/src/layers/time-axis-layer.ts b/dev/timeline-layers/src/layers/time-axis-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/time-axis-layer.ts rename to dev/timeline-layers/src/layers/time-axis-layer.ts diff --git a/modules/timeline-layers/src/layers/vertical-grid-layer.ts b/dev/timeline-layers/src/layers/vertical-grid-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/vertical-grid-layer.ts rename to dev/timeline-layers/src/layers/vertical-grid-layer.ts diff --git a/modules/timeline-layers/src/utils/format-utils.ts b/dev/timeline-layers/src/utils/format-utils.ts similarity index 100% rename from modules/timeline-layers/src/utils/format-utils.ts rename to dev/timeline-layers/src/utils/format-utils.ts diff --git a/modules/timeline-layers/src/utils/tick-utils.ts b/dev/timeline-layers/src/utils/tick-utils.ts similarity index 100% rename from modules/timeline-layers/src/utils/tick-utils.ts rename to dev/timeline-layers/src/utils/tick-utils.ts diff --git a/modules/timeline-layers/test/index.spec.ts b/dev/timeline-layers/test/index.spec.ts similarity index 100% rename from modules/timeline-layers/test/index.spec.ts rename to dev/timeline-layers/test/index.spec.ts diff --git a/modules/timeline-layers/tsconfig.json b/dev/timeline-layers/tsconfig.json similarity index 100% rename from modules/timeline-layers/tsconfig.json rename to dev/timeline-layers/tsconfig.json diff --git a/lerna.json b/lerna.json index 19063139e..a2519130e 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,8 @@ "version": "9.2.8", "npmClient": "yarn", "packages": [ - "modules/*" + "modules/*", + "dev/*" ], "publishConfig": { "access": "public" diff --git a/package.json b/package.json index 249ab0942..7e08e4470 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "type": "module", "workspaces": [ "modules/*", + "dev/*", "examples/**" ], "scripts": { diff --git a/tsconfig.json b/tsconfig.json index dcc5733fe..2bf349b8d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,8 @@ "@deck.gl-community/react": ["./modules/react/src"], "@deck.gl-community/template": ["./modules/template/src"], "@deck.gl-community/graph-layers": ["./modules/graph-layers/src"], - "@deck.gl-community/three": ["./modules/three/src"] + "@deck.gl-community/three": ["./modules/three/src"], + "@deck.gl-community/timeline-layers": ["./dev/timeline-layers/src"] }, "typeRoots": ["./node_modules/@types"], "plugins": [ @@ -64,6 +65,7 @@ }, "include": [ "modules", + "dev", "test", "examples", "scripts" diff --git a/vitest.config.ts b/vitest.config.ts index 2348553de..f4d2fa2b7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,22 +13,18 @@ const CONFIG = defineConfig({ coverage: { provider: 'v8', reporter: ['text', 'lcov'], - include: ['modules/*/src/**/*.{ts,tsx,js,jsx}'], - exclude: [ - 'modules/basemap-props/**', - 'modules/basemap-props-wip/**', - 'modules/template/**', - 'modules/*/src/**/*.d.ts' - ] + include: ['modules/*/src/**/*.{ts,tsx,js,jsx}', 'dev/*/src/**/*.{ts,tsx,js,jsx}'], + exclude: ['modules/template/**', 'modules/*/src/**/*.d.ts', 'dev/*/src/**/*.d.ts'] }, projects: [ { test: { name: 'node', environment: 'node', - include: ['modules/**/*.{test,spec}.{js,ts}'], + include: ['modules/**/*.{test,spec}.{js,ts}', 'dev/**/*.{test,spec}.{js,ts}'], exclude: [ 'modules/**/*.browser.{test,spec}.{js,ts}', + 'dev/**/*.browser.{test,spec}.{js,ts}', 'modules/basemap-props/**' ], browser: { @@ -43,9 +39,10 @@ const CONFIG = defineConfig({ environment: 'node', include: [ 'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', - 'modules/**/*.{test,spec}.{jsx,tsx}' + 'modules/**/*.{test,spec}.{jsx,tsx}', + 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', + 'dev/**/*.{test,spec}.{jsx,tsx}' ], - exclude: ['modules/basemap-props/**'], browser: { enabled: true, provider: playwright(), @@ -60,9 +57,10 @@ const CONFIG = defineConfig({ environment: 'node', include: [ 'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', - 'modules/**/*.{test,spec}.{jsx,tsx}' + 'modules/**/*.{test,spec}.{jsx,tsx}', + 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', + 'dev/**/*.{test,spec}.{jsx,tsx}' ], - exclude: ['modules/basemap-props/**'], browser: { enabled: true, headless: true, From 8a8acf4f66cb70ebaaa7a8d2cb4fd28af6e69eb2 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 00:48:39 -0500 Subject: [PATCH 05/16] chore: update yarn.lock after timeline-layers workspace path change Moving timeline-layers from modules/ to dev/ changed its workspace identifier. Yarn 4 hardened mode on public PRs treats installs as --immutable, so the lockfile must be committed with the updated path. Co-Authored-By: Claude Sonnet 4.6 --- yarn.lock | 630 +++--------------------------------------------------- 1 file changed, 24 insertions(+), 606 deletions(-) diff --git a/yarn.lock b/yarn.lock index bca23f258..e0f50fd51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -195,13 +195,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.0": - version: 7.28.6 - resolution: "@babel/runtime@npm:7.28.6" - checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d - languageName: node - linkType: hard - "@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" @@ -269,21 +262,6 @@ __metadata: languageName: node linkType: hard -"@carto/api-client@npm:^0.5.19": - version: 0.5.24 - resolution: "@carto/api-client@npm:0.5.24" - dependencies: - "@loaders.gl/schema": "npm:^4.3.3" - "@types/geojson": "npm:^7946.0.16" - d3-format: "npm:^3.1.0" - d3-scale: "npm:^4.0.2" - h3-js: "npm:^4.1.0" - jsep: "npm:^1.4.0" - quadbin: "npm:^0.4.1-alpha.0" - checksum: 10c0/1a110955a9c0eefb4f322b1241d8a858d4cafd292ad988973c56765cad62c2f002a056f4692442e59129e483bc2814e2731bdd92492f213a7ebda2a241af76cf - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -293,7 +271,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl-community/arrow-layers@workspace:*, @deck.gl-community/arrow-layers@workspace:modules/arrow-layers": +"@deck.gl-community/arrow-layers@workspace:modules/arrow-layers": version: 0.0.0-use.local resolution: "@deck.gl-community/arrow-layers@workspace:modules/arrow-layers" dependencies: @@ -532,9 +510,9 @@ __metadata: languageName: unknown linkType: soft -"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:modules/timeline-layers": +"@deck.gl-community/timeline-layers@workspace:dev/timeline-layers": version: 0.0.0-use.local - resolution: "@deck.gl-community/timeline-layers@workspace:modules/timeline-layers" + resolution: "@deck.gl-community/timeline-layers@workspace:dev/timeline-layers" dependencies: "@deck.gl/core": "npm:~9.2.8" "@deck.gl/layers": "npm:~9.2.8" @@ -558,7 +536,7 @@ __metadata: languageName: unknown linkType: soft -"@deck.gl/aggregation-layers@npm:9.2.8, @deck.gl/aggregation-layers@npm:~9.2.8": +"@deck.gl/aggregation-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/aggregation-layers@npm:9.2.8" dependencies: @@ -576,62 +554,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/arcgis@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/arcgis@npm:9.2.8" - dependencies: - "@luma.gl/constants": "npm:^9.2.6" - esri-loader: "npm:^3.7.0" - peerDependencies: - "@arcgis/core": ^4.0.0 - "@deck.gl/core": ~9.2.0 - "@luma.gl/core": ~9.2.6 - "@luma.gl/engine": ~9.2.6 - "@luma.gl/webgl": ~9.2.6 - checksum: 10c0/2083b2dc5cb25982d0eab06b443be4a90f8315aca9b048c8578d9d2e1c43d9aa772db8ed2fcb726fb3c323a2e1af4f8abbe3c2189e13ede1f7346d88804f2d6b - languageName: node - linkType: hard - -"@deck.gl/carto@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/carto@npm:9.2.8" - dependencies: - "@carto/api-client": "npm:^0.5.19" - "@loaders.gl/compression": "npm:^4.3.4" - "@loaders.gl/gis": "npm:^4.3.4" - "@loaders.gl/loader-utils": "npm:^4.3.4" - "@loaders.gl/mvt": "npm:^4.3.4" - "@loaders.gl/schema": "npm:^4.3.4" - "@loaders.gl/tiles": "npm:^4.3.4" - "@luma.gl/core": "npm:^9.2.6" - "@luma.gl/shadertools": "npm:^9.2.6" - "@math.gl/web-mercator": "npm:^4.1.0" - "@types/d3-array": "npm:^3.0.2" - "@types/d3-color": "npm:^1.4.2" - "@types/d3-scale": "npm:^3.0.0" - cartocolor: "npm:^5.0.2" - d3-array: "npm:^3.2.0" - d3-color: "npm:^3.1.0" - d3-format: "npm:^3.1.0" - d3-scale: "npm:^4.0.0" - earcut: "npm:^2.2.4" - h3-js: "npm:^4.1.0" - moment-timezone: "npm:^0.6.0" - pbf: "npm:^3.2.1" - quadbin: "npm:^0.4.0" - peerDependencies: - "@deck.gl/aggregation-layers": ~9.2.0 - "@deck.gl/core": ~9.2.0 - "@deck.gl/extensions": ~9.2.0 - "@deck.gl/geo-layers": ~9.2.0 - "@deck.gl/layers": ~9.2.0 - "@loaders.gl/core": ^4.3.4 - "@luma.gl/core": ~9.2.6 - checksum: 10c0/6d76eb6ba48ce2ba9791888af08c0439db81f27c916fec645fec327082bf625f9f63056b45b756a71d62a28a8f639777ba255a50d36b427bec68e4e62f0973cd - languageName: node - linkType: hard - -"@deck.gl/core@npm:9.2.8, @deck.gl/core@npm:~9.2.8": +"@deck.gl/core@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/core@npm:9.2.8" dependencies: @@ -656,7 +579,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/extensions@npm:9.2.8, @deck.gl/extensions@npm:~9.2.8": +"@deck.gl/extensions@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/extensions@npm:9.2.8" dependencies: @@ -671,7 +594,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/geo-layers@npm:9.2.8, @deck.gl/geo-layers@npm:~9.2.8": +"@deck.gl/geo-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/geo-layers@npm:9.2.8" dependencies: @@ -704,35 +627,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/google-maps@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/google-maps@npm:9.2.8" - dependencies: - "@luma.gl/constants": "npm:^9.2.6" - "@luma.gl/webgl": "npm:^9.2.6" - "@math.gl/core": "npm:^4.1.0" - "@types/google.maps": "npm:^3.48.6" - peerDependencies: - "@deck.gl/core": ~9.2.0 - "@luma.gl/constants": ~9.2.6 - "@luma.gl/core": ~9.2.6 - "@luma.gl/webgl": ~9.2.6 - checksum: 10c0/bdf7d26c29b7ae73c9f955ef2966fdcfa99fdfb94908e26e35be0e16850de4a7e84770e5eff1418acdac9eed1018a6478a42286ce36a26815fdeeabe5ca66376 - languageName: node - linkType: hard - -"@deck.gl/json@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/json@npm:9.2.8" - dependencies: - jsep: "npm:^0.3.0" - peerDependencies: - "@deck.gl/core": ~9.2.0 - checksum: 10c0/f46cd6aeb25bd09c43a5da17a39d539ab1e9e70225423fced353317160b0fbef59c628152c546212e11c50582593099d25898e5bbe98d00012a4ec077a4585ab - languageName: node - linkType: hard - -"@deck.gl/layers@npm:9.2.8, @deck.gl/layers@npm:~9.2.8": +"@deck.gl/layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/layers@npm:9.2.8" dependencies: @@ -753,22 +648,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/mapbox@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/mapbox@npm:9.2.8" - dependencies: - "@luma.gl/constants": "npm:^9.2.6" - "@math.gl/web-mercator": "npm:^4.1.0" - peerDependencies: - "@deck.gl/core": ~9.2.0 - "@luma.gl/constants": ~9.2.6 - "@luma.gl/core": ~9.2.6 - "@math.gl/web-mercator": ^4.1.0 - checksum: 10c0/2846a382b48bcf59c9c1d1246f7914597a9a2179ebb5246b71799b0c2ccdca5f84cadaae4a2fd9918671383fe1b22347f66ad7ecce1b1523632621ed94e77f47 - languageName: node - linkType: hard - -"@deck.gl/mesh-layers@npm:9.2.8, @deck.gl/mesh-layers@npm:~9.2.8": +"@deck.gl/mesh-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/mesh-layers@npm:9.2.8" dependencies: @@ -786,7 +666,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/react@npm:9.2.8, @deck.gl/react@npm:~9.2.8": +"@deck.gl/react@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/react@npm:9.2.8" peerDependencies: @@ -813,7 +693,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/widgets@npm:9.2.8, @deck.gl/widgets@npm:~9.2.8": +"@deck.gl/widgets@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/widgets@npm:9.2.8" dependencies: @@ -1917,7 +1797,7 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/compression@npm:4.3.4, @loaders.gl/compression@npm:^4.3.4": +"@loaders.gl/compression@npm:4.3.4": version: 4.3.4 resolution: "@loaders.gl/compression@npm:4.3.4" dependencies: @@ -2093,7 +1973,7 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.3, @loaders.gl/schema@npm:^4.3.4": +"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.4": version: 4.3.4 resolution: "@loaders.gl/schema@npm:4.3.4" dependencies: @@ -2343,13 +2223,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/geojson-types@npm:^1.0.2": - version: 1.0.2 - resolution: "@mapbox/geojson-types@npm:1.0.2" - checksum: 10c0/aa0a2cb95a358d8756ab5aa70356bcbd6f554a4571703a88a09e7db6580061d6ef4054db5fe3ecb2817c383b8b5433746a8f46712dc606b32063f73b154f99fc - languageName: node - linkType: hard - "@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2": version: 2.0.2 resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" @@ -2378,15 +2251,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/mapbox-gl-supported@npm:^1.5.0": - version: 1.5.0 - resolution: "@mapbox/mapbox-gl-supported@npm:1.5.0" - peerDependencies: - mapbox-gl: ">=0.32.1 <2.0.0" - checksum: 10c0/5b7712e8b546e598dc5152632504cad53081211b64ddc4447825840ddca703275bc36599167b9550ab906ca8a9554936bcdae562073fdef24b8d38d78ee262fb - languageName: node - linkType: hard - "@mapbox/martini@npm:^0.2.0": version: 0.2.0 resolution: "@mapbox/martini@npm:0.2.0" @@ -2401,13 +2265,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/tiny-sdf@npm:^1.1.1": - version: 1.2.5 - resolution: "@mapbox/tiny-sdf@npm:1.2.5" - checksum: 10c0/de0252388a628ddb491c986c715f0b63ca6a74f5dac16d3e51eb75a21935a31e34fba5db47c81cc59a462d782021fc68b2e3cc119b2d6cabe15d31e311674d6c - languageName: node - linkType: hard - "@mapbox/tiny-sdf@npm:^2.0.5, @mapbox/tiny-sdf@npm:^2.0.6": version: 2.0.7 resolution: "@mapbox/tiny-sdf@npm:2.0.7" @@ -2555,16 +2412,6 @@ __metadata: languageName: node linkType: hard -"@math.gl/web-mercator@npm:^3.5.5": - version: 3.6.3 - resolution: "@math.gl/web-mercator@npm:3.6.3" - dependencies: - "@babel/runtime": "npm:^7.12.0" - gl-matrix: "npm:^3.4.0" - checksum: 10c0/95a1267261e03fa0194ec5ac5f3f8c3a13e1182cf3865d4d3bbd669706d42bd067335d1812df2e844c63c1f0f21c50e08af2b90168f2db1d3fa3ba2087f59703 - languageName: node - linkType: hard - "@monaco-editor/loader@npm:^1.5.0": version: 1.6.1 resolution: "@monaco-editor/loader@npm:1.6.1" @@ -4230,36 +4077,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:^3.0.2": - version: 3.2.2 - resolution: "@types/d3-array@npm:3.2.2" - checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028 - languageName: node - linkType: hard - -"@types/d3-color@npm:^1.4.2": - version: 1.4.5 - resolution: "@types/d3-color@npm:1.4.5" - checksum: 10c0/d84233fdfb3ea81710a2aa7eb8f332a0199dfbefb10cd81a68e504dfb2a00b78d309ebe0b1b452c97d7b9de193a301b8ce3cec0842ec300277caef34ef2dd8ed - languageName: node - linkType: hard - -"@types/d3-scale@npm:^3.0.0": - version: 3.3.5 - resolution: "@types/d3-scale@npm:3.3.5" - dependencies: - "@types/d3-time": "npm:^2" - checksum: 10c0/2689ab13092e3fded22cdd1b888afd91aa60190be40c8eddc12b2d42de59b00917778340f90317c68c5ffc3a1bee68f5ca155434cd466bc7804f400f3f9e7529 - languageName: node - linkType: hard - -"@types/d3-time@npm:^2": - version: 2.1.4 - resolution: "@types/d3-time@npm:2.1.4" - checksum: 10c0/b597bfa51a163d4231e953d6903b06fd6341d0f11a28222a79fafaddb46155d7f458a67c814de53df84926a47dd535897228a475679d228576b0cda87351e534 - languageName: node - linkType: hard - "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -4297,20 +4114,6 @@ __metadata: languageName: node linkType: hard -"@types/google.maps@npm:^3.48.6": - version: 3.58.1 - resolution: "@types/google.maps@npm:3.58.1" - checksum: 10c0/0247c61d2aeb216d04ab9392602ef41d58328c6336ed4e9a4e9e69834002e82c116a34def2bf4ceb31c4292f14899815058da4bb8d803147295c6b47c1162621 - languageName: node - linkType: hard - -"@types/hammerjs@npm:^2.0.41": - version: 2.0.46 - resolution: "@types/hammerjs@npm:2.0.46" - checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:*": version: 3.3.7 resolution: "@types/hoist-non-react-statics@npm:3.3.7" @@ -4354,15 +4157,6 @@ __metadata: languageName: node linkType: hard -"@types/mapbox-gl@npm:^2.0.3": - version: 2.7.21 - resolution: "@types/mapbox-gl@npm:2.7.21" - dependencies: - "@types/geojson": "npm:*" - checksum: 10c0/51bcf2a0481a53a8f8148af9b7f0f9e4d1be2ebdd9390194e4e4a0f8c8375446004c8624e67b4a255384cf0a0ae0c5b9eb99fa865acadca0ac0b0f5853f3f957 - languageName: node - linkType: hard - "@types/mapbox__point-geometry@npm:*, @types/mapbox__point-geometry@npm:^0.1.4": version: 0.1.4 resolution: "@types/mapbox__point-geometry@npm:0.1.4" @@ -4500,16 +4294,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18.0.0": - version: 18.3.28 - resolution: "@types/react@npm:18.3.28" - dependencies: - "@types/prop-types": "npm:*" - csstype: "npm:^3.2.2" - checksum: 10c0/683e19cd12b5c691215529af2e32b5ffbaccae3bf0ba93bfafa0e460e8dfee18423afed568be2b8eadf4b837c3749dd296a4f64e2d79f68fa66962c05f5af661 - languageName: node - linkType: hard - "@types/react@npm:^18.3.3": version: 18.3.26 resolution: "@types/react@npm:18.3.26" @@ -5308,7 +5092,7 @@ __metadata: languageName: node linkType: hard -"apache-arrow@npm:>=14, apache-arrow@npm:>=15": +"apache-arrow@npm:>=15": version: 21.1.0 resolution: "apache-arrow@npm:21.1.0" dependencies: @@ -6123,15 +5907,6 @@ __metadata: languageName: node linkType: hard -"cartocolor@npm:^5.0.2": - version: 5.0.2 - resolution: "cartocolor@npm:5.0.2" - dependencies: - colorbrewer: "npm:1.5.6" - checksum: 10c0/305412d0354ed90b7db050f055a56fd68a95c2e9659d55c1f93272cd30d6c1148519e0b55378be1a1ad2c61147f63c3426c317f8ce5d7359f47266dc2d92f663 - languageName: node - linkType: hard - "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -6394,13 +6169,6 @@ __metadata: languageName: node linkType: hard -"colorbrewer@npm:1.5.6": - version: 1.5.6 - resolution: "colorbrewer@npm:1.5.6" - checksum: 10c0/da8045058698e2df21df1b676184d9db6651b5c4c2d4775d24bfb60c761150c4e102b54db4a85f4b3019726b5a16053b040f026ee0214cc0ee6ba8612d8a54cd - languageName: node - linkType: hard - "columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -6744,7 +6512,7 @@ __metadata: languageName: node linkType: hard -"csscolorparser@npm:~1.0.2, csscolorparser@npm:~1.0.3": +"csscolorparser@npm:~1.0.2": version: 1.0.3 resolution: "csscolorparser@npm:1.0.3" checksum: 10c0/57b30e1dd3e639fb74d63d3ee5a078ae6d0aaba26bc731fdec1a0f50fd77c2531a1fca2bbe07c18e0569255f92064cda0747e0115955fdb8c037332798ca634f @@ -6767,13 +6535,6 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.2.2": - version: 3.2.3 - resolution: "csstype@npm:3.2.3" - checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce - languageName: node - linkType: hard - "cwise-compiler@npm:^1.0.0": version: 1.1.3 resolution: "cwise-compiler@npm:1.1.3" @@ -6790,7 +6551,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.0, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4": +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4": version: 3.2.4 resolution: "d3-array@npm:3.2.4" dependencies: @@ -6799,7 +6560,7 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": +"d3-color@npm:1 - 3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c @@ -6875,7 +6636,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:^4.0.0, d3-scale@npm:^4.0.2": +"d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -7062,166 +6823,6 @@ __metadata: languageName: unknown linkType: soft -"deck.gl@npm:^9.1.0": - version: 9.2.8 - resolution: "deck.gl@npm:9.2.8" - dependencies: - "@deck.gl/aggregation-layers": "npm:9.2.8" - "@deck.gl/arcgis": "npm:9.2.8" - "@deck.gl/carto": "npm:9.2.8" - "@deck.gl/core": "npm:9.2.8" - "@deck.gl/extensions": "npm:9.2.8" - "@deck.gl/geo-layers": "npm:9.2.8" - "@deck.gl/google-maps": "npm:9.2.8" - "@deck.gl/json": "npm:9.2.8" - "@deck.gl/layers": "npm:9.2.8" - "@deck.gl/mapbox": "npm:9.2.8" - "@deck.gl/mesh-layers": "npm:9.2.8" - "@deck.gl/react": "npm:9.2.8" - "@deck.gl/widgets": "npm:9.2.8" - "@loaders.gl/core": "npm:^4.3.4" - "@luma.gl/core": "npm:^9.2.6" - "@luma.gl/engine": "npm:^9.2.6" - peerDependencies: - "@arcgis/core": ^4.0.0 - react: ">=16.3.0" - react-dom: ">=16.3.0" - peerDependenciesMeta: - "@arcgis/core": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 10c0/1a442f0fd2f3cdc419b84ebea12acd525581ab6d61d9903342f4f3e05d55722369f9379d40325df12acdd968947fc5f75e931740f926ba6441b7ccf01a694a9d - languageName: node - linkType: hard - -"deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -7549,7 +7150,7 @@ __metadata: languageName: node linkType: hard -"earcut@npm:^2.2.2, earcut@npm:^2.2.4": +"earcut@npm:^2.2.4": version: 2.2.4 resolution: "earcut@npm:2.2.4" checksum: 10c0/01ca51830edd2787819f904ae580087d37351f6048b4565e7add4b3da8a86b7bc19262ab2aa7fdc64129ab03af2d9cec8cccee4d230c82275f97ef285c79aafb @@ -8580,13 +8181,6 @@ __metadata: languageName: node linkType: hard -"esri-loader@npm:^3.7.0": - version: 3.7.0 - resolution: "esri-loader@npm:3.7.0" - checksum: 10c0/56c8febebb8698cb9884b9dd5d6c5800da0f4f3b66a10c0959892564621fbf976d73a3e65c5e940f86f7498897d50dff7354e17862041e9698a92a15efc60f8b - languageName: node - linkType: hard - "estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" @@ -9146,13 +8740,6 @@ __metadata: languageName: node linkType: hard -"geojson-vt@npm:^3.2.1": - version: 3.2.1 - resolution: "geojson-vt@npm:3.2.1" - checksum: 10c0/db2fc1a452067ee8436fa86e5a138f6ebd3d64893e0af097bc1cc960ec63d67c0ce77444711e9583036192d6bf9ce754bf9b56a76789684fc0fea4d52321fffc - languageName: node - linkType: hard - "geojson-vt@npm:^4.0.2": version: 4.0.2 resolution: "geojson-vt@npm:4.0.2" @@ -9356,7 +8943,7 @@ __metadata: languageName: node linkType: hard -"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.2.1, gl-matrix@npm:^3.4.0, gl-matrix@npm:^3.4.3": +"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.4.3": version: 3.4.4 resolution: "gl-matrix@npm:3.4.4" checksum: 10c0/9aa022ffac0d158212ad0cd29939864ad919ac31cd5dc5a5d35e9d66bb62679ddf152ff7b2173ded20131045e40572b87f31b26a920be2a7583a1516b13b5b4b @@ -9527,13 +9114,6 @@ __metadata: languageName: node linkType: hard -"grid-index@npm:^1.1.0": - version: 1.1.0 - resolution: "grid-index@npm:1.1.0" - checksum: 10c0/0ba2a622a52badc86642a002abee79b48c207092347e869528253e634573b9b55494db649fbd1779122d797aaa3b59e284023a96a7a5c666c7375f0e320f8f57 - languageName: node - linkType: hard - "h3-js@npm:^4.1.0, h3-js@npm:^4.2.1": version: 4.3.0 resolution: "h3-js@npm:4.3.0" @@ -9541,13 +9121,6 @@ __metadata: languageName: node linkType: hard -"hammerjs@npm:^2.0.8": - version: 2.0.8 - resolution: "hammerjs@npm:2.0.8" - checksum: 10c0/5c95e5774b5ea49492cb3fa8f1949aea67048a0b84af33acb555e7139abfcf3c83aca2b83e0c5008755bc230166df7b5e469d1e3eb6746c48f215f3672609fed - languageName: node - linkType: hard - "handlebars@npm:^4.7.7": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -9700,25 +9273,6 @@ __metadata: languageName: node linkType: hard -"horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer": - version: 0.0.0-use.local - resolution: "horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer" - dependencies: - "@deck.gl-community/timeline-layers": "workspace:*" - "@deck.gl/core": "npm:~9.2.8" - "@deck.gl/layers": "npm:~9.2.8" - "@deck.gl/react": "npm:~9.2.8" - "@deck.gl/widgets": "npm:~9.2.8" - "@loaders.gl/core": "npm:^4.2.0" - "@luma.gl/core": "npm:~9.2.6" - "@luma.gl/engine": "npm:~9.2.6" - "@luma.gl/shadertools": "npm:~9.2.6" - react: "npm:^18.3.1" - react-dom: "npm:^18.3.1" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -10695,20 +10249,6 @@ __metadata: languageName: node linkType: hard -"jsep@npm:^0.3.0": - version: 0.3.5 - resolution: "jsep@npm:0.3.5" - checksum: 10c0/fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614 - languageName: node - linkType: hard - -"jsep@npm:^1.4.0": - version: 1.4.0 - resolution: "jsep@npm:1.4.0" - checksum: 10c0/fe60adf47e050e22eadced42514a51a15a3cf0e2d147896584486acd8ee670fc16641101b9aeb81f4aaba382043d29744b7aac41171e8106515b14f27e0c7116 - languageName: node - linkType: hard - "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -10913,13 +10453,6 @@ __metadata: languageName: node linkType: hard -"kdbush@npm:^3.0.0": - version: 3.0.0 - resolution: "kdbush@npm:3.0.0" - checksum: 10c0/3fc8795870bd04f60627e7345b26fd0644beb91bc4164912c9d9378b39c674ba01c31db68ecaf6266d51c9ad81bf5b770b7effa51eeee37553d38293a094a686 - languageName: node - linkType: hard - "kdbush@npm:^4.0.2": version: 4.0.2 resolution: "kdbush@npm:4.0.2" @@ -11456,36 +10989,6 @@ __metadata: languageName: node linkType: hard -"mapbox-gl@npm:^1.0.0": - version: 1.13.3 - resolution: "mapbox-gl@npm:1.13.3" - dependencies: - "@mapbox/geojson-rewind": "npm:^0.5.2" - "@mapbox/geojson-types": "npm:^1.0.2" - "@mapbox/jsonlint-lines-primitives": "npm:^2.0.2" - "@mapbox/mapbox-gl-supported": "npm:^1.5.0" - "@mapbox/point-geometry": "npm:^0.1.0" - "@mapbox/tiny-sdf": "npm:^1.1.1" - "@mapbox/unitbezier": "npm:^0.0.0" - "@mapbox/vector-tile": "npm:^1.3.1" - "@mapbox/whoots-js": "npm:^3.1.0" - csscolorparser: "npm:~1.0.3" - earcut: "npm:^2.2.2" - geojson-vt: "npm:^3.2.1" - gl-matrix: "npm:^3.2.1" - grid-index: "npm:^1.1.0" - murmurhash-js: "npm:^1.0.0" - pbf: "npm:^3.2.1" - potpack: "npm:^1.0.1" - quickselect: "npm:^2.0.0" - rw: "npm:^1.3.3" - supercluster: "npm:^7.1.0" - tinyqueue: "npm:^2.0.3" - vt-pbf: "npm:^3.1.1" - checksum: 10c0/82bf86daa69e068d4138c4e5a1dbfc2a28c6d0e2cc142ef96d3ce671770e8173d3ffd62e6614b3606f932f99b40eb4dfc52d5f44c48dabd393ba27b61d9e50c4 - languageName: node - linkType: hard - "maplibre-gl@npm:^4.1.3": version: 4.7.1 resolution: "maplibre-gl@npm:4.7.1" @@ -11893,16 +11396,6 @@ __metadata: languageName: node linkType: hard -"mjolnir.js@npm:^2.5.0": - version: 2.7.3 - resolution: "mjolnir.js@npm:2.7.3" - dependencies: - "@types/hammerjs": "npm:^2.0.41" - hammerjs: "npm:^2.0.8" - checksum: 10c0/82f86dedfd410a640e5d0ef6f04e1d72ab292e61d42434bc56f103670ada45b2aacf130527f434d7d4bd0b8d2d5bc82bd36850e7586f1b70ebb72da2fe2c72b2 - languageName: node - linkType: hard - "mjolnir.js@npm:^3.0.0": version: 3.0.0 resolution: "mjolnir.js@npm:3.0.0" @@ -11947,22 +11440,6 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.6.0": - version: 0.6.0 - resolution: "moment-timezone@npm:0.6.0" - dependencies: - moment: "npm:^2.29.4" - checksum: 10c0/16164cf321d8be0bf7d43855286b426c94c8200e0634f2e42cf469f591c6a230ac43f37d3826d76b05ac221f69a571400323fb8625e3d4e8669f4d9ab00fe779 - languageName: node - linkType: hard - -"moment@npm:^2.29.4": - version: 2.30.1 - resolution: "moment@npm:2.30.1" - checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a - languageName: node - linkType: hard - "monaco-editor@npm:^0.53.0": version: 0.53.0 resolution: "monaco-editor@npm:0.53.0" @@ -13300,13 +12777,6 @@ __metadata: languageName: node linkType: hard -"potpack@npm:^1.0.1": - version: 1.0.2 - resolution: "potpack@npm:1.0.2" - checksum: 10c0/670c23898a4257130858b960c2e654d3327c0f6a7e7091ff5846f213e65af8f9476320b995b8ad561a47a4d1c359c7ef347de57d22e7b02597051abb52bc85c4 - languageName: node - linkType: hard - "potpack@npm:^2.0.0": version: 2.1.0 resolution: "potpack@npm:2.1.0" @@ -13567,15 +13037,6 @@ __metadata: languageName: node linkType: hard -"quadbin@npm:^0.4.0, quadbin@npm:^0.4.1-alpha.0": - version: 0.4.2 - resolution: "quadbin@npm:0.4.2" - dependencies: - "@math.gl/web-mercator": "npm:^4.1.0" - checksum: 10c0/40ad1efa7146762e3cf6d9abfe47d452e69cceb0a72b5caec27e8bdf66d1e4522d6a1bde7e643015d9651eaa9f12611208f073856662fb6266263dcbf72d6dd8 - languageName: node - linkType: hard - "quadprog@npm:^1.6.1": version: 1.6.1 resolution: "quadprog@npm:1.6.1" @@ -13655,7 +13116,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.0.0, react-dom@npm:^18.3.1": +"react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -13714,24 +13175,6 @@ __metadata: languageName: node linkType: hard -"react-map-gl@npm:^5.3.0": - version: 5.3.21 - resolution: "react-map-gl@npm:5.3.21" - dependencies: - "@babel/runtime": "npm:^7.0.0" - "@types/geojson": "npm:^7946.0.7" - "@types/mapbox-gl": "npm:^2.0.3" - mapbox-gl: "npm:^1.0.0" - mjolnir.js: "npm:^2.5.0" - prop-types: "npm:^15.7.2" - resize-observer-polyfill: "npm:^1.5.1" - viewport-mercator-project: "npm:^7.0.4" - peerDependencies: - react: ">=16.3.0" - checksum: 10c0/fa7bf984b3c5af19dfdaf12018753bfe3cfd7a040bf94cb3ad669f5f1aa42a7ae8c32a8b4523554b16ba1c532197800a4656ecd7c0e2f8e5818b16b1c00ae0a3 - languageName: node - linkType: hard - "react-map-gl@npm:^7.1.7": version: 7.1.9 resolution: "react-map-gl@npm:7.1.9" @@ -13803,7 +13246,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.0.0, react@npm:^18.3.1": +"react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -14019,13 +13462,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -15275,15 +14711,6 @@ __metadata: languageName: node linkType: hard -"supercluster@npm:^7.1.0": - version: 7.1.5 - resolution: "supercluster@npm:7.1.5" - dependencies: - kdbush: "npm:^3.0.0" - checksum: 10c0/bbebf45927d0019831731c94b78d1c9a1f3e2da0be9875d7ea75c6f261487e0f15d3f1822a9a49256e3c1672bdfb9138f9a5e44e2de99edb06cd1e758083e12d - languageName: node - linkType: hard - "supercluster@npm:^8.0.1": version: 8.0.1 resolution: "supercluster@npm:8.0.1" @@ -15634,7 +15061,7 @@ __metadata: languageName: node linkType: hard -"tinyqueue@npm:^2.0.0, tinyqueue@npm:^2.0.3": +"tinyqueue@npm:^2.0.0": version: 2.0.3 resolution: "tinyqueue@npm:2.0.3" checksum: 10c0/d7b590088f015a94a17132fa209c2f2a80c45158259af5474901fdf5932e95ea13ff6f034bcc725a6d5f66d3e5b888b048c310229beb25ad5bebb4f0a635abf2 @@ -16258,15 +15685,6 @@ __metadata: languageName: node linkType: hard -"viewport-mercator-project@npm:^7.0.4": - version: 7.0.4 - resolution: "viewport-mercator-project@npm:7.0.4" - dependencies: - "@math.gl/web-mercator": "npm:^3.5.5" - checksum: 10c0/9a8a12f4bd68355a8162a23d4b161133b45aee71b2453fd8d57f54c55466007428cf502d0c02f58620d4c92ba934c29608613bc3176dd8c1d348f8a9deb34e32 - languageName: node - linkType: hard - "vite@npm:^4.5.0": version: 4.5.14 resolution: "vite@npm:4.5.14" @@ -16494,7 +15912,7 @@ __metadata: languageName: node linkType: hard -"vt-pbf@npm:^3.1.1, vt-pbf@npm:^3.1.3": +"vt-pbf@npm:^3.1.3": version: 3.1.3 resolution: "vt-pbf@npm:3.1.3" dependencies: From 9d54c5815c323d93b2b42b6776bf9fd20935d6de Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 01:18:38 -0500 Subject: [PATCH 06/16] fix(website): update horizon-graph-layer path after move to dev/timeline-layers The example was moved from examples/timeline-layers/horizon-graph-layer to dev/timeline-layers/examples/horizon-graph-layer. Update the website component require() and code link to match. Co-Authored-By: Claude Sonnet 4.6 --- website/src/examples/timeline-layers/horizon-graph-layer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/examples/timeline-layers/horizon-graph-layer.tsx b/website/src/examples/timeline-layers/horizon-graph-layer.tsx index 47700c297..2fbb44b2b 100644 --- a/website/src/examples/timeline-layers/horizon-graph-layer.tsx +++ b/website/src/examples/timeline-layers/horizon-graph-layer.tsx @@ -8,7 +8,7 @@ import {makeExample} from '../../components'; class HorizonDemo extends Component { static title = 'Horizon Graph Layer Demo'; - static code = `${GITHUB_TREE}/examples/timeline-layers/horizon-graph-layer`; + static code = `${GITHUB_TREE}/dev/timeline-layers/examples/horizon-graph-layer`; static renderInfo(meta) { return <>; @@ -20,7 +20,7 @@ class HorizonDemo extends Component { return ( {() => { - const App = require('../../../../examples/timeline-layers/horizon-graph-layer/app').default; + const App = require('../../../../dev/timeline-layers/examples/horizon-graph-layer/app').default; return ; }} From 96f1e6004e350c74b6840fff21c9fe3d60f6851a Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 01:25:08 -0500 Subject: [PATCH 07/16] fix(website): update timeline-layers webpack alias to dev/ path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the move of modules/timeline-layers → dev/timeline-layers. Co-Authored-By: Claude Sonnet 4.6 --- website/docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ce51f43b0..83ae83fa9 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -88,7 +88,7 @@ const config = { '@deck.gl-community/leaflet': resolve('../modules/leaflet/src'), '@deck.gl-community/graph-layers': resolve('../modules/graph-layers/src'), '@deck.gl-community/infovis-layers': resolve('../modules/infovis-layers/src'), - '@deck.gl-community/timeline-layers': resolve('../modules/timeline-layers/src'), + '@deck.gl-community/timeline-layers': resolve('../dev/timeline-layers/src'), '@deck.gl-community/react': resolve('../modules/react/src'), '@deck.gl-community/layers': resolve('../modules/layers/src'), '@deck.gl-community/arrow-layers': resolve('../modules/arrow-layers/src'), From 674ddfc71455aa412fa460c82695ad5bf03785e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:12:04 +0000 Subject: [PATCH 08/16] feat: add TimelineLayer to timeline-layers module with example Co-authored-by: charlieforward9 <62311337+charlieforward9@users.noreply.github.com> --- .../examples/timeline-layer/README.md | 38 + .../examples/timeline-layer/app.tsx | 174 ++++ .../examples/timeline-layer/demo-controls.tsx | 520 +++++++++++ .../examples/timeline-layer/demo-utils.ts | 64 ++ .../examples/timeline-layer/index.html | 17 + .../examples/timeline-layer/index.tsx | 10 + .../examples/timeline-layer/package.json | 19 + .../examples/timeline-layer/timeline-hooks.ts | 467 ++++++++++ .../examples/timeline-layer/tsconfig.json | 4 + dev/timeline-layers/src/index.ts | 41 + .../timeline-layer/timeline-collision.ts | 54 ++ .../layers/timeline-layer/timeline-layer.ts | 867 ++++++++++++++++++ .../layers/timeline-layer/timeline-layout.ts | 80 ++ .../layers/timeline-layer/timeline-types.ts | 141 +++ .../layers/timeline-layer/timeline-utils.ts | 85 ++ dev/timeline-layers/test/index.spec.ts | 31 +- yarn.lock | 74 +- 17 files changed, 2683 insertions(+), 3 deletions(-) create mode 100644 dev/timeline-layers/examples/timeline-layer/README.md create mode 100644 dev/timeline-layers/examples/timeline-layer/app.tsx create mode 100644 dev/timeline-layers/examples/timeline-layer/demo-controls.tsx create mode 100644 dev/timeline-layers/examples/timeline-layer/demo-utils.ts create mode 100644 dev/timeline-layers/examples/timeline-layer/index.html create mode 100644 dev/timeline-layers/examples/timeline-layer/index.tsx create mode 100644 dev/timeline-layers/examples/timeline-layer/package.json create mode 100644 dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts create mode 100644 dev/timeline-layers/examples/timeline-layer/tsconfig.json create mode 100644 dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts create mode 100644 dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts create mode 100644 dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts create mode 100644 dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts create mode 100644 dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts diff --git a/dev/timeline-layers/examples/timeline-layer/README.md b/dev/timeline-layers/examples/timeline-layer/README.md new file mode 100644 index 000000000..aa57866e0 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/README.md @@ -0,0 +1,38 @@ +# Timeline Layer Example + +An interactive timeline visualization built with `@deck.gl-community/timeline-layers`. + +## Features + +- Multi-track timeline with clip visualization +- Collision detection for overlapping clips (automatic subtrack assignment) +- Interactive playhead scrubber with drag support +- Zoom (mouse wheel) and pan (click + drag when zoomed) +- Clip and track selection with hover highlighting +- Multiple time formatting options + +## Usage + +```bash +yarn start +``` + +Open [http://localhost:5173](http://localhost:5173) + +## API + +Import `TimelineLayer` from `@deck.gl-community/timeline-layers`: + +```ts +import {TimelineLayer} from '@deck.gl-community/timeline-layers'; + +const layer = new TimelineLayer({ + data: tracks, // TimelineTrack[] + timelineStart: 0, // start of full range (ms) + timelineEnd: 60000, // end of full range (ms) + currentTimeMs: 5000, // current playhead position + onClipClick: ({clip, track}) => console.log('clicked', clip.label) +}); +``` + +See `TimelineLayerProps` for the full API reference. diff --git a/dev/timeline-layers/examples/timeline-layer/app.tsx b/dev/timeline-layers/examples/timeline-layer/app.tsx new file mode 100644 index 000000000..d3765d9c6 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/app.tsx @@ -0,0 +1,174 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {ReactElement, useMemo, useEffect} from 'react'; +import DeckGL from '@deck.gl/react'; +import {OrthographicView, type OrthographicViewState} 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 ORTHOGRAPHIC_VIEW = new OrthographicView(); + +const CONTROLLER_CONFIG = { + scrollZoom: false, + doubleClickZoom: false, + touchZoom: false, + dragPan: false, + dragRotate: false, + keyboard: false +} as const; + +export default function App(): ReactElement { + const {state, controls, trackCount, clipsPerTrack, labelFormatterType} = useTimelineControls(); + const {state: interactionState, setState} = useTimelineInteractionState(); + const {timelineLayerRef, containerRef} = useTimelineRefs(); + + const initialViewState = useMemo((): OrthographicViewState => { + const w = (typeof window !== 'undefined' ? window.innerWidth : 1280) - 320; + const h = typeof window !== 'undefined' ? window.innerHeight : 720; + return {target: [w / 2, h / 2, 0], zoom: 0}; + }, []); + + 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); + + const viewport = useMemo( + () => ({startMs: state.viewportStartMs, endMs: state.viewportEndMs}), + [state.viewportStartMs, state.viewportEndMs] + ); + + 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 timelineLayer = useMemo( + () => + new TimelineLayer({ + 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 + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + 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 + ] + ); + + useEffect(() => { + timelineLayerRef.current = timelineLayer; + }, [timelineLayer, timelineLayerRef]); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx new file mode 100644 index 000000000..212907cb8 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx @@ -0,0 +1,520 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {ReactElement, useMemo, useState} from 'react'; +import { + timeAxisFormatters, + type TimelineTrack, + type TimelineClip +} from '@deck.gl-community/timeline-layers'; +import {generateRandomTracks} from './demo-utils'; + +const PANEL_WIDTH = 320; +const INITIAL_CANVAS_WIDTH = (typeof window !== 'undefined' ? window.innerWidth : 1280) - PANEL_WIDTH; + +export type TimelineControlsState = { + tracks: TimelineTrack[]; + timelineStart: number; + timelineEnd: number; + currentTimeMs: number; + zoomLevel: number; + trackHeight: number; + trackSpacing: number; + timelineX: number; + timelineY: number; + timelineWidth: number; + viewportStartMs?: number; + viewportEndMs?: number; + labelFormatter: (timeMs: number) => string; + selectedClip: TimelineClip | null; + hoveredClip: TimelineClip | null; + selectedTrack: TimelineTrack | null; + hoveredTrack: TimelineTrack | null; + selectedLineWidth: number; + hoveredLineWidth: number; + showSubtrackSeparators: boolean; +}; + +export function useTimelineControls() { + const [trackCount, setTrackCount] = useState(4); + const [clipsPerTrack, setClipsPerTrack] = useState(3); + const [timelineStart] = useState(0); + const [timelineEnd, setTimelineEnd] = useState(60000); + const [currentTimeMs, setCurrentTimeMs] = useState(0); + const [zoomLevel, setZoomLevel] = useState(1); + + const [trackHeight, setTrackHeight] = useState(40); + const [trackSpacing, setTrackSpacing] = useState(8); + const [timelineX] = useState(150); + const [timelineY] = useState(80); + const [timelineWidth, setTimelineWidth] = useState(INITIAL_CANVAS_WIDTH - 200); + + const [viewportStartMs, setViewportStartMs] = useState(undefined); + const [viewportEndMs, setViewportEndMs] = useState(undefined); + + const [selectedClip, setSelectedClip] = useState(null); + const [hoveredClip, setHoveredClip] = useState(null); + const [selectedTrack, setSelectedTrack] = useState(null); + const [hoveredTrack, setHoveredTrack] = useState(null); + + const [labelFormatterType, setLabelFormatterType] = + useState('seconds'); + const labelFormatter = timeAxisFormatters[labelFormatterType]; + + const [selectedLineWidth, setSelectedLineWidth] = useState(3); + const [hoveredLineWidth, setHoveredLineWidth] = useState(2); + const [showSubtrackSeparators, setShowSubtrackSeparators] = useState(true); + + const tracks = useMemo( + () => generateRandomTracks(trackCount, clipsPerTrack, timelineStart, timelineEnd), + [trackCount, clipsPerTrack, timelineStart, timelineEnd] + ); + + const state: TimelineControlsState = useMemo( + () => ({ + tracks, + timelineStart, + timelineEnd, + currentTimeMs, + zoomLevel, + trackHeight, + trackSpacing, + timelineX, + timelineY, + timelineWidth, + viewportStartMs, + viewportEndMs, + labelFormatter, + selectedClip, + hoveredClip, + selectedTrack, + hoveredTrack, + selectedLineWidth, + hoveredLineWidth, + showSubtrackSeparators + }), + [ + tracks, + timelineStart, + timelineEnd, + currentTimeMs, + zoomLevel, + trackHeight, + trackSpacing, + timelineX, + timelineY, + timelineWidth, + viewportStartMs, + viewportEndMs, + labelFormatter, + selectedClip, + hoveredClip, + selectedTrack, + hoveredTrack, + selectedLineWidth, + hoveredLineWidth, + showSubtrackSeparators + ] + ); + + const controls = useMemo( + () => ({ + setTrackCount, + setClipsPerTrack, + setTimelineEnd, + setCurrentTimeMs, + setZoomLevel, + setTrackHeight, + setTrackSpacing, + setTimelineWidth, + setViewportStartMs, + setViewportEndMs, + setLabelFormatterType, + setSelectedClip, + setHoveredClip, + setSelectedTrack, + setHoveredTrack, + setSelectedLineWidth, + setHoveredLineWidth, + setShowSubtrackSeparators + }), + [] + ); + + return {state, controls, trackCount, clipsPerTrack, labelFormatterType}; +} + +export function TimelineControls({ + state, + controls, + trackCount, + clipsPerTrack, + labelFormatterType +}: { + state: TimelineControlsState; + controls: ReturnType['controls']; + trackCount: number; + clipsPerTrack: number; + labelFormatterType: string; +}): ReactElement { + return ( +
+
+

Timeline Controls

+
+ +
+
+ + + controls.setTimelineEnd(state.timelineStart + v)} + /> +
+ +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+ +
+ + + + {state.selectedClip && ( + controls.setSelectedClip(null)} /> + )} + + {state.selectedTrack && ( + controls.setSelectedTrack(null)} /> + )} +
+
+ ); +} + +function Section({title, children}: {title: string; children: React.ReactNode}): ReactElement { + return ( +
+

{title}

+ {children} +
+ ); +} + +function RangeField({ + label, + min, + max, + step = 1, + value, + onChange +}: { + label: string; + min: number; + max: number; + step?: number; + value: number; + onChange: (v: number) => void; +}): ReactElement { + return ( +
+ + onChange(Number(e.target.value))} + style={{width: '100%'}} + /> +
+ ); +} + +function SelectField({ + label, + value, + options, + onChange +}: { + label: string; + value: number; + options: number[]; + onChange: (v: number) => void; +}): ReactElement { + return ( +
+ + +
+ ); +} + +function AdvancedSettings({ + state, + controls +}: { + state: TimelineControlsState; + controls: ReturnType['controls']; +}): ReactElement { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+
setIsExpanded(!isExpanded)} + style={{ + cursor: 'pointer', + padding: '10px', + background: '#f5f5f5', + border: '1px solid #ddd', + borderRadius: '4px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: isExpanded ? '10px' : '0' + }} + > +

Advanced Settings

+ {isExpanded ? '▼' : '▶'} +
+ + {isExpanded && ( +
+ + + +
+ )} +
+ ); +} + +function ClipDetails({ + clip, + formatter, + onClear +}: { + clip: TimelineClip; + formatter: (timeMs: number) => string; + onClear: () => void; +}): ReactElement { + return ( +
+

+ Selected Clip +

+
+
+ Label: {clip.label} +
+
+ Start: {formatter(clip.startMs)} +
+
+ Duration: {formatter(clip.endMs - clip.startMs)} +
+ +
+
+ ); +} + +function TrackDetails({ + track, + onClear +}: { + track: TimelineTrack; + onClear: () => void; +}): ReactElement { + return ( +
+

+ Selected Track +

+
+
+ Name: {track.name} +
+
+ Clips: {track.clips.length} +
+ +
+
+ ); +} diff --git a/dev/timeline-layers/examples/timeline-layer/demo-utils.ts b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts new file mode 100644 index 000000000..012dcd143 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts @@ -0,0 +1,64 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimelineClip, TimelineTrack} from '@deck.gl-community/timeline-layers'; + +/** + * Generate random tracks with clips for demo purposes. + */ +export function generateRandomTracks( + trackCount: number, + clipsPerTrack: number, + timelineStart: number, + timelineEnd: number +): TimelineTrack[] { + const colors: [number, number, number, number][] = [ + [80, 120, 160, 220], + [120, 160, 180, 220], + [100, 140, 120, 220], + [140, 120, 140, 220], + [160, 140, 100, 220], + [120, 100, 140, 220], + [100, 120, 120, 220], + [140, 120, 100, 220] + ]; + + const tracks: TimelineTrack[] = []; + + for (let trackIdx = 0; trackIdx < trackCount; trackIdx++) { + const clips: TimelineClip[] = []; + + for (let clipIdx = 0; clipIdx < clipsPerTrack; clipIdx++) { + const duration = Math.random() * (timelineEnd - timelineStart) * 0.1; + let startMs: number; + + // 30% chance of an overlapping clip (tests collision detection) + if (clipIdx > 0 && Math.random() < 0.3) { + const prevClip = clips[clips.length - 1]; + const overlapPoint = + prevClip.startMs + (prevClip.endMs - prevClip.startMs) * (0.3 + Math.random() * 0.4); + startMs = overlapPoint; + } else { + startMs = Math.random() * (timelineEnd - timelineStart - duration) + timelineStart; + } + + clips.push({ + id: `track-${trackIdx}-clip-${clipIdx}`, + startMs, + endMs: startMs + duration, + label: `Clip ${clipIdx + 1}`, + color: colors[clipIdx % colors.length] + }); + } + + tracks.push({ + id: trackIdx, + name: `Track ${trackIdx + 1}`, + visible: true, + clips + }); + } + + return tracks; +} diff --git a/dev/timeline-layers/examples/timeline-layer/index.html b/dev/timeline-layers/examples/timeline-layer/index.html new file mode 100644 index 000000000..550c87aba --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/index.html @@ -0,0 +1,17 @@ + + + + + Timeline Layer Example + + + + + + diff --git a/dev/timeline-layers/examples/timeline-layer/index.tsx b/dev/timeline-layers/examples/timeline-layer/index.tsx new file mode 100644 index 000000000..b884876b3 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/index.tsx @@ -0,0 +1,10 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import App from './app'; + +const root = createRoot(document.body.appendChild(document.createElement('div'))); +root.render(); diff --git a/dev/timeline-layers/examples/timeline-layer/package.json b/dev/timeline-layers/examples/timeline-layer/package.json new file mode 100644 index 000000000..0307a6900 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/package.json @@ -0,0 +1,19 @@ +{ + "license": "MIT", + "scripts": { + "start": "vite --open", + "start-local": "vite --config ../../vite.config.local.mjs" + }, + "dependencies": { + "@deck.gl-community/timeline-layers": "workspace:*", + "@deck.gl/core": "~9.2.1", + "@deck.gl/layers": "~9.2.1", + "@deck.gl/react": "~9.2.1", + "@luma.gl/core": "~9.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "vite": "7.1.1" + } +} diff --git a/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts new file mode 100644 index 000000000..82e866dc2 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts @@ -0,0 +1,467 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {useState, useRef, useEffect, useCallback, useMemo} from 'react'; +import type {TimelineLayer, TimelineClipInfo, TimelineTrackInfo} from '@deck.gl-community/timeline-layers'; +import {positionToTime} from '@deck.gl-community/timeline-layers'; +import type {TimelineControlsState} from './demo-controls'; + +const DRAG_THRESHOLD = 5; + +interface TimelineInteractionState { + isDraggingScrubber: boolean; + isPanning: boolean; + panStartX: number; + panStartViewport: {start: number; end: number} | null; + mouseDownPos: {x: number; y: number} | null; + hoveredObjectId: string | null; +} + +interface TimelineRefs { + timelineLayerRef: React.RefObject; + containerRef: React.RefObject; +} + +interface TimelineControls { + setSelectedClip: (clip: unknown) => void; + setHoveredClip: (clip: unknown) => void; + setSelectedTrack: (track: unknown) => void; + setHoveredTrack: (track: unknown) => void; + setCurrentTimeMs: (timeMs: number) => void; + setViewportStartMs: (startMs: number | undefined) => void; + setViewportEndMs: (endMs: number | undefined) => void; + setZoomLevel: (zoomLevel: number) => void; +} + +/** Hook for managing timeline interaction state */ +export function useTimelineInteractionState(): { + state: TimelineInteractionState; + setState: { + setIsDraggingScrubber: (value: boolean) => void; + setIsPanning: (value: boolean) => void; + setPanStartX: (value: number) => void; + setPanStartViewport: (value: {start: number; end: number} | null) => void; + setMouseDownPos: (value: {x: number; y: number} | null) => void; + setHoveredObjectId: (value: string | null) => void; + }; +} { + const [isDraggingScrubber, setIsDraggingScrubber] = useState(false); + const [isPanning, setIsPanning] = useState(false); + const [panStartX, setPanStartX] = useState(0); + const [panStartViewport, setPanStartViewport] = useState<{start: number; end: number} | null>( + null + ); + const [mouseDownPos, setMouseDownPos] = useState<{x: number; y: number} | null>(null); + const [hoveredObjectId, setHoveredObjectId] = useState(null); + + const state = useMemo( + () => ({isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId}), + [isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId] + ); + + const setState = useMemo( + () => ({ + setIsDraggingScrubber, + setIsPanning, + setPanStartX, + setPanStartViewport, + setMouseDownPos, + setHoveredObjectId + }), + [] + ); + + return {state, setState}; +} + +/** Hook for creating timeline refs */ +export function useTimelineRefs(): TimelineRefs { + const timelineLayerRef = useRef(null); + const containerRef = useRef(null); + return useMemo(() => ({timelineLayerRef, containerRef}), []); +} + +/** Hook for global mouse-up cleanup */ +export function useGlobalMouseUpCleanup( + setIsDraggingScrubber: (value: boolean) => void, + setIsPanning: (value: boolean) => void, + setPanStartViewport: (value: {start: number; end: number} | null) => void +): void { + useEffect(() => { + const handleGlobalMouseUp = () => { + setIsDraggingScrubber(false); + setIsPanning(false); + setPanStartViewport(null); + }; + document.addEventListener('mouseup', handleGlobalMouseUp); + return () => document.removeEventListener('mouseup', handleGlobalMouseUp); + }, [setIsDraggingScrubber, setIsPanning, setPanStartViewport]); +} + +/** Hook for mouse-wheel zoom */ +export function useWheelZoom( + containerRef: React.RefObject, + timelineLayerRef: React.RefObject, + zoomLevel: number +): void { + useEffect(() => { + const container = containerRef.current; + if (!container) return undefined; + + const handleWheel = (e: WheelEvent) => { + if (!timelineLayerRef.current) return; + e.preventDefault(); + const rect = container.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const zoomFactor = e.deltaY > 0 ? 0.8 : 1.2; + timelineLayerRef.current.zoomToPoint(zoomFactor, mouseX, zoomLevel); + }; + + container.addEventListener('wheel', handleWheel, {passive: false}); + return () => container.removeEventListener('wheel', handleWheel); + }, [containerRef, timelineLayerRef, zoomLevel]); +} + +/** Hook for timeline layer event callbacks */ +export function useTimelineCallbacks(controls: TimelineControls): { + handleClipClick: (info: TimelineClipInfo) => void; + handleClipHover: (info: TimelineClipInfo | null) => void; + handleTrackClick: (info: TimelineTrackInfo) => void; + handleTrackHover: (info: TimelineTrackInfo | null) => void; + handleScrubberDrag: (timeMs: number) => void; + handleTimelineClick: (timeMs: number) => void; + handleViewportChange: (startMs: number, endMs: number) => void; + handleZoomChange: (zoomLevel: number) => void; +} { + const { + setSelectedClip, + setHoveredClip, + setSelectedTrack, + setHoveredTrack, + setCurrentTimeMs, + setViewportStartMs, + setViewportEndMs, + setZoomLevel + } = controls; + + const handleClipClick = useCallback((info: TimelineClipInfo) => setSelectedClip(info.clip), [setSelectedClip]); + const handleClipHover = useCallback((info: TimelineClipInfo | null) => setHoveredClip(info?.clip ?? null), [setHoveredClip]); + const handleTrackClick = useCallback((info: TimelineTrackInfo) => setSelectedTrack(info.track), [setSelectedTrack]); + const handleTrackHover = useCallback((info: TimelineTrackInfo | null) => setHoveredTrack(info?.track ?? null), [setHoveredTrack]); + const handleScrubberDrag = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]); + const handleTimelineClick = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]); + + const handleViewportChange = useCallback( + (startMs: number, endMs: number) => { + setViewportStartMs(startMs); + setViewportEndMs(endMs); + }, + [setViewportStartMs, setViewportEndMs] + ); + + const handleZoomChange = useCallback((zoomLevel: number) => setZoomLevel(zoomLevel), [setZoomLevel]); + + return useMemo( + () => ({ + handleClipClick, + handleClipHover, + handleTrackClick, + handleTrackHover, + handleScrubberDrag, + handleTimelineClick, + handleViewportChange, + handleZoomChange + }), + [ + handleClipClick, + handleClipHover, + handleTrackClick, + handleTrackHover, + handleScrubberDrag, + handleTimelineClick, + handleViewportChange, + handleZoomChange + ] + ); +} + +/** Hook for container mouse event handlers (scrubber drag + pan) */ +export function useContainerHandlers( + interactionState: TimelineInteractionState, + setState: ReturnType['setState'], + _timelineLayerRef: React.RefObject, + controls: TimelineControls, + state: TimelineControlsState +): { + handleContainerMouseDown: (e: React.MouseEvent) => void; + handleContainerMouseMove: (e: React.MouseEvent) => void; + handleContainerMouseUp: () => void; +} { + const {setCurrentTimeMs, setViewportStartMs, setViewportEndMs} = controls; + const {setIsDraggingScrubber, setMouseDownPos, setIsPanning, setPanStartX, setPanStartViewport} = + setState; + + const handleContainerMouseDown = useCallback( + (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName !== 'CANVAS') return; + + if (interactionState.hoveredObjectId === 'scrubber-handle') { + setIsDraggingScrubber(true); + return; + } + + if (state.zoomLevel > 1.0) { + const rect = e.currentTarget.getBoundingClientRect(); + setMouseDownPos({x: e.clientX - rect.left, y: e.clientY - rect.top}); + } + }, + [interactionState.hoveredObjectId, state.zoomLevel, setIsDraggingScrubber, setMouseDownPos] + ); + + const handleContainerMouseMove = useCallback( + (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Scrubber dragging + if (interactionState.isDraggingScrubber) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + mouseX, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + return; + } + + // Pan start detection + if ( + interactionState.mouseDownPos && + !interactionState.isPanning && + !interactionState.isDraggingScrubber + ) { + const dx = mouseX - interactionState.mouseDownPos.x; + const dy = mouseY - interactionState.mouseDownPos.y; + if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD && state.zoomLevel > 1.0) { + setIsPanning(true); + setPanStartX(interactionState.mouseDownPos.x); + setPanStartViewport({ + start: state.viewportStartMs ?? state.timelineStart, + end: state.viewportEndMs ?? state.timelineEnd + }); + setMouseDownPos(null); + } + } + + // Pan update + if (interactionState.isPanning && interactionState.panStartViewport) { + e.preventDefault(); + const deltaX = mouseX - interactionState.panStartX; + const currentRange = interactionState.panStartViewport.end - interactionState.panStartViewport.start; + const timeDelta = -(deltaX / state.timelineWidth) * currentRange; + + let newStart = interactionState.panStartViewport.start + timeDelta; + let newEnd = interactionState.panStartViewport.end + timeDelta; + + if (newStart < state.timelineStart) { + newStart = state.timelineStart; + newEnd = state.timelineStart + currentRange; + } else if (newEnd > state.timelineEnd) { + newEnd = state.timelineEnd; + newStart = state.timelineEnd - currentRange; + } + + setViewportStartMs(newStart); + setViewportEndMs(newEnd); + } + }, + [ + interactionState, + state.zoomLevel, + state.timelineStart, + state.timelineEnd, + state.timelineWidth, + state.timelineX, + state.viewportStartMs, + state.viewportEndMs, + setCurrentTimeMs, + setIsPanning, + setPanStartX, + setPanStartViewport, + setMouseDownPos, + setViewportStartMs, + setViewportEndMs + ] + ); + + const handleContainerMouseUp = useCallback(() => { + setMouseDownPos(null); + if (interactionState.isDraggingScrubber) setIsDraggingScrubber(false); + if (interactionState.isPanning) { + setIsPanning(false); + setPanStartViewport(null); + } + }, [ + interactionState.isDraggingScrubber, + interactionState.isPanning, + setMouseDownPos, + setIsDraggingScrubber, + setIsPanning, + setPanStartViewport + ]); + + return useMemo( + () => ({handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp}), + [handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp] + ); +} + +/** Hook for DeckGL hover/click handlers */ +export function useDeckGLHandlers( + setState: ReturnType['setState'], + _timelineLayerRef: React.RefObject, + controls: TimelineControls, + state: TimelineControlsState +): { + handleDeckGLHover: (info: any) => void; + handleDeckGLClick: (info: any) => void; +} { + const {setCurrentTimeMs, setSelectedClip, setSelectedTrack, setHoveredClip, setHoveredTrack} = + controls; + const {setHoveredObjectId, setIsDraggingScrubber, setMouseDownPos} = setState; + + const handleDeckGLHover = useCallback( + (info: any) => { + if (!info.object) { + setHoveredObjectId(null); + setHoveredClip(null); + setHoveredTrack(null); + return; + } + + const objectId = String(info.object.id); + setHoveredObjectId(objectId); + + if (objectId.startsWith('track-') && objectId.includes('-clip-')) { + if (info.object.clip) setHoveredClip(info.object.clip); + setHoveredTrack(null); + } else if (objectId.startsWith('track-bg-')) { + setHoveredClip(null); + if (info.object.track) setHoveredTrack(info.object.track); + } else { + setHoveredClip(null); + setHoveredTrack(null); + } + }, + [setHoveredObjectId, setHoveredClip, setHoveredTrack] + ); + + const handleDeckGLClick = useCallback( + (info: any) => { + if (!info.object) { + if (info.coordinate) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + info.coordinate[0] || 0, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + } + setSelectedClip(null); + setSelectedTrack(null); + return; + } + + const objectId = String(info.object.id); + + if (objectId.startsWith('track-') && objectId.includes('-clip-')) { + if (info.object.clip) setSelectedClip(info.object.clip); + setSelectedTrack(null); + } else if (objectId === 'scrubber-handle') { + setIsDraggingScrubber(true); + setMouseDownPos(null); + setSelectedClip(null); + setSelectedTrack(null); + } else if (objectId.startsWith('track-bg-')) { + if (info.coordinate) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + info.coordinate[0] || 0, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + } + setSelectedClip(null); + if (info.object.track) setSelectedTrack(info.object.track); + } else { + setSelectedClip(null); + setSelectedTrack(null); + } + }, + [ + state.timelineX, + state.timelineWidth, + state.viewportStartMs, + state.viewportEndMs, + state.timelineStart, + state.timelineEnd, + setCurrentTimeMs, + setSelectedClip, + setSelectedTrack, + setIsDraggingScrubber, + setMouseDownPos + ] + ); + + return useMemo( + () => ({handleDeckGLHover, handleDeckGLClick}), + [handleDeckGLHover, handleDeckGLClick] + ); +} + +/** Hook for cursor style */ +export function useCursorGetter( + interactionState: TimelineInteractionState, + zoomLevel: number +): ({isHovering}: {isHovering: boolean}) => string { + return useCallback( + ({isHovering}: {isHovering: boolean}) => { + if (interactionState.isDraggingScrubber || interactionState.isPanning) return 'grabbing'; + if (interactionState.hoveredObjectId === 'scrubber-handle') return 'grab'; + if (isHovering) return 'pointer'; + if (zoomLevel > 1.0) return 'grab'; + return 'default'; + }, + [ + interactionState.isDraggingScrubber, + interactionState.isPanning, + interactionState.hoveredObjectId, + zoomLevel + ] + ); +} + +/** Hook to update timeline width on window resize */ +export function useTimelineResize(setTimelineWidth: (width: number) => void): void { + useEffect(() => { + const handleResize = () => { + setTimelineWidth(window.innerWidth - 320 - 200); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [setTimelineWidth]); +} diff --git a/dev/timeline-layers/examples/timeline-layer/tsconfig.json b/dev/timeline-layers/examples/timeline-layer/tsconfig.json new file mode 100644 index 000000000..61f3747df --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./*.tsx", "./*.ts"] +} diff --git a/dev/timeline-layers/src/index.ts b/dev/timeline-layers/src/index.ts index d65c9b9fe..e6a2f9d4a 100644 --- a/dev/timeline-layers/src/index.ts +++ b/dev/timeline-layers/src/index.ts @@ -2,12 +2,53 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors +// HORIZON GRAPH LAYERS export type {HorizonGraphLayerProps} from './layers/horizon-graph-layer/horizon-graph-layer'; export {HorizonGraphLayer} from './layers/horizon-graph-layer/horizon-graph-layer'; export type {MultiHorizonGraphLayerProps} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; export {MultiHorizonGraphLayer} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; + +// AXIS LAYERS export type {TimeAxisLayerProps} from './layers/time-axis-layer'; export {TimeAxisLayer} from './layers/time-axis-layer'; export type {VerticalGridLayerProps} from './layers/vertical-grid-layer'; export {VerticalGridLayer} from './layers/vertical-grid-layer'; export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils'; + +// TIMELINE LAYER +export type {TimelineLayerProps} from './layers/timeline-layer/timeline-layer'; +export {TimelineLayer} from './layers/timeline-layer/timeline-layer'; + +export type { + TimelineClip, + TimelineTrack, + TrackWithSubtracks, + TrackPosition, + TrackBackgroundData, + TrackLabelData, + ClipPolygonData, + ClipLabelData, + SeparatorLineData, + AxisLineData, + AxisLabelData, + ScrubberLineData, + ScrubberHandleData, + ScrubberLabelData, + TimelineTick, + TimelineViewport, + TimelineClipInfo, + TimelineTrackInfo, + TimelineScrubberInfo, + TimelineViewportInfo, + TimeAxisLabelFormatter +} from './layers/timeline-layer/timeline-types'; + +export type {SelectionStyle, TimelineLayout} from './layers/timeline-layer/timeline-layout'; +export {DEFAULT_TIMELINE_LAYOUT} from './layers/timeline-layer/timeline-layout'; + +export { + positionToTime, + timeToPosition, + timeAxisFormatters, + type GenerateTicksOptions +} from './layers/timeline-layer/timeline-utils'; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts new file mode 100644 index 000000000..5bcbd5151 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts @@ -0,0 +1,54 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimelineClip} from './timeline-types'; + +export type ClipWithSubtrack = TimelineClip & { + subtrackIndex: number; +}; + +/** + * Detects overlapping clips and assigns them to subtracks. + * Uses a greedy algorithm to minimize the number of subtracks needed. + */ +export function assignClipsToSubtracks(clips: TimelineClip[]): ClipWithSubtrack[] { + if (clips.length === 0) return []; + + // Sort clips by start time + const sortedClips = [...clips].sort((a, b) => a.startMs - b.startMs); + + // Track the end time of the last clip in each subtrack + const subtrackEndTimes: number[] = []; + const result: ClipWithSubtrack[] = []; + + for (const clip of sortedClips) { + let assignedSubtrack = -1; + + for (let i = 0; i < subtrackEndTimes.length; i++) { + if (clip.startMs >= subtrackEndTimes[i]) { + assignedSubtrack = i; + subtrackEndTimes[i] = clip.endMs; + break; + } + } + + if (assignedSubtrack === -1) { + assignedSubtrack = subtrackEndTimes.length; + subtrackEndTimes.push(clip.endMs); + } + + result.push({...clip, subtrackIndex: assignedSubtrack}); + } + + return result; +} + +/** + * Calculate the number of subtracks needed for a set of clips. + */ +export function calculateSubtrackCount(clips: TimelineClip[]): number { + if (clips.length === 0) return 1; + const clipsWithSubtracks = assignClipsToSubtracks(clips); + return Math.max(...clipsWithSubtracks.map((c) => c.subtrackIndex)) + 1; +} diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts new file mode 100644 index 000000000..8b94d54a1 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts @@ -0,0 +1,867 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {CompositeLayer, COORDINATE_SYSTEM, type PickingInfo, type Layer} from '@deck.gl/core'; +import {SolidPolygonLayer, LineLayer, TextLayer} from '@deck.gl/layers'; +import type {CompositeLayerProps} from '@deck.gl/core'; +import type {LineLayerProps, SolidPolygonLayerProps, TextLayerProps} from '@deck.gl/layers'; + +import type { + TimelineClipInfo, + TimelineTrackInfo, + TimelineTrack, + TrackWithSubtracks, + TrackPosition, + TrackBackgroundData, + TrackLabelData, + ClipPolygonData, + ClipLabelData, + SeparatorLineData, + AxisLineData, + AxisLabelData, + ScrubberLineData, + ScrubberHandleData, + ScrubberLabelData, + TimeAxisLabelFormatter +} from './timeline-types'; + +import type {SelectionStyle} from './timeline-layout'; + +import { + timeAxisFormatters, + generateTimelineTicks, + timeToPosition, + positionToTime +} from './timeline-utils'; +import {assignClipsToSubtracks, calculateSubtrackCount} from './timeline-collision'; + +function lightenColor( + color: [number, number, number, number], + amount: number = 30 +): [number, number, number, number] { + return [ + Math.min(255, color[0] + amount), + Math.min(255, color[1] + amount), + Math.min(255, color[2] + amount), + color[3] + ]; +} + +const defaultProps = { + x: 150, + y: 100, + width: 800, + trackHeight: 40, + trackSpacing: 10, + currentTimeMs: 0, + showScrubber: true, + showClipLabels: true, + showTrackLabels: true, + showAxis: true, + showSubtrackSeparators: true, + timeFormatter: timeAxisFormatters.seconds, + selectionStyle: { + 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: 3, + hoveredLineWidth: 2 + } +}; + +export type TimelineLayerProps = CompositeLayerProps & { + /** Array of timeline tracks, each containing clips */ + data: TimelineTrack[]; + /** Start of the full timeline range in milliseconds */ + timelineStart: number; + /** End of the full timeline range in milliseconds */ + timelineEnd: number; + + /** X offset of the timeline in canvas coordinates */ + x?: number; + /** Y offset of the timeline in canvas coordinates */ + y?: number; + /** Width of the timeline in canvas coordinates */ + width?: number; + /** Height of each track row in canvas coordinates */ + trackHeight?: number; + /** Spacing between tracks in canvas coordinates */ + trackSpacing?: number; + + /** Current playhead time in milliseconds */ + currentTimeMs?: number; + /** Optional zoomed viewport range */ + viewport?: {startMs?: number; endMs?: number}; + /** Formatter for time axis labels */ + timeFormatter?: TimeAxisLabelFormatter; + + /** ID of the currently selected clip */ + selectedClipId?: string | number | null; + /** ID of the currently hovered clip */ + hoveredClipId?: string | number | null; + /** ID of the currently selected track */ + selectedTrackId?: string | number | null; + /** ID of the currently hovered track */ + hoveredTrackId?: string | number | null; + /** Colors and line widths for selected/hovered states */ + selectionStyle?: SelectionStyle; + + /** Whether to show the playhead scrubber */ + showScrubber?: boolean; + /** Whether to show labels on clips */ + showClipLabels?: boolean; + /** Whether to show labels on tracks */ + showTrackLabels?: boolean; + /** Whether to show the time axis */ + showAxis?: boolean; + /** Whether to show separators between collision subtracks */ + showSubtrackSeparators?: boolean; + + /** Override props for the clip polygon sub-layer */ + clipProps?: Partial>; + /** Override props for the track background sub-layer */ + trackProps?: Partial>; + /** Override props for the track label sub-layer */ + trackLabelProps?: Partial>; + /** Override props for the clip label sub-layer */ + clipLabelProps?: Partial>; + /** Override props for the axis line sub-layer */ + axisLineProps?: Partial>; + /** Override props for the axis label sub-layer */ + axisLabelProps?: Partial>; + /** Override props for the scrubber line sub-layer */ + scrubberLineProps?: Partial>; + + /** Callback when a clip is clicked */ + onClipClick?: (info: TimelineClipInfo, event: PickingInfo) => void; + /** Callback when a clip is hovered */ + onClipHover?: (info: TimelineClipInfo | null, event: PickingInfo) => void; + /** Callback when a track is clicked */ + onTrackClick?: (info: TimelineTrackInfo, event: PickingInfo) => void; + /** Callback when a track is hovered */ + onTrackHover?: (info: TimelineTrackInfo | null, event: PickingInfo) => void; + /** Callback when the scrubber handle is hovered */ + onScrubberHover?: (isHovering: boolean, event: PickingInfo) => void; + /** Callback when a scrubber drag begins */ + onScrubberDragStart?: (event: PickingInfo) => void; + /** Callback when the scrubber is dragged to a new time */ + onScrubberDrag?: (timeMs: number, event: PickingInfo) => void; + /** Callback when the timeline background is clicked */ + onTimelineClick?: (timeMs: number, event: PickingInfo) => void; + + /** Callback when the current time changes */ + onCurrentTimeChange?: (timeMs: number) => void; + /** Callback when the viewport (zoom/pan) changes */ + onViewportChange?: (startMs: number, endMs: number) => void; + /** Callback when the zoom level changes */ + onZoomChange?: (zoomLevel: number) => void; +}; + +export class TimelineLayer extends CompositeLayer { + static layerName = 'TimelineLayer'; + static defaultProps = defaultProps; + + /** Convert a canvas X coordinate to a time in milliseconds */ + getTimeFromPosition(x: number): number { + const {timelineStart, timelineEnd, viewport, x: timelineX = 150, width = 800} = this.props; + const effectiveStartMs = viewport?.startMs ?? timelineStart; + const effectiveEndMs = viewport?.endMs ?? timelineEnd; + return positionToTime(x, timelineX, width, effectiveStartMs, effectiveEndMs); + } + + /** Zoom the timeline viewport around a canvas X coordinate */ + zoomToPoint(zoomFactor: number, mouseX: number, currentZoomLevel: number): void { + const { + timelineStart, + timelineEnd, + viewport, + x: timelineX = 150, + width = 800, + onViewportChange, + onZoomChange + } = this.props; + + const newZoomLevel = Math.max(1.0, Math.min(100, currentZoomLevel * zoomFactor)); + const mouseRatio = Math.max(0, Math.min(1, (mouseX - timelineX) / width)); + const currentStartMs = viewport?.startMs ?? timelineStart; + const currentEndMs = viewport?.endMs ?? timelineEnd; + const mouseTimeMs = currentStartMs + mouseRatio * (currentEndMs - currentStartMs); + + const fullTimeRange = timelineEnd - timelineStart; + const newViewportRange = fullTimeRange / newZoomLevel; + + let newStartMs = mouseTimeMs - mouseRatio * newViewportRange; + let newEndMs = newStartMs + newViewportRange; + + if (newStartMs < timelineStart) { + newStartMs = timelineStart; + newEndMs = timelineStart + newViewportRange; + } else if (newEndMs > timelineEnd) { + newEndMs = timelineEnd; + newStartMs = timelineEnd - newViewportRange; + } + + if (newZoomLevel > 1.0) { + onViewportChange?.(newStartMs, newEndMs); + } else { + onViewportChange?.(timelineStart, timelineEnd); + } + + onZoomChange?.(newZoomLevel); + } + + // ===== LAYOUT CALCULATION ===== + + private _calculateTrackPositions( + tracksWithSubtracks: TrackWithSubtracks[], + y: number, + trackHeight: number, + trackSpacing: number + ): {trackPositions: TrackPosition[]; totalTimelineHeight: number} { + let currentY = y; + const trackPositions: TrackPosition[] = []; + const subtrackSpacing = 2; + + for (const {subtrackCount} of tracksWithSubtracks) { + const trackTotalHeight = subtrackCount * trackHeight + (subtrackCount - 1) * subtrackSpacing; + trackPositions.push({y: currentY, height: trackTotalHeight, subtrackCount}); + currentY += trackTotalHeight + trackSpacing; + } + + const totalTimelineHeight = currentY - y - trackSpacing; + return {trackPositions, totalTimelineHeight}; + } + + // ===== DATA GENERATION ===== + + private _generateTrackBackgrounds( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): TrackBackgroundData[] { + const { + x = 150, + width = 800, + selectedTrackId, + hoveredTrackId, + selectionStyle = defaultProps.selectionStyle + } = this.props; + + return tracksWithSubtracks.map(({track, trackIndex}, i) => { + const {y: trackY, height} = trackPositions[i]; + const isSelected = selectedTrackId === track.id; + const isHovered = hoveredTrackId === track.id; + + let color: [number, number, number, number] = [60, 60, 60, 255]; + if (isSelected) { + color = selectionStyle.selectedTrackColor!; + } else if (isHovered) { + color = lightenColor([60, 60, 60, 255], 20); + } + + return { + id: `track-bg-${track.id}`, + track, + trackIndex, + polygon: [ + [x, trackY], + [x + width, trackY], + [x + width, trackY + height], + [x, trackY + height] + ], + color + }; + }); + } + + private _generateTrackLabels( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): TrackLabelData[] { + const {x = 150, showTrackLabels = true} = this.props; + if (!showTrackLabels) return []; + + return tracksWithSubtracks.map(({track}, i) => { + const label = track.name || `Track ${track.id}`; + const {y: trackY, height} = trackPositions[i]; + return {text: label, position: [x - 10, trackY + height / 2, 0]}; + }); + } + + private _buildClipPolygon( + clip: ReturnType[number], + opts: { + track: TimelineTrack; + trackIndex: number; + clipIndex: number; + subtrackHeight: number; + baseTrackY: number; + x: number; + width: number; + effectiveStartMs: number; + effectiveEndMs: number; + selectedClipId: string | number | null | undefined; + hoveredClipId: string | number | null | undefined; + selectionStyle: SelectionStyle; + } + ): ClipPolygonData | null { + const {id: clipId, startMs, endMs, subtrackIndex = 0} = clip; + const { + track, + trackIndex, + clipIndex, + subtrackHeight, + baseTrackY, + x, + width, + effectiveStartMs, + effectiveEndMs, + selectedClipId, + hoveredClipId, + selectionStyle + } = opts; + + if (endMs <= effectiveStartMs || startMs >= effectiveEndMs) return null; + + const clipPadding = 2; + const subtrackSpacing = 2; + const clipTrackY = baseTrackY + subtrackIndex * (subtrackHeight + subtrackSpacing); + const clipStartRatio = (startMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs); + const clipEndRatio = (endMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs); + const clipStartX = x + Math.max(0, clipStartRatio) * width; + const clipEndX = x + Math.min(1, clipEndRatio) * width; + + const baseColor = clip.color || ([80, 120, 160, 220] as [number, number, number, number]); + const isSelected = selectedClipId !== null && String(selectedClipId) === String(clipId); + const isHovered = hoveredClipId !== null && String(hoveredClipId) === String(clipId); + + let color = baseColor; + if (isSelected) { + color = selectionStyle.selectedClipColor!; + } else if (isHovered) { + color = lightenColor(baseColor, 40); + } + + return { + id: clipId, + clip, + track, + clipIndex, + trackIndex, + subtrackIndex, + polygon: [ + [clipStartX, clipTrackY + clipPadding], + [clipEndX, clipTrackY + clipPadding], + [clipEndX, clipTrackY + subtrackHeight - clipPadding], + [clipStartX, clipTrackY + subtrackHeight - clipPadding] + ], + color, + label: clip.label || '', + labelPosition: [clipStartX + (clipEndX - clipStartX) / 2, clipTrackY + subtrackHeight / 2, 0] + }; + } + + private _generateClipPolygons( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[], + effectiveStartMs: number, + effectiveEndMs: number + ): ClipPolygonData[] { + const { + x = 150, + width = 800, + selectedClipId, + hoveredClipId, + selectionStyle = defaultProps.selectionStyle + } = this.props; + + const subtrackSpacing = 2; + const clipPolygons: ClipPolygonData[] = []; + + for (let i = 0; i < tracksWithSubtracks.length; i++) { + const {track, trackIndex, clips, subtrackCount} = tracksWithSubtracks[i]; + const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i]; + const subtrackHeight = + (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount; + + for (let clipIndex = 0; clipIndex < clips.length; clipIndex++) { + const polygon = this._buildClipPolygon(clips[clipIndex], { + track, + trackIndex, + clipIndex, + subtrackHeight, + baseTrackY, + x, + width, + effectiveStartMs, + effectiveEndMs, + selectedClipId, + hoveredClipId, + selectionStyle + }); + if (polygon) { + clipPolygons.push(polygon); + } + } + } + + return clipPolygons; + } + + private _generateSubtrackSeparators( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): SeparatorLineData[] { + const {x = 150, width = 800, showSubtrackSeparators = true} = this.props; + if (!showSubtrackSeparators) return []; + + const subtrackSpacing = 2; + const separatorLines: SeparatorLineData[] = []; + + for (let i = 0; i < tracksWithSubtracks.length; i++) { + const {subtrackCount} = tracksWithSubtracks[i]; + if (subtrackCount <= 1) { + // No separators needed for single-subtrack rows + } else { + const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i]; + const subtrackHeight = + (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount; + + for (let j = 1; j < subtrackCount; j++) { + const separatorY = + baseTrackY + j * (subtrackHeight + subtrackSpacing) - subtrackSpacing / 2; + separatorLines.push({ + sourcePosition: [x, separatorY], + targetPosition: [x + width, separatorY] + }); + } + } + } + + return separatorLines; + } + + private _generateAxis( + totalTimelineHeight: number, + effectiveStartMs: number, + effectiveEndMs: number + ): {axisLines: AxisLineData[]; axisLabels: AxisLabelData[]} { + const { + x = 150, + y = 100, + width = 800, + showAxis = true, + timeFormatter = timeAxisFormatters.seconds + } = this.props; + + const axisLines: AxisLineData[] = []; + const axisLabels: AxisLabelData[] = []; + + if (!showAxis) return {axisLines, axisLabels}; + + const axisHeight = 30; + const tickCount = Math.max(4, Math.min(10, Math.floor(width / 80))); + + const timelineTicks = generateTimelineTicks({ + startMs: effectiveStartMs, + endMs: effectiveEndMs, + timelineX: x, + timelineWidth: width, + tickCount, + formatter: timeFormatter + }); + + const axisY = y + totalTimelineHeight + axisHeight; + + axisLines.push({sourcePosition: [x, axisY], targetPosition: [x + width, axisY]}); + + for (const tick of timelineTicks) { + axisLines.push({ + sourcePosition: [tick.position, axisY - 5], + targetPosition: [tick.position, axisY + 5] + }); + axisLabels.push({text: tick.label, position: [tick.position, axisY + 15, 0]}); + } + + return {axisLines, axisLabels}; + } + + private _generateScrubber( + totalTimelineHeight: number, + effectiveStartMs: number, + effectiveEndMs: number + ): { + scrubberLine: ScrubberLineData[]; + scrubberHandle: ScrubberHandleData[]; + scrubberLabel: ScrubberLabelData[]; + } { + const { + x = 150, + y = 100, + width = 800, + showScrubber = true, + currentTimeMs = 0, + timeFormatter = timeAxisFormatters.seconds + } = this.props; + + if (!showScrubber) return {scrubberLine: [], scrubberHandle: [], scrubberLabel: []}; + + const scrubberPosition = timeToPosition( + currentTimeMs, + x, + width, + effectiveStartMs, + effectiveEndMs + ); + + const scrubberLine: ScrubberLineData[] = [ + { + sourcePosition: [scrubberPosition, y - 30], + targetPosition: [scrubberPosition, y + totalTimelineHeight + 30] + } + ]; + + const scrubberHandle: ScrubberHandleData[] = [ + { + id: 'scrubber-handle', + polygon: [ + [scrubberPosition - 8, y - 35], + [scrubberPosition + 8, y - 35], + [scrubberPosition + 8, y - 20], + [scrubberPosition - 8, y - 20] + ], + color: [255, 100, 100, 255] + } + ]; + + const scrubberLabel: ScrubberLabelData[] = [ + {text: timeFormatter(currentTimeMs), position: [scrubberPosition, y - 40, 0]} + ]; + + return {scrubberLine, scrubberHandle, scrubberLabel}; + } + + // ===== LAYER CREATION ===== + + private _createTrackLayers( + trackBackgrounds: TrackBackgroundData[], + trackLabels: TrackLabelData[] + ): Layer[] { + const {trackProps, showTrackLabels = true, onTrackClick, onTrackHover} = this.props; + const layers: Layer[] = []; + + layers.push( + new SolidPolygonLayer( + this.getSubLayerProps({ + ...trackProps, + id: 'tracks', + data: trackBackgrounds, + getPolygon: (d: TrackBackgroundData) => d.polygon, + getFillColor: (d: TrackBackgroundData) => d.color, + stroked: true, + getLineColor: [100, 100, 100, 255], + getLineWidth: 1, + pickable: Boolean(onTrackClick) || Boolean(onTrackHover), + onClick: (info: PickingInfo) => { + if (info.object && onTrackClick) { + const obj = info.object as TrackBackgroundData; + onTrackClick({track: obj.track, index: obj.trackIndex}, info); + } + }, + onHover: (info: PickingInfo) => { + if (onTrackHover) { + const obj = info.object as TrackBackgroundData | undefined; + onTrackHover(obj ? {track: obj.track, index: obj.trackIndex} : null, info); + } + } + }) + ) + ); + + if (showTrackLabels) { + layers.push( + new TextLayer({ + id: `${this.props.id}-track-labels`, + data: trackLabels, + getText: (d: TrackLabelData) => d.text, + getPosition: (d: TrackLabelData) => d.position, + getSize: 12, + getColor: [60, 60, 60, 255], + getTextAnchor: 'end', + getAlignmentBaseline: 'center', + fontFamily: 'Arial, sans-serif', + fontWeight: 'bold', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }) + ); + } + + return layers; + } + + private _createClipLayers( + clipPolygons: ClipPolygonData[], + clipLabelsData: ClipLabelData[] + ): Layer[] { + const { + clipProps, + showClipLabels = true, + selectedClipId, + selectionStyle = defaultProps.selectionStyle, + onClipClick, + onClipHover + } = this.props; + const layers: Layer[] = []; + + layers.push( + new SolidPolygonLayer( + this.getSubLayerProps({ + ...clipProps, + id: 'clips', + data: clipPolygons, + getPolygon: (d: ClipPolygonData) => d.polygon, + getFillColor: (d: ClipPolygonData) => d.color, + stroked: true, + getLineColor: [255, 255, 255, 200], + getLineWidth: (d: ClipPolygonData) => { + const isSelected = selectedClipId !== null && String(selectedClipId) === String(d.id); + return isSelected ? selectionStyle.selectedLineWidth || 3 : 2; + }, + pickable: Boolean(onClipClick) || Boolean(onClipHover), + autoHighlight: true, + onClick: (info: PickingInfo) => { + if (info.object && onClipClick) { + const obj = info.object as ClipPolygonData; + onClipClick( + { + clip: obj.clip, + track: obj.track, + clipIndex: obj.clipIndex, + trackIndex: obj.trackIndex, + subtrackIndex: obj.subtrackIndex + }, + info + ); + } + }, + onHover: (info: PickingInfo) => { + if (onClipHover) { + const obj = info.object as ClipPolygonData | undefined; + onClipHover( + obj + ? { + clip: obj.clip, + track: obj.track, + clipIndex: obj.clipIndex, + trackIndex: obj.trackIndex, + subtrackIndex: obj.subtrackIndex + } + : null, + info + ); + } + } + }) + ) + ); + + if (showClipLabels) { + layers.push( + new TextLayer({ + id: `${this.props.id}-clip-labels`, + data: clipLabelsData, + getText: (d: ClipLabelData) => d.text, + getPosition: (d: ClipLabelData) => d.position, + getSize: 10, + getColor: [255, 255, 255, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + fontFamily: 'Arial, sans-serif', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }) + ); + } + + return layers; + } + + private _createSubtrackSeparatorLayer(separators: SeparatorLineData[]): Layer | null { + const {showSubtrackSeparators = true} = this.props; + if (!showSubtrackSeparators) return null; + + return new LineLayer({ + id: `${this.props.id}-subtrack-separators`, + data: separators, + getSourcePosition: (d: SeparatorLineData) => d.sourcePosition, + getTargetPosition: (d: SeparatorLineData) => d.targetPosition, + getColor: [180, 180, 180, 128], + getWidth: 1, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }); + } + + private _createAxisLayers(axisLines: AxisLineData[], axisLabels: AxisLabelData[]): Layer[] { + const {axisLineProps, axisLabelProps, showAxis = true} = this.props; + if (!showAxis) return []; + + return [ + new LineLayer( + this.getSubLayerProps({ + ...axisLineProps, + id: 'axis-lines', + data: axisLines, + getSourcePosition: (d: AxisLineData) => d.sourcePosition, + getTargetPosition: (d: AxisLineData) => d.targetPosition, + getColor: [150, 150, 150, 255], + getWidth: 2 + }) + ), + new TextLayer( + this.getSubLayerProps({ + ...axisLabelProps, + id: 'axis-labels', + data: axisLabels, + getText: (d: AxisLabelData) => d.text, + getPosition: (d: AxisLabelData) => d.position, + getSize: 11, + getColor: [150, 150, 150, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top' + }) + ) + ]; + } + + private _createScrubberLayers( + scrubberLine: ScrubberLineData[], + scrubberHandle: ScrubberHandleData[], + scrubberLabel: ScrubberLabelData[] + ): Layer[] { + const { + scrubberLineProps, + showScrubber = true, + onScrubberDragStart, + onScrubberHover + } = this.props; + if (!showScrubber) return []; + + return [ + new LineLayer( + this.getSubLayerProps({ + ...scrubberLineProps, + id: 'scrubber-line', + data: scrubberLine, + getSourcePosition: (d: ScrubberLineData) => d.sourcePosition, + getTargetPosition: (d: ScrubberLineData) => d.targetPosition, + getColor: [255, 100, 100, 255], + getWidth: 2 + }) + ), + new SolidPolygonLayer( + this.getSubLayerProps({ + id: 'scrubber-handle', + data: scrubberHandle, + getPolygon: (d: ScrubberHandleData) => d.polygon, + getFillColor: (d: ScrubberHandleData) => d.color, + stroked: true, + getLineColor: [255, 255, 255, 255], + getLineWidth: 2, + pickable: true, + onClick: (info: PickingInfo) => { + if (onScrubberDragStart && info.object) { + onScrubberDragStart(info); + } + }, + onHover: (info: PickingInfo) => { + if (onScrubberHover) { + onScrubberHover(Boolean(info.object), info); + } + } + }) + ), + new TextLayer( + this.getSubLayerProps({ + id: 'scrubber-label', + data: scrubberLabel, + getText: (d: ScrubberLabelData) => d.text, + getPosition: (d: ScrubberLabelData) => d.position, + getSize: 11, + getColor: [255, 100, 100, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'bottom' + }) + ) + ]; + } + + // ===== MAIN RENDER ===== + + renderLayers(): Layer[] { + const { + data: tracks, + timelineStart, + timelineEnd, + viewport, + y = 100, + trackHeight = 40, + trackSpacing = 10 + } = this.props; + + const effectiveStartMs = viewport?.startMs ?? timelineStart; + const effectiveEndMs = viewport?.endMs ?? timelineEnd; + + const visibleTracks = tracks.filter((track) => track.visible !== false); + + const tracksWithSubtracks: TrackWithSubtracks[] = visibleTracks.map((track, trackIndex) => { + const clips = track.clips || []; + const clipsWithSubtracks = assignClipsToSubtracks(clips); + const subtrackCount = Math.max(1, calculateSubtrackCount(clips)); + return {track, trackIndex, clips: clipsWithSubtracks, subtrackCount}; + }); + + const {trackPositions, totalTimelineHeight} = this._calculateTrackPositions( + tracksWithSubtracks, + y, + trackHeight, + trackSpacing + ); + + const trackBackgrounds = this._generateTrackBackgrounds(tracksWithSubtracks, trackPositions); + const trackLabels = this._generateTrackLabels(tracksWithSubtracks, trackPositions); + const clipPolygons = this._generateClipPolygons( + tracksWithSubtracks, + trackPositions, + effectiveStartMs, + effectiveEndMs + ); + const clipLabelsData = clipPolygons.map((clip) => ({ + text: clip.label, + position: clip.labelPosition + })); + const subtrackSeparators = this._generateSubtrackSeparators( + tracksWithSubtracks, + trackPositions + ); + const {axisLines, axisLabels} = this._generateAxis( + totalTimelineHeight, + effectiveStartMs, + effectiveEndMs + ); + const {scrubberLine, scrubberHandle, scrubberLabel} = this._generateScrubber( + totalTimelineHeight, + effectiveStartMs, + effectiveEndMs + ); + + const layers = [ + ...this._createTrackLayers(trackBackgrounds, trackLabels), + ...this._createClipLayers(clipPolygons, clipLabelsData), + this._createSubtrackSeparatorLayer(subtrackSeparators), + ...this._createAxisLayers(axisLines, axisLabels), + ...this._createScrubberLayers(scrubberLine, scrubberHandle, scrubberLabel) + ].filter((layer): layer is Layer => layer !== null); + + return layers; + } +} diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts new file mode 100644 index 000000000..554ac513f --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts @@ -0,0 +1,80 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// ===== STYLE TYPES ===== + +export type SelectionStyle = { + selectedClipColor?: [number, number, number, number]; + hoveredClipColor?: [number, number, number, number]; + selectedTrackColor?: [number, number, number, number]; + hoveredTrackColor?: [number, number, number, number]; + selectedLineWidth?: number; + hoveredLineWidth?: number; +}; + +export type TimelineLayout = { + x?: number; + y?: number; + width?: number; + trackHeight?: number; + trackSpacing?: number; + showTopAxis?: boolean; + showBottomAxis?: boolean; + axisHeight?: number; + axisTickHeight?: number; + axisLabelSize?: number; + showVerticalGrid?: boolean; + showTrackLabels?: boolean; + trackLabelWidth?: number; + scrubberWidth?: number; + scrubberHandleSize?: number; + scrubberLabelSize?: number; + clipPadding?: number; + clipLabelSize?: number; + showClipLabels?: boolean; + clipLabelsMinZoom?: number; + maxClipLabels?: number; + backgroundColor?: [number, number, number, number]; + trackBackgroundColor?: [number, number, number, number]; + trackBorderColor?: [number, number, number, number]; + scrubberColor?: [number, number, number, number]; + axisColor?: [number, number, number, number]; + gridColor?: [number, number, number, number]; + selectedClipColor?: [number, number, number, number]; + hoveredClipColor?: [number, number, number, number]; +}; + +// ===== CONSTANTS ===== + +export const DEFAULT_TIMELINE_LAYOUT: Required = { + x: 50, + y: 100, + width: 800, + trackHeight: 60, + trackSpacing: 10, + showTopAxis: true, + showBottomAxis: false, + axisHeight: 30, + axisTickHeight: 10, + axisLabelSize: 11, + showVerticalGrid: true, + showTrackLabels: true, + trackLabelWidth: 100, + scrubberWidth: 3, + scrubberHandleSize: 16, + scrubberLabelSize: 10, + clipPadding: 5, + clipLabelSize: 10, + showClipLabels: true, + clipLabelsMinZoom: 1.5, + maxClipLabels: 1000, + backgroundColor: [255, 255, 255, 0], + trackBackgroundColor: [240, 240, 240, 100], + trackBorderColor: [200, 200, 200, 255], + scrubberColor: [255, 69, 0, 255], + axisColor: [50, 50, 50, 255], + gridColor: [200, 200, 200, 128], + selectedClipColor: [255, 255, 0, 220], + hoveredClipColor: [255, 255, 255, 255] +}; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts new file mode 100644 index 000000000..801250f85 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts @@ -0,0 +1,141 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// ===== CORE DATA TYPES ===== + +export type TimelineClip = { + id: string | number; + startMs: number; + endMs: number; + color?: [number, number, number, number]; + label?: string; + subtrackIndex?: number; + [key: string]: unknown; +}; + +export type TimelineTrack = { + id: string | number; + clips: TimelineClip[]; + visible?: boolean; + name?: string; + [key: string]: unknown; +}; + +// ===== INTERNAL DATA TYPES ===== + +export type TrackWithSubtracks = { + track: TimelineTrack; + trackIndex: number; + clips: TimelineClip[]; + subtrackCount: number; +}; + +export type TrackPosition = { + y: number; + height: number; + subtrackCount: number; +}; + +export type TrackBackgroundData = { + id: string; + track: TimelineTrack; + trackIndex: number; + polygon: [number, number][]; + color: [number, number, number, number]; +}; + +export type TrackLabelData = { + text: string; + position: [number, number, number]; +}; + +export type ClipPolygonData = { + id: string | number; + clip: TimelineClip; + track: TimelineTrack; + clipIndex: number; + trackIndex: number; + subtrackIndex: number; + polygon: [number, number][]; + color: [number, number, number, number]; + label: string; + labelPosition: [number, number, number]; +}; + +export type ClipLabelData = { + text: string; + position: [number, number, number]; +}; + +export type SeparatorLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type AxisLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type AxisLabelData = { + text: string; + position: [number, number, number]; +}; + +export type ScrubberLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type ScrubberHandleData = { + id: string; + polygon: [number, number][]; + color: [number, number, number, number]; +}; + +export type ScrubberLabelData = { + text: string; + position: [number, number, number]; +}; + +export type TimelineTick = { + position: number; + timeMs: number; + label: string; +}; + +// ===== CALLBACK INFO TYPES ===== + +export type TimelineViewport = { + startMs?: number; + endMs?: number; +}; + +export type TimelineClipInfo = { + clip: TimelineClip; + track: TimelineTrack; + clipIndex: number; + trackIndex: number; + subtrackIndex: number; +}; + +export type TimelineTrackInfo = { + track: TimelineTrack; + index: number; +}; + +export type TimelineScrubberInfo = { + timeMs: number; + isDragging: boolean; +}; + +export type TimelineViewportInfo = { + startMs: number; + endMs: number; + zoomLevel: number; +}; + +// ===== FORMATTER TYPE ===== + +export type TimeAxisLabelFormatter = (timeMs: number) => string; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts new file mode 100644 index 000000000..2b1becc09 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts @@ -0,0 +1,85 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimeAxisLabelFormatter, TimelineTick} from './timeline-types'; + +// ===== TIME FORMATTERS ===== + +export const timeAxisFormatters = { + seconds: (timeMs: number): string => `${(timeMs / 1000).toFixed(1)}s`, + timestamp: (timeMs: number): string => new Date(timeMs).toLocaleTimeString(), + minutesSeconds: (timeMs: number): string => { + const totalSeconds = Math.floor(timeMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }, + hoursMinutesSeconds: (timeMs: number): string => { + const totalSeconds = Math.floor(timeMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } +}; + +// ===== TIMELINE HELPERS ===== + +export type GenerateTicksOptions = { + startMs: number; + endMs: number; + timelineX: number; + timelineWidth: number; + tickCount: number; + formatter: TimeAxisLabelFormatter; +}; + +/** + * Generate timeline axis ticks + */ +export function generateTimelineTicks(opts: GenerateTicksOptions): TimelineTick[] { + const {startMs, endMs, timelineX, timelineWidth, tickCount, formatter} = opts; + const ticks: TimelineTick[] = []; + const timeRange = endMs - startMs; + const step = timeRange / (tickCount - 1); + + for (let i = 0; i < tickCount; i++) { + const timeMs = startMs + i * step; + const position = timelineX + (i / (tickCount - 1)) * timelineWidth; + const label = formatter(timeMs); + ticks.push({position, timeMs, label}); + } + + return ticks; +} + +/** + * Convert canvas/pixel position to time + */ +export function positionToTime( + x: number, + timelineX: number, + timelineWidth: number, + startMs: number, + endMs: number +): number { + const ratio = (x - timelineX) / timelineWidth; + const clampedRatio = Math.max(0, Math.min(1, ratio)); + return startMs + clampedRatio * (endMs - startMs); +} + +/** + * Convert time to canvas/pixel position + */ +export function timeToPosition( + timeMs: number, + timelineX: number, + timelineWidth: number, + startMs: number, + endMs: number +): number { + const timeRatio = (timeMs - startMs) / (endMs - startMs); + const clampedRatio = Math.max(0, Math.min(1, timeRatio)); + return timelineX + clampedRatio * timelineWidth; +} diff --git a/dev/timeline-layers/test/index.spec.ts b/dev/timeline-layers/test/index.spec.ts index 62b62609f..712c4c1ce 100644 --- a/dev/timeline-layers/test/index.spec.ts +++ b/dev/timeline-layers/test/index.spec.ts @@ -4,7 +4,11 @@ import { MultiHorizonGraphLayer, TimeAxisLayer, VerticalGridLayer, - formatTimeMs + TimelineLayer, + formatTimeMs, + timeAxisFormatters, + positionToTime, + timeToPosition } from '../src'; describe('@deck.gl-community/timeline-layers', () => { @@ -18,4 +22,29 @@ describe('@deck.gl-community/timeline-layers', () => { expect(VerticalGridLayer).toBeDefined(); expect(formatTimeMs).toBeTypeOf('function'); }); + + it('exports TimelineLayer', () => { + expect(TimelineLayer).toBeDefined(); + }); + + it('exports time axis formatters', () => { + expect(timeAxisFormatters.seconds).toBeTypeOf('function'); + expect(timeAxisFormatters.seconds(5000)).toBe('5.0s'); + expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30'); + }); + + it('exports position/time conversion utilities', () => { + expect(positionToTime).toBeTypeOf('function'); + expect(timeToPosition).toBeTypeOf('function'); + + // positionToTime: x=0 maps to startMs, x=width maps to endMs + expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0); + expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000); + expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500); + + // timeToPosition: startMs maps to x=0, endMs maps to x=width + expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0); + expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100); + expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50); + }); }); diff --git a/yarn.lock b/yarn.lock index e0f50fd51..b58b2b990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8366,7 +8366,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.5.0": +"fdir@npm:^6.4.3, fdir@npm:^6.4.6, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" peerDependencies: @@ -15004,6 +15004,21 @@ __metadata: languageName: node linkType: hard +"timeline-layer-0054e4@workspace:examples/timeline-layers/timeline-layer": + version: 0.0.0-use.local + resolution: "timeline-layer-0054e4@workspace:examples/timeline-layers/timeline-layer" + dependencies: + "@deck.gl-community/timeline-layers": "workspace:*" + "@deck.gl/core": "npm:~9.2.1" + "@deck.gl/layers": "npm:~9.2.1" + "@deck.gl/react": "npm:~9.2.1" + "@luma.gl/core": "npm:~9.2.0" + react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" + vite: "npm:7.1.1" + languageName: unknown + linkType: soft + "tiny-invariant@npm:^1.0.2": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -15051,7 +15066,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -15685,6 +15700,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:7.1.1": + version: 7.1.1 + resolution: "vite@npm:7.1.1" + dependencies: + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.6" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.14" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/391a5c8b8f287b7b1653dedbe952fb4cb93bf7c99b19dab915cf63497892427198fef637e943a3391eacfecf7f2e8f55c40d0fa065fabdd885641430d0b74af7 + languageName: node + linkType: hard + "vite@npm:^4.5.0": version: 4.5.14 resolution: "vite@npm:4.5.14" From 0256d9a6a0e144dd159521780e40a6b815ea301a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:28:38 +0000 Subject: [PATCH 09/16] fix(timeline-layers): resolve TS2345 - move ClipWithSubtrack to timeline-types, use in TrackWithSubtracks Co-authored-by: charlieforward9 <62311337+charlieforward9@users.noreply.github.com> --- dev/timeline-layers/src/index.ts | 1 + .../src/layers/timeline-layer/timeline-collision.ts | 7 ++----- .../src/layers/timeline-layer/timeline-layer.ts | 3 ++- .../src/layers/timeline-layer/timeline-types.ts | 7 ++++++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dev/timeline-layers/src/index.ts b/dev/timeline-layers/src/index.ts index e6a2f9d4a..31f419259 100644 --- a/dev/timeline-layers/src/index.ts +++ b/dev/timeline-layers/src/index.ts @@ -21,6 +21,7 @@ export {TimelineLayer} from './layers/timeline-layer/timeline-layer'; export type { TimelineClip, + ClipWithSubtrack, TimelineTrack, TrackWithSubtracks, TrackPosition, diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts index 5bcbd5151..2062d139e 100644 --- a/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts @@ -2,12 +2,9 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import type {TimelineClip} from './timeline-types'; - -export type ClipWithSubtrack = TimelineClip & { - subtrackIndex: number; -}; +import type {TimelineClip, ClipWithSubtrack} from './timeline-types'; +export type {ClipWithSubtrack}; /** * Detects overlapping clips and assigns them to subtracks. * Uses a greedy algorithm to minimize the number of subtracks needed. diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts index 8b94d54a1..8348e93c1 100644 --- a/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts @@ -17,6 +17,7 @@ import type { TrackLabelData, ClipPolygonData, ClipLabelData, + ClipWithSubtrack, SeparatorLineData, AxisLineData, AxisLabelData, @@ -290,7 +291,7 @@ export class TimelineLayer extends CompositeLayer { } private _buildClipPolygon( - clip: ReturnType[number], + clip: ClipWithSubtrack, opts: { track: TimelineTrack; trackIndex: number; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts index 801250f85..161d91da2 100644 --- a/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts @@ -24,10 +24,15 @@ export type TimelineTrack = { // ===== INTERNAL DATA TYPES ===== +/** A clip that has been assigned to a specific subtrack by the collision-detection algorithm */ +export type ClipWithSubtrack = TimelineClip & { + subtrackIndex: number; +}; + export type TrackWithSubtracks = { track: TimelineTrack; trackIndex: number; - clips: TimelineClip[]; + clips: ClipWithSubtrack[]; subtrackCount: number; }; From 26967c4dbb674fe61c2e7c3c58627ba110359510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:30:58 +0000 Subject: [PATCH 10/16] test(timeline-layers): add unit tests for TimelineLayer, collision detection, time utils Co-authored-by: charlieforward9 <62311337+charlieforward9@users.noreply.github.com> --- .../test/timeline-layer.spec.ts | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 dev/timeline-layers/test/timeline-layer.spec.ts diff --git a/dev/timeline-layers/test/timeline-layer.spec.ts b/dev/timeline-layers/test/timeline-layer.spec.ts new file mode 100644 index 000000000..057d603bb --- /dev/null +++ b/dev/timeline-layers/test/timeline-layer.spec.ts @@ -0,0 +1,391 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {describe, it, expect, vi} from 'vitest'; +import { + assignClipsToSubtracks, + calculateSubtrackCount +} from '../src/layers/timeline-layer/timeline-collision'; +import { + positionToTime, + timeToPosition, + timeAxisFormatters, + generateTimelineTicks +} from '../src/layers/timeline-layer/timeline-utils'; +import {TimelineLayer} from '../src/layers/timeline-layer/timeline-layer'; +import type {TimelineClip, TimelineTrack} from '../src/layers/timeline-layer/timeline-types'; + +// ===== COLLISION DETECTION ===== + +describe('assignClipsToSubtracks', () => { + it('returns empty array for no clips', () => { + expect(assignClipsToSubtracks([])).toEqual([]); + }); + + it('assigns a single clip to subtrack 0', () => { + const clips: TimelineClip[] = [{id: 'a', startMs: 0, endMs: 1000}]; + const result = assignClipsToSubtracks(clips); + expect(result).toHaveLength(1); + expect(result[0].subtrackIndex).toBe(0); + }); + + it('places non-overlapping clips on the same subtrack', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 500, endMs: 1000} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + expect(byId.a).toBe(0); + expect(byId.b).toBe(0); + }); + + it('places overlapping clips on different subtracks', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 500, endMs: 1500} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + expect(byId.a).not.toBe(byId.b); + }); + + it('packs three clips into two subtracks when third fits in first lane', () => { + // a: 0-500, b: 0-500 (overlaps a), c: 600-1000 (fits after a) + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 0, endMs: 500}, + {id: 'c', startMs: 600, endMs: 1000} + ]; + const result = assignClipsToSubtracks(clips); + const subtracks = result.map((c) => c.subtrackIndex); + const uniqueSubtracks = new Set(subtracks); + expect(uniqueSubtracks.size).toBe(2); + }); + + it('sorts clips by startMs before assigning subtracks', () => { + // Provide clips in reverse order; the algorithm must still work correctly + const clips: TimelineClip[] = [ + {id: 'b', startMs: 1000, endMs: 2000}, + {id: 'a', startMs: 0, endMs: 500} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + // Both fit on subtrack 0 since they don't overlap + expect(byId.a).toBe(0); + expect(byId.b).toBe(0); + }); + + it('preserves original clip properties', () => { + const clip: TimelineClip = { + id: 'x', + startMs: 100, + endMs: 200, + label: 'hello', + color: [1, 2, 3, 4] + }; + const [result] = assignClipsToSubtracks([clip]); + expect(result.id).toBe('x'); + expect(result.label).toBe('hello'); + expect(result.color).toEqual([1, 2, 3, 4]); + expect(result.subtrackIndex).toBeTypeOf('number'); + }); +}); + +describe('calculateSubtrackCount', () => { + it('returns 1 for empty clips array', () => { + expect(calculateSubtrackCount([])).toBe(1); + }); + + it('returns 1 for non-overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 500, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(1); + }); + + it('returns 2 for two fully overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 0, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(2); + }); + + it('returns 3 for three mutually overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 0, endMs: 1000}, + {id: 'c', startMs: 0, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(3); + }); +}); + +// ===== TIME / POSITION MATH ===== + +describe('positionToTime', () => { + it('maps left edge to startMs', () => { + expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0); + }); + + it('maps right edge to endMs', () => { + expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000); + }); + + it('maps midpoint correctly', () => { + expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500); + }); + + it('clamps to startMs when x is before timeline', () => { + expect(positionToTime(-10, 0, 100, 0, 1000)).toBe(0); + }); + + it('clamps to endMs when x is beyond timeline', () => { + expect(positionToTime(200, 0, 100, 0, 1000)).toBe(1000); + }); + + it('respects non-zero timelineX offset', () => { + // Timeline starts at x=50, width=100 → x=100 is midpoint + expect(positionToTime(100, 50, 100, 0, 1000)).toBe(500); + }); + + it('works with non-zero startMs', () => { + expect(positionToTime(50, 0, 100, 5000, 6000)).toBe(5500); + }); +}); + +describe('timeToPosition', () => { + it('maps startMs to left edge', () => { + expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0); + }); + + it('maps endMs to right edge', () => { + expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100); + }); + + it('maps midpoint correctly', () => { + expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50); + }); + + it('clamps when time is before startMs', () => { + expect(timeToPosition(-100, 0, 100, 0, 1000)).toBe(0); + }); + + it('clamps when time is after endMs', () => { + expect(timeToPosition(9000, 0, 100, 0, 1000)).toBe(100); + }); + + it('round-trips with positionToTime', () => { + const timeMs = 750; + const x = timeToPosition(timeMs, 0, 800, 0, 60000); + expect(positionToTime(x, 0, 800, 0, 60000)).toBeCloseTo(timeMs); + }); +}); + +// ===== TIME AXIS FORMATTERS ===== + +describe('timeAxisFormatters', () => { + describe('seconds', () => { + it('formats 0ms as 0.0s', () => { + expect(timeAxisFormatters.seconds(0)).toBe('0.0s'); + }); + + it('formats 5000ms as 5.0s', () => { + expect(timeAxisFormatters.seconds(5000)).toBe('5.0s'); + }); + + it('formats 1500ms as 1.5s', () => { + expect(timeAxisFormatters.seconds(1500)).toBe('1.5s'); + }); + }); + + describe('minutesSeconds', () => { + it('formats 0ms as 0:00', () => { + expect(timeAxisFormatters.minutesSeconds(0)).toBe('0:00'); + }); + + it('formats 90000ms as 1:30', () => { + expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30'); + }); + + it('formats 3600000ms as 60:00', () => { + expect(timeAxisFormatters.minutesSeconds(3600000)).toBe('60:00'); + }); + + it('pads seconds with leading zero', () => { + expect(timeAxisFormatters.minutesSeconds(65000)).toBe('1:05'); + }); + }); + + describe('hoursMinutesSeconds', () => { + it('formats 0ms as 0:00:00', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(0)).toBe('0:00:00'); + }); + + it('formats 3661000ms as 1:01:01', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(3661000)).toBe('1:01:01'); + }); + + it('pads minutes and seconds with leading zeros', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(3600000)).toBe('1:00:00'); + }); + }); +}); + +// ===== TICK GENERATION ===== + +describe('generateTimelineTicks', () => { + it('generates the requested number of ticks', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 0, + timelineWidth: 100, + tickCount: 5, + formatter: timeAxisFormatters.seconds + }); + expect(ticks).toHaveLength(5); + }); + + it('first tick is at timelineX with startMs', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 50, + timelineWidth: 200, + tickCount: 3, + formatter: timeAxisFormatters.seconds + }); + expect(ticks[0].position).toBe(50); + expect(ticks[0].timeMs).toBe(0); + }); + + it('last tick is at timelineX + width with endMs', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 50, + timelineWidth: 200, + tickCount: 3, + formatter: timeAxisFormatters.seconds + }); + expect(ticks[2].position).toBe(250); + expect(ticks[2].timeMs).toBe(1000); + }); + + it('applies the formatter to produce labels', () => { + const fmt = (ms: number) => `t=${ms}`; + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 0, + timelineWidth: 100, + tickCount: 3, + formatter: fmt + }); + expect(ticks.map((t) => t.label)).toEqual(['t=0', 't=500', 't=1000']); + }); +}); + +// ===== TIMELINE LAYER ===== + +describe('TimelineLayer', () => { + it('has correct static layerName', () => { + expect(TimelineLayer.layerName).toBe('TimelineLayer'); + }); + + it('has default props', () => { + expect(TimelineLayer.defaultProps).toBeDefined(); + expect(TimelineLayer.defaultProps.x).toBe(150); + expect(TimelineLayer.defaultProps.y).toBe(100); + expect(TimelineLayer.defaultProps.width).toBe(800); + expect(TimelineLayer.defaultProps.trackHeight).toBe(40); + expect(TimelineLayer.defaultProps.trackSpacing).toBe(10); + expect(TimelineLayer.defaultProps.showScrubber).toBe(true); + expect(TimelineLayer.defaultProps.showAxis).toBe(true); + expect(TimelineLayer.defaultProps.showClipLabels).toBe(true); + expect(TimelineLayer.defaultProps.showTrackLabels).toBe(true); + expect(TimelineLayer.defaultProps.showSubtrackSeparators).toBe(true); + }); +}); + +describe('TimelineLayer.zoomToPoint', () => { + function makeLayer( + overrides: Partial> = {} + ): { + layer: TimelineLayer; + viewportSpy: ReturnType; + zoomSpy: ReturnType; + } { + const viewportSpy = vi.fn(); + const zoomSpy = vi.fn(); + + const tracks: TimelineTrack[] = []; + const layer = new TimelineLayer({ + id: 'test', + data: tracks, + timelineStart: 0, + timelineEnd: 60000, + x: 0, + width: 600, + onViewportChange: viewportSpy, + onZoomChange: zoomSpy + }); + + return {layer, viewportSpy, zoomSpy}; + } + + it('fires onZoomChange with new zoom level', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(2.0, 300, 1.0); // zoom in 2× from centre + expect(zoomSpy).toHaveBeenCalledOnce(); + expect(zoomSpy.mock.calls[0][0]).toBeCloseTo(2.0); + }); + + it('clamps zoom level to a maximum of 100', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(1000, 300, 50.0); + expect(zoomSpy.mock.calls[0][0]).toBe(100); + }); + + it('clamps zoom level to a minimum of 1.0', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(0.0001, 300, 1.0); + expect(zoomSpy.mock.calls[0][0]).toBe(1.0); + }); + + it('fires onViewportChange when zooming in', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(2.0, 300, 1.0); + expect(viewportSpy).toHaveBeenCalledOnce(); + const [startMs, endMs] = viewportSpy.mock.calls[0]; + expect(endMs - startMs).toBeCloseTo(30000); // half the range + }); + + it('fires onViewportChange with full range when zooming back to 1×', () => { + const {layer, viewportSpy} = makeLayer(); + // Force zoom back to exactly 1.0 (factor 0.5 × 2.0 = 1.0) + layer.zoomToPoint(0.5, 300, 2.0); + expect(viewportSpy).toHaveBeenCalledOnce(); + const [startMs, endMs] = viewportSpy.mock.calls[0]; + expect(startMs).toBe(0); + expect(endMs).toBe(60000); + }); + + it('viewport stays within timeline bounds when zooming near left edge', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(4.0, 0, 1.0); // zoom in 4× at left edge + const [startMs] = viewportSpy.mock.calls[0]; + expect(startMs).toBeGreaterThanOrEqual(0); + }); + + it('viewport stays within timeline bounds when zooming near right edge', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(4.0, 600, 1.0); // zoom in 4× at right edge + const [, endMs] = viewportSpy.mock.calls[0]; + expect(endMs).toBeLessThanOrEqual(60000); + }); +}); From d55b207440174dbeb9e833bc633eae129d998691 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 01:49:39 -0500 Subject: [PATCH 11/16] chore(dev/timeline-layers): wire example into workspace + local dev setup - Add dev/timeline-layers/examples/* to root workspace glob so yarn resolves the package correctly - Add vite.config.local.mjs (aliases @deck.gl-community/timeline-layers to src/ so dist/ is never stale during dev) - Fix tsconfig.json extends depth (examples/ moved one level deeper vs original PR path) - Update example README with prototype video + end-goal screenshot from PR #379, start-local instructions, and context links to #379/#517/#38 Co-Authored-By: Claude Sonnet 4.6 --- .../examples/timeline-layer/README.md | 38 ++- .../examples/timeline-layer/tsconfig.json | 2 +- dev/timeline-layers/vite.config.local.mjs | 35 +++ package.json | 1 + yarn.lock | 248 ++++++++++++------ 5 files changed, 237 insertions(+), 87 deletions(-) create mode 100644 dev/timeline-layers/vite.config.local.mjs diff --git a/dev/timeline-layers/examples/timeline-layer/README.md b/dev/timeline-layers/examples/timeline-layer/README.md index aa57866e0..d303681e4 100644 --- a/dev/timeline-layers/examples/timeline-layer/README.md +++ b/dev/timeline-layers/examples/timeline-layer/README.md @@ -1,6 +1,14 @@ # Timeline Layer Example -An interactive timeline visualization built with `@deck.gl-community/timeline-layers`. +An interactive GPU-accelerated timeline built with `@deck.gl-community/timeline-layers` — a deck.gl `CompositeLayer` that replaces DOM-driven timelines and handles hundreds of thousands of datapoints without lag. + +### Prototype + +https://github.com/user-attachments/assets/95006924-913f-4025-a0ef-6d74b336ca67 + +### End Goal + +Timeline end goal ## Features @@ -14,25 +22,31 @@ An interactive timeline visualization built with `@deck.gl-community/timeline-la ## Usage ```bash +# Against installed packages yarn start -``` -Open [http://localhost:5173](http://localhost:5173) +# Against local source (dev) +yarn start-local +``` ## API -Import `TimelineLayer` from `@deck.gl-community/timeline-layers`: - ```ts import {TimelineLayer} from '@deck.gl-community/timeline-layers'; -const layer = new TimelineLayer({ - data: tracks, // TimelineTrack[] - timelineStart: 0, // start of full range (ms) - timelineEnd: 60000, // end of full range (ms) - currentTimeMs: 5000, // current playhead position - onClipClick: ({clip, track}) => console.log('clicked', clip.label) +new TimelineLayer({ + data: tracks, // TimelineTrack[] + timelineStart: 0, // start of full range (ms) + timelineEnd: 60_000, // end of full range (ms) + currentTimeMs: 5_000, // current playhead position + onClipClick: ({clip, track}) => console.log('clicked', clip.label), + onViewportChange: (startMs, endMs) => setViewport({startMs, endMs}), + onZoomChange: (zoom) => setZoom(zoom) }); ``` -See `TimelineLayerProps` for the full API reference. +See [`TimelineLayerProps`](../../src/layers/timeline-layer/timeline-types.ts) for the full API reference. + +## Context + +Originally prototyped in PR [#379](https://github.com/visgl/deck.gl-community/pull/379), refined in PR [#517](https://github.com/visgl/deck.gl-community/pull/517). Lives in `dev/timeline-layers` until promoted to `modules/` — tracked in [#38](https://github.com/visgl/deck.gl-community/issues/38). diff --git a/dev/timeline-layers/examples/timeline-layer/tsconfig.json b/dev/timeline-layers/examples/timeline-layer/tsconfig.json index 61f3747df..3fd35f2e6 100644 --- a/dev/timeline-layers/examples/timeline-layer/tsconfig.json +++ b/dev/timeline-layers/examples/timeline-layer/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../tsconfig.json", "include": ["./*.tsx", "./*.ts"] } diff --git a/dev/timeline-layers/vite.config.local.mjs b/dev/timeline-layers/vite.config.local.mjs new file mode 100644 index 000000000..e8fda4224 --- /dev/null +++ b/dev/timeline-layers/vite.config.local.mjs @@ -0,0 +1,35 @@ +// Allows examples in dev/timeline-layers/examples/ to be built against +// the source code in this repo instead of installed node_modules. + +import {defineConfig} from 'vite'; +import {getOcularConfig} from '@vis.gl/dev-tools'; +import {join} from 'path'; + +const rootDir = join(__dirname, '../..'); + +/** https://vitejs.dev/config/ */ +export default defineConfig(async () => { + const {aliases} = await getOcularConfig({root: rootDir}); + + return { + resolve: { + alias: { + ...aliases, + '@deck.gl-community/timeline-layers': join(rootDir, './dev/timeline-layers/src'), + '@deck.gl': join(rootDir, './node_modules/@deck.gl'), + '@luma.gl': join(rootDir, './node_modules/@luma.gl'), + '@math.gl': join(rootDir, './node_modules/@math.gl'), + '@loaders.gl': join(rootDir, './node_modules/@loaders.gl'), + 'react': join(rootDir, './node_modules/react'), + 'react-dom': join(rootDir, './node_modules/react-dom'), + } + }, + server: { + open: true, + port: 8080 + }, + optimizeDeps: { + esbuildOptions: {target: 'es2020'} + } + }; +}); diff --git a/package.json b/package.json index 7e08e4470..a9dc30855 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "workspaces": [ "modules/*", "dev/*", + "dev/timeline-layers/examples/*", "examples/**" ], "scripts": { diff --git a/yarn.lock b/yarn.lock index b58b2b990..be128eb71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -510,7 +510,7 @@ __metadata: languageName: unknown linkType: soft -"@deck.gl-community/timeline-layers@workspace:dev/timeline-layers": +"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:dev/timeline-layers": version: 0.0.0-use.local resolution: "@deck.gl-community/timeline-layers@workspace:dev/timeline-layers" dependencies: @@ -554,6 +554,31 @@ __metadata: languageName: node linkType: hard +"@deck.gl/core@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/core@npm:9.2.10" + dependencies: + "@loaders.gl/core": "npm:^4.3.4" + "@loaders.gl/images": "npm:^4.3.4" + "@luma.gl/constants": "npm:^9.2.6" + "@luma.gl/core": "npm:^9.2.6" + "@luma.gl/engine": "npm:^9.2.6" + "@luma.gl/shadertools": "npm:^9.2.6" + "@luma.gl/webgl": "npm:^9.2.6" + "@math.gl/core": "npm:^4.1.0" + "@math.gl/sun": "npm:^4.1.0" + "@math.gl/types": "npm:^4.1.0" + "@math.gl/web-mercator": "npm:^4.1.0" + "@probe.gl/env": "npm:^4.1.1" + "@probe.gl/log": "npm:^4.1.1" + "@probe.gl/stats": "npm:^4.1.1" + "@types/offscreencanvas": "npm:^2019.6.4" + gl-matrix: "npm:^3.0.0" + mjolnir.js: "npm:^3.0.0" + checksum: 10c0/195a5a2ae4216bfef0c7d280fd8b28a3be14c423ee4d42e4a0c8c6d960b9845ce33eba72390b61d5f57fa9be676672fe7f92013ff5c02989f85792c309480c8e + languageName: node + linkType: hard + "@deck.gl/core@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/core@npm:9.2.8" @@ -627,6 +652,27 @@ __metadata: languageName: node linkType: hard +"@deck.gl/layers@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/layers@npm:9.2.10" + dependencies: + "@loaders.gl/images": "npm:^4.3.4" + "@loaders.gl/schema": "npm:^4.3.4" + "@luma.gl/shadertools": "npm:^9.2.6" + "@mapbox/tiny-sdf": "npm:^2.0.5" + "@math.gl/core": "npm:^4.1.0" + "@math.gl/polygon": "npm:^4.1.0" + "@math.gl/web-mercator": "npm:^4.1.0" + earcut: "npm:^2.2.4" + peerDependencies: + "@deck.gl/core": ~9.2.0 + "@loaders.gl/core": ^4.3.4 + "@luma.gl/core": ~9.2.6 + "@luma.gl/engine": ~9.2.6 + checksum: 10c0/78e55822e06981725784b4d876a1aa2cc51a607c5e53bd5cf319abb6134305b83f2d07699932b6b0143cb4a14c8b3befedccb575cde62a24211e3c2052dc5a13 + languageName: node + linkType: hard + "@deck.gl/layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/layers@npm:9.2.8" @@ -666,6 +712,18 @@ __metadata: languageName: node linkType: hard +"@deck.gl/react@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/react@npm:9.2.10" + peerDependencies: + "@deck.gl/core": ~9.2.0 + "@deck.gl/widgets": ~9.2.0 + react: ">=16.3.0" + react-dom: ">=16.3.0" + checksum: 10c0/87b79ad77e494fb16cb0c0ac0d00d9911478a95f9e686e681687fc3e395e469380f1f79429bbbdc67c9e8adac49d8c2311aba991222e30620ca5a540e72578ed + languageName: node + linkType: hard + "@deck.gl/react@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/react@npm:9.2.8" @@ -2112,7 +2170,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.6": +"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.0, @luma.gl/core@npm:~9.2.6": version: 9.2.6 resolution: "@luma.gl/core@npm:9.2.6" dependencies: @@ -2939,6 +2997,13 @@ __metadata: languageName: node linkType: hard +"@probe.gl/env@npm:4.1.1, @probe.gl/env@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/env@npm:4.1.1" + checksum: 10c0/431f53d95936ba1ee08e8d4c155cc3c49d80d528acaff933ae8f32a4725f04d88ee634707e20baa885a505d6f1401f895bdfcfa42c259b3220dd26463d2cfb2d + languageName: node + linkType: hard + "@probe.gl/log@npm:4.1.0, @probe.gl/log@npm:^4.0.2, @probe.gl/log@npm:^4.0.4, @probe.gl/log@npm:^4.0.8, @probe.gl/log@npm:^4.1.0": version: 4.1.0 resolution: "@probe.gl/log@npm:4.1.0" @@ -2948,6 +3013,15 @@ __metadata: languageName: node linkType: hard +"@probe.gl/log@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/log@npm:4.1.1" + dependencies: + "@probe.gl/env": "npm:4.1.1" + checksum: 10c0/713043aea095dbe6e97525a5d254f2563e9e0b6c64fdb888603f41342ed0f0a76a64e6996d2d461c7f1f9c6271a931b2e922d9e47f8c43f3d4116867f4cd5d0a + languageName: node + linkType: hard + "@probe.gl/stats@npm:^4.0.0, @probe.gl/stats@npm:^4.0.2, @probe.gl/stats@npm:^4.0.8, @probe.gl/stats@npm:^4.1.0": version: 4.1.0 resolution: "@probe.gl/stats@npm:4.1.0" @@ -2955,6 +3029,13 @@ __metadata: languageName: node linkType: hard +"@probe.gl/stats@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/stats@npm:4.1.1" + checksum: 10c0/7fd49d891e5ecfc82330c0db9180cc6a3fae0f78062570fabdc1860f79f09b612c53af08b9d6e2bb701b79fb7156ec2c9c0ddf53e5dba12b0560c7bbc957afc7 + languageName: node + linkType: hard + "@probe.gl/test-utils@npm:^4.0.4, @probe.gl/test-utils@npm:^4.0.6": version: 4.1.0 resolution: "@probe.gl/test-utils@npm:4.1.0" @@ -8366,7 +8447,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.4.6, fdir@npm:^6.5.0": +"fdir@npm:^6.4.3, fdir@npm:^6.4.6, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" peerDependencies: @@ -9273,6 +9354,25 @@ __metadata: languageName: node linkType: hard +"horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer": + version: 0.0.0-use.local + resolution: "horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer" + dependencies: + "@deck.gl-community/timeline-layers": "workspace:*" + "@deck.gl/core": "npm:~9.2.8" + "@deck.gl/layers": "npm:~9.2.8" + "@deck.gl/react": "npm:~9.2.8" + "@deck.gl/widgets": "npm:~9.2.8" + "@loaders.gl/core": "npm:^4.2.0" + "@luma.gl/core": "npm:~9.2.6" + "@luma.gl/engine": "npm:~9.2.6" + "@luma.gl/shadertools": "npm:~9.2.6" + react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" + vite: "npm:^7.3.1" + languageName: unknown + linkType: soft + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -15004,21 +15104,21 @@ __metadata: languageName: node linkType: hard -"timeline-layer-0054e4@workspace:examples/timeline-layers/timeline-layer": - version: 0.0.0-use.local - resolution: "timeline-layer-0054e4@workspace:examples/timeline-layers/timeline-layer" - dependencies: - "@deck.gl-community/timeline-layers": "workspace:*" - "@deck.gl/core": "npm:~9.2.1" - "@deck.gl/layers": "npm:~9.2.1" - "@deck.gl/react": "npm:~9.2.1" - "@luma.gl/core": "npm:~9.2.0" - react: "npm:^18.3.1" - react-dom: "npm:^18.3.1" - vite: "npm:7.1.1" - languageName: unknown - linkType: soft - +"timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer": + version: 0.0.0-use.local + resolution: "timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer" + dependencies: + "@deck.gl-community/timeline-layers": "workspace:*" + "@deck.gl/core": "npm:~9.2.1" + "@deck.gl/layers": "npm:~9.2.1" + "@deck.gl/react": "npm:~9.2.1" + "@luma.gl/core": "npm:~9.2.0" + react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" + vite: "npm:7.1.1" + languageName: unknown + linkType: soft + "tiny-invariant@npm:^1.0.2": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -15066,7 +15166,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -15700,61 +15800,61 @@ __metadata: languageName: node linkType: hard -"vite@npm:7.1.1": - version: 7.1.1 - resolution: "vite@npm:7.1.1" - dependencies: - esbuild: "npm:^0.25.0" - fdir: "npm:^6.4.6" - fsevents: "npm:~2.3.3" - picomatch: "npm:^4.0.3" - postcss: "npm:^8.5.6" - rollup: "npm:^4.43.0" - tinyglobby: "npm:^0.2.14" - peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - jiti: ">=1.21.0" - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: ">=0.54.8" - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - bin: - vite: bin/vite.js - checksum: 10c0/391a5c8b8f287b7b1653dedbe952fb4cb93bf7c99b19dab915cf63497892427198fef637e943a3391eacfecf7f2e8f55c40d0fa065fabdd885641430d0b74af7 - languageName: node - linkType: hard - +"vite@npm:7.1.1": + version: 7.1.1 + resolution: "vite@npm:7.1.1" + dependencies: + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.6" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.14" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/391a5c8b8f287b7b1653dedbe952fb4cb93bf7c99b19dab915cf63497892427198fef637e943a3391eacfecf7f2e8f55c40d0fa065fabdd885641430d0b74af7 + languageName: node + linkType: hard + "vite@npm:^4.5.0": version: 4.5.14 resolution: "vite@npm:4.5.14" From 85590fc9829620228deb524d0077e195b542535e Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 02:04:06 -0500 Subject: [PATCH 12/16] fix(ci): grant pull-requests+checks write for Coveralls PR comment The global permissions block only had contents:read. Coveralls needs pull-requests:write to post a coverage comment and checks:write for the status check. Scoped to test-node job only. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4943e6ba..a38f4377f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,10 @@ jobs: test-node: runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: write + checks: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From fd97dbb318c8ac3760383668ad8ac19e2440a371 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 02:16:32 -0500 Subject: [PATCH 13/16] fix(ci): post coverage summary directly as PR comment Replace Coveralls upload (which requires the Coveralls GitHub App to be installed on the org to post PR comments) with an actions/github-script step that reads the vitest coverage table from stdout and posts/updates it as a PR comment directly. No external service setup needed. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 48 ++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a38f4377f..ab6998207 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,13 +81,51 @@ jobs: run: yarn build - name: Run node tests with coverage - run: yarn test-ci + run: yarn test-ci 2>&1 | tee /tmp/coverage.txt; exit ${PIPESTATUS[0]} - - name: Coveralls - uses: coverallsapp/github-action@v2 + - name: Post coverage summary to PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - fail-on-error: false + script: | + const fs = require('fs'); + const raw = fs.readFileSync('/tmp/coverage.txt', 'utf8'); + // extract the coverage table (lines between the --- dividers) + const lines = raw.split('\n'); + const start = lines.findIndex(l => l.includes('% Stmts')); + const end = lines.findLastIndex(l => l.match(/^-+\|/)); + const table = start >= 0 && end > start + ? lines.slice(start - 1, end + 1).join('\n') + : '_Coverage table not found_'; + const testLine = lines.find(l => l.includes('Test Files')) || ''; + const body = [ + '## Coverage report', + '```', + table, + '```', + testLine.trim() ? `\`${testLine.trim()}\`` : '', + ].filter(Boolean).join('\n'); + const {data: comments} = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.startsWith('## Coverage report')); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } test-headless: runs-on: ubuntu-22.04 From 80ccadd4abac7c74400babe1c7eaeeb4230098b5 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 02:21:22 -0500 Subject: [PATCH 14/16] chore(ci): remove duplicate test.yaml superseded by test.yml test.yaml was the original single-job workflow. test.yml replaced it with parallel lint/test-node/test-headless jobs. Both were running on every PR causing duplicate test-node checks. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yaml | 101 ------------------------------------ 1 file changed, 101 deletions(-) delete mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 6f8a16f9d..000000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: test - -# On every pull request, but only on push to master -on: - push: - branches: - - master - pull_request: - -permissions: - contents: read - -jobs: - test-node: - runs-on: ubuntu-latest - - # Keep env simple. You don't need --openssl-legacy-provider for Node 20. - env: - NODE_ENV: test - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '20' # ✅ match what Vite/Vitest expect - - # ✅ Enable Corepack so the `packageManager` field is honored - - name: Enable Corepack / Yarn 4 - run: | - corepack enable - corepack prepare yarn@4.10.3 --activate - yarn -v - - # ✅ Cache Yarn 4's compressed archives - - name: Cache Yarn 4 artifacts - id: yarn-cache - uses: actions/cache@v4 - with: - path: | - .yarn/cache - .pnp.* # if you use Plug’n’Play - .yarn/install-state.gz - key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn-cache-${{ runner.os }}- - - - name: Print versions - run: | - node -v - yarn -v - node -p "process.versions" - - - name: Install deps - # Always run to materialize node_modules; the cache only restores tarballs. - run: yarn --frozen-lockfile - - # Sanity-check that Vite sees the root config (this MUST be true) - - name: Verify vite.config.ts presence - run: | - test -f vite.config.ts || (echo "vite.config.ts missing at repo root" && exit 1) - node -e "console.log('vite.config.ts exists, good')" - - - name: Build packages - run: yarn build - - - name: Run node tests - run: yarn test-node - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: pw-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - restore-keys: pw-${{ runner.os }}- - - - name: Install Playwright Chromium (only) - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps chromium - - - name: Run headless browser tests - run: yarn test-headless --reporter=verbose - - - name: Cache website Yarn artifacts - id: website-yarn-cache - uses: actions/cache@v4 - with: - path: | - website/.yarn/cache - website/.pnp.* - website/.yarn/install-state.gz - key: website-yarn-cache-${{ runner.os }}-${{ hashFiles('website/yarn.lock') }} - restore-keys: | - website-yarn-cache-${{ runner.os }}- - - - name: Build website - run: | - cd website - yarn - yarn build From aa2e4bd47fad30884921f34b5536e7859df6f380 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 02:27:55 -0500 Subject: [PATCH 15/16] fix(ci): compact coverage comment to overall % only Replace verbose per-file table with a single "All files" totals line. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab6998207..4e34552f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,20 +90,19 @@ jobs: script: | const fs = require('fs'); const raw = fs.readFileSync('/tmp/coverage.txt', 'utf8'); - // extract the coverage table (lines between the --- dividers) const lines = raw.split('\n'); - const start = lines.findIndex(l => l.includes('% Stmts')); - const end = lines.findLastIndex(l => l.match(/^-+\|/)); - const table = start >= 0 && end > start - ? lines.slice(start - 1, end + 1).join('\n') - : '_Coverage table not found_'; + // Extract the "All files" totals line + const allFiles = lines.find(l => l.match(/^All files\s*\|/)); + let coverageLine = '_Coverage data not found_'; + if (allFiles) { + const [, stmts, branch, funcs, ln] = allFiles.split('|').map(s => s.trim()); + coverageLine = `Stmts: **${stmts}%** | Branch: **${branch}%** | Funcs: **${funcs}%** | Lines: **${ln}%**`; + } const testLine = lines.find(l => l.includes('Test Files')) || ''; const body = [ - '## Coverage report', - '```', - table, - '```', - testLine.trim() ? `\`${testLine.trim()}\`` : '', + '## Coverage', + coverageLine, + testLine.trim() ? `> ${testLine.trim()}` : '', ].filter(Boolean).join('\n'); const {data: comments} = await github.rest.issues.listComments({ owner: context.repo.owner, From a4ec9c255e6865b4cc833e03664ea55b96dfac16 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Thu, 5 Mar 2026 02:30:35 -0500 Subject: [PATCH 16/16] feat(ci): show coverage delta vs master in PR comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Save "All files" line as artifact on master pushes - On PRs, find + download latest master baseline artifact - Show delta (▲/▼/●) next to each metric vs master Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 88 +++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e34552f1..549e1e622 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,7 @@ jobs: runs-on: ubuntu-22.04 permissions: contents: read + actions: read pull-requests: write checks: write @@ -83,6 +84,55 @@ jobs: - name: Run node tests with coverage run: yarn test-ci 2>&1 | tee /tmp/coverage.txt; exit ${PIPESTATUS[0]} + - name: Save coverage summary + if: always() + run: grep -m1 "^All files" /tmp/coverage.txt > /tmp/coverage-summary.txt || true + + - name: Upload coverage baseline + if: github.ref == 'refs/heads/master' + uses: actions/upload-artifact@v4 + with: + name: coverage-baseline + path: /tmp/coverage-summary.txt + retention-days: 90 + + - name: Find latest master run with baseline + if: github.event_name == 'pull_request' + id: find-baseline + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const runs = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'test.yml', + branch: 'master', + status: 'success', + per_page: 5, + }); + for (const run of runs.data.workflow_runs) { + const {data} = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + if (data.artifacts.find(a => a.name === 'coverage-baseline')) { + return String(run.id); + } + } + return ''; + + - name: Download coverage baseline + if: github.event_name == 'pull_request' && steps.find-baseline.outputs.result != '' + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-baseline + path: /tmp/baseline + run-id: ${{ steps.find-baseline.outputs.result }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Post coverage summary to PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 @@ -91,25 +141,51 @@ jobs: const fs = require('fs'); const raw = fs.readFileSync('/tmp/coverage.txt', 'utf8'); const lines = raw.split('\n'); - // Extract the "All files" totals line - const allFiles = lines.find(l => l.match(/^All files\s*\|/)); + + const parseAllFiles = text => { + const line = text.split('\n').find(l => l.match(/^All files\s*\|/)); + if (!line) return null; + const [, stmts, branch, funcs, ln] = line.split('|').map(s => s.trim()); + return {stmts: parseFloat(stmts), branch: parseFloat(branch), funcs: parseFloat(funcs), ln: parseFloat(ln)}; + }; + + const current = parseAllFiles(raw); + let baseline = null; + try { baseline = parseAllFiles(fs.readFileSync('/tmp/baseline/coverage-summary.txt', 'utf8')); } catch {} + + const fmt = (val, baseVal) => { + const str = `**${val.toFixed(2)}%**`; + if (baseline === null || isNaN(baseVal)) return str; + const d = (val - baseVal).toFixed(2); + const sign = d >= 0 ? '+' : ''; + const color = d > 0 ? '▲' : d < 0 ? '▼' : '●'; + return `${str} ${color}${sign}${d}%`; + }; + let coverageLine = '_Coverage data not found_'; - if (allFiles) { - const [, stmts, branch, funcs, ln] = allFiles.split('|').map(s => s.trim()); - coverageLine = `Stmts: **${stmts}%** | Branch: **${branch}%** | Funcs: **${funcs}%** | Lines: **${ln}%**`; + if (current) { + const b = baseline || {}; + coverageLine = [ + `Stmts: ${fmt(current.stmts, b.stmts)}`, + `Branch: ${fmt(current.branch, b.branch)}`, + `Funcs: ${fmt(current.funcs, b.funcs)}`, + `Lines: ${fmt(current.ln, b.ln)}`, + ].join(' | '); } + const testLine = lines.find(l => l.includes('Test Files')) || ''; const body = [ '## Coverage', coverageLine, testLine.trim() ? `> ${testLine.trim()}` : '', ].filter(Boolean).join('\n'); + const {data: comments} = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); - const existing = comments.find(c => c.body.startsWith('## Coverage report')); + const existing = comments.find(c => c.body.startsWith('## Coverage')); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner,