Skip to content

Points feature filter (MVP step 2) + responsive off-thread loads - #81

Merged
xinaesthete merged 41 commits into
mainfrom
points-feature-filter
Jul 13, 2026
Merged

Points feature filter (MVP step 2) + responsive off-thread loads#81
xinaesthete merged 41 commits into
mainfrom
points-feature-filter

Conversation

@xinaesthete

@xinaesthete xinaesthete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Points feature filter (MVP step 2) + responsive off-thread loads

Follow-up to #80. Adds the Points feature filter on the @spatialdata/layers
PointsLayer composite and makes the geometry / catalog / codes loading
off-thread, so large transcripts layers stay responsive while loading,
filtering, and colouring. points-feature-filtermain.

The PR grew well past the original MVP-step-2 scope (the earlier "known
limitations" — whole-dataset scan, full catalog, memory-cap UI — are now all
done). It's at a mergeable checkpoint: correct, honest, and non-regressed,
with a deliberate stopping line before a larger redesign. Known rough edges are
inventoried in docs/plans/points-redesign-punchlist.md
rather than patched here (see Deferred below).

What's in it

  • PointsDataEngine owns the feature catalog + per-row codes — a React-free,
    headless-tested cache in @spatialdata/layers with subscribe/notify.
  • Off-thread geometry + features decode — decode runs in the points worker;
    the main thread only does async range-read fetches. This is what keeps loading
    and filtering responsive. Silent/misconfigured workers time out to a main-thread
    fallback instead of hanging.
  • Whole-dataset feature-index scan — when a selection is active, scan the
    whole dataset for matching feature codes (parquet footer stats skip the
    row groups a selected feature can't live in) and load only matching rows up to
    the memory cap. So genes outside the resident preload window still render.
    Includes a Thrift-Compact footer-stats parser for row-group column min/max.
  • Full-dataset catalog with counts — the panel lists all features (with
    counts), not just the resident preview; the instant resident subset is
    superseded by the full scan when it settles.
  • Dictionary-only (wild-type) supporttranscripts stores with a feature_name
    dict column and no *_codes column are matched by resolving each row's name
    against one authoritative code space (the full catalog).
  • Colour-by-feature — per-point feature codes fed to deck as binary
    attributes; a GPU shader extension colours by an OKLCh golden-angle palette
    (single source for the L/C constants, JS swatch mirror in lockstep).
  • Per-layer memory cap UIpointsMemoryCap (1M–16M, default 4M); lowering
    sheds resident rows in memory (no refetch), raising reloads only to grow a
    truncated batch; an AbortController per load cancels superseded fetches.
  • Reactive feature-state hookusePointsFeatureState (one
    useSyncExternalStore over the engine) replaces the prop-drilled getters; the
    'use no memo' escape hatch is now scoped to two leaf panels instead of the
    whole canvas shell.
  • Progressive matched-scan render — the scan streams matched chunks into a
    growing buffer that renders as an overlay while it loads (spike quality — see
    Deferred).

Verified

  • Type-checks clean across core / layers / vis; tests core 146 · layers
    120 · vis 50
    .
  • Live on a real Xenium transcripts_feature_then_morton layer: load + toggles
    stay responsive (worker path; prior main-thread path froze the tab); the 541-
    feature catalog appears and filters correctly; selecting a non-resident gene
    (e.g. MALL, 646k pts) loads and renders via the scan; colour matches the panel
    swatches.

On the vendored parquet-wasm

All browser parquet decoding uses the Vitessce CDN build of parquet-wasm
(Mark Keller's fork), copied into packages/core/vendor/parquet-wasm/ rather than
loaded from cdn.vitessce.io at runtime. That build exposes readMetadata /
readParquetRowGroup (the row-group APIs from
kylebarron/parquet-wasm#804)
which parquet-wasm@0.6.1 on npm lacks. docs/parquet-wasm-limitations.md
records the details. Key limitation: no per-column-chunk offsets, so we can't
do a projected fetch — we fetch all columns' bytes (async) and decode only the
projected columns off-thread. (readParquetRowGroup also mis-decodes dictionary
columns like feature_name, so those decode via whole parts using readParquet.)

Deferred — see the punch-list

Rather than keep patching, the remaining issues are tracked in
docs/plans/points-redesign-punchlist.md,
split fix-before-merge (done in this PR: F1–F4) vs defer-to-redesign. The deferred
set is mostly one root cause: PointsEntry is an imperative mutable record
mutated with side effects mid-flight, read through the monolithic useLayerData
so "which points to show" and the stats are decided ad-hoc. The planned follow-up
(break up useLayerData, spike Effect / TanStack Query) fixes the class.
Highlights deferred there:

  • Progressive-overlay visibility + flashing (D10) — the overlay still has
    visibility-logic problems and flashes, because each chunk rebuilds the render
    resource (deck recreates the layer per step); the real fix is a stable growing
    GPU buffer (preallocate to cap, append, bump a draw count).
  • Progressive initial load (D3), progressive/active stats (D4),
    tiled/Morton viewport-driven loading (D5), multi-layer worker contention
    (D6), GeoArrow (D7), streaming cancellation (D8), removing the
    'use no memo' hatches
    (D9), and a precise per-selection matched count (F3 now
    reports the honest in-memory batch size instead).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added feature-based filtering for point layers, including progressive loading and filtering status.
    • Added optional categorical coloring by feature.
    • Added controls for point memory limits and clearer truncation/loading indicators.
    • Point data can now load and decode off the main thread for improved responsiveness.
  • Bug Fixes
    • Preserved feature metadata when filtering, capping, and rendering point data.
    • Added safeguards so stalled background loading requests time out and recover gracefully.
  • Documentation
    • Documented current Parquet and point-loading limitations, performance findings, and future improvements.

xinaesthete and others added 6 commits July 6, 2026 17:36
MVP step 2 (points feature filter) groundwork. Extend the framework-agnostic
PointsDataEngine so the feature catalog and per-row feature codes are
engine-owned state (not vis god-hook refs, the way the WIP branch held them):

- ensureFeatureCatalog / getFeatureCatalog / isFeatureCatalogLoading — builds
  the catalog via element.listFeaturesWithCounts (feature-column scan,
  worker-offloaded for oversized datasets), reactively via subscribe/notify.
  Tri-state getFeatureCatalog: undefined (unrequested) / null (no feature_key) /
  catalog.
- ensureRowFeatureCodes / getRowFeatureCodes / hasRowFeatureCodes — loads the
  filter mask row-aligned with the resident batch; reuses the engine catalog for
  name->code mapping when built. Documents the load-bearing alignment invariant
  (geometry preload and row codes must read the same file-order rows under the
  same memory cap).

9 new headless unit tests (idempotency, reactivity, null/undefined settling,
catalog reuse, evict). Layers: 88 tests pass, typecheck clean. No vis wiring yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the engine-owned filter mask into the PointsLayer composite:

- Add serializable `featureCodes?: number[]` to PointsLayerConfig (undefined =
  all features shown; array = restrict to those Feature Codes). Distinct from the
  runtime-only Feature Highlight coming in step 3.
- In useLayerData getLayers, when a filter is active, idempotently kick
  pointsEngine.ensureRowFeatureCodes and pass the resulting row-aligned
  preloadedFeatureCodes plus featureCodes into PointsLayer. The composite only
  filters once both are present, so it draws unfiltered until the codes settle
  (then notify -> re-render flows them in). featureCodes undefined = unchanged
  parity behaviour.

No filter UI yet (step 3). Vis + layers typecheck clean; vis 42 tests, layers 88.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MVP step 2 UI. Port PointsFeatureFilterPanel into vis, sourced from the
engine-owned catalog rather than the WIP branch's god-hook refs:

- useLayerData gains thin bindings requestPointsFeatureCatalog /
  getPointsFeatureCatalog / isPointsFeatureCatalogLoading over
  PointsDataEngine's catalog cache (added to the hook API + return).
- index.tsx renders the panel under the points properties (beside the point-size
  slider); checkbox toggles write serializable config.featureCodes via
  updateLayer, driving the render-path filter wired in the previous commit.

Verified live against a real Xenium transcripts layer: "Load feature list"
builds a 541-entry catalog (feature_name genes + control codewords) via the
feature-column fast-path scan and the panel renders the selectable list.

Known follow-up (next commit): the catalog/row-codes build runs on the main
thread with the points worker off by default, so it can stall for tens of
seconds on large transcripts — the worker-enable + timeout-fallback lands next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A worker that loads but never posts a response (e.g. a host points
enablePointsWorker() at a URL that resolves but whose module is misconfigured)
would leave a request awaiting forever. Add a per-request timeout in
pointsWorkerClient: after a generous budget (default 30s, tunable via
setPointsWorkerRequestTimeout) with no reply, reject the request so the caller
falls back to the main thread (every *InWorker helper is wrapped in try/catch
fallback). Timeouts are cleared on response/error/disable. New unit test drives a
SilentWorker stub and asserts the request rejects rather than hangs (core: 130
tests). setPointsWorkerRequestTimeout exported from the barrel.

Deliberately NOT enabling the worker in the demo yet: verifying live on a real
Xenium transcripts element showed the worker catalog path
(readParquetWorkerPayload with fullPartsForFallback) fetches the WHOLE parquet
before scanning, whereas the main-thread path does a projected single-column
range read — so for a transcripts element with no {feature_key}_codes column
(no cheap dictionary-page scan) enabling the worker regressed catalog build from
~20s to >150s. Recorded as the next perf task (projected/dictionary-only worker
payload) in docs/plans/points-mvp-and-roadmap.md; demo main.tsx documents why.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the feature filter's separate, blocking loads at filter time. Previously
the first filter interaction triggered a catalog scan AND a row-code decode
(each reading the feature column of the whole transcripts file on the main
thread), so filtering froze the UI for tens of seconds.

Now the feature column is read once, WITH the geometry, in the same projected
capped preload:

- core: PointsLoadOptions.includeFeatureCodes reads the feature column(s)
  alongside the axes; loadPoints derives the per-row codes AND the catalog from
  that single decode and returns them (PointsLoadResult.featureCatalog). The
  catalog reflects the resident (preloaded) rows — the features actually drawn.
- layers: PointsDataEngine.ensureLoaded requests includeFeatureCodes and stores
  the catalog + row codes as resident/settled state, so getFeatureCatalog /
  getRowFeatureCodes return them immediately and ensureFeatureCatalog /
  ensureRowFeatureCodes become no-ops. isFeatureCatalogLoading now tracks the
  geometry load, so the panel shows a spinner during load (via the reliable
  onStatus re-render) instead of a premature "load feature list" prompt — fixing
  the catalog reactivity gap. The filter's row codes are ready the moment points
  draw, so toggling no longer triggers a separate load.

Tests: core 131 (loadPoints includeFeatureCodes derives [0,1,0,2,1] + 3-gene
catalog), layers 90 (resident-from-one-load + loading-while-preloading).

KNOWN LIMITATION (next step): this consolidates the filter's loads but does not
eliminate the underlying cost — decoding the feature column of a large
transcripts file on the main thread still blocks, now folded into the initial
load. On very large files (e.g. the index-permutations benchmark) the initial
load itself stalls. The clean fix is off-thread decode via a projected worker
payload (fetch only the needed column chunks, decode in the worker); the current
worker path fetches whole-file/all-column bytes and cannot help. Also, the
per-toggle in-memory re-filter is still main-thread (worker/GPU is the follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The codes-with-geometry preload previously decoded the feature column on the main
thread, freezing the UI for tens of seconds on large transcripts. Move that
decode into the points worker so the main thread only does async range-read
fetches while the worker does the CPU-heavy parquet decode.

- core: new `decodeGeometryWithFeatures` worker request + `geometryWithFeatures`
  response; `decodeGeometryWithFeaturesFromPayload` (in pointsWorkerScan) does one
  projected decode → geometry + per-row codes + catalog, mirroring the main-thread
  derivation so both produce identical results. Client
  `decodeGeometryWithFeaturesInWorker`; VPointsSource.loadPoints uses it when the
  worker is enabled (fetches whole parts via `fetchParquetPayloadCapped`, decodes
  off-thread), falling back to the main-thread decode otherwise.
- Uses whole-PART bytes (readParquet), not per-row-group reads: readParquetRowGroup
  mis-decodes dictionary-encoded columns like `feature_name` (same reason
  scanFeatureCatalogFromPayload falls back to parts) — row-group decode produced a
  1-entry empty catalog; parts decode yields the correct 541-feature catalog.
- demo: enable the worker + widen the request timeout to 120s (large decodes are
  legitimately slow); a silent worker still falls back.
- docs/parquet-wasm-limitations.md: why projected *fetch* is impossible with the
  vendored bindings (no column-chunk offsets; refs kylebarron/parquet-wasm#804)
  and what we'd want from alternative/extended bindings.

Verified live on the Xenium index-permutations benchmark: the ~49s load and the
feature toggles stay fully responsive (monitor: 0 janky frames >150ms, max gap
~123ms during load / ~76ms on toggle), the catalog auto-appears with 541 correct
features, and "Center on layer" is reactive. Tests: core 133 (2 new decode-helper
cases), layers 90, vis 42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@xinaesthete, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da8174d6-7710-442d-9e7c-74d51246382c

📥 Commits

Reviewing files that changed from the base of the PR and between ee58000 and ed87b18.

📒 Files selected for processing (1)
  • docs/plans/points-redesign-punchlist.md
📝 Walkthrough

Walkthrough

Changes

This change adds feature-code-aware point loading, filtering, worker decoding, memory-cap management, GPU coloring, and SpatialCanvas controls. It also adds Parquet footer-stat parsing, worker timeout handling, tests, and documentation of parquet-wasm limitations.

Points data and workers

Layer / File(s) Summary
Core feature loading and filtering
packages/core/src/..., packages/core/tests/...
Point batches and load results now carry aligned feature codes and catalogs, with row-group statistics used to skip impossible feature groups.
Worker decoding and request lifecycle
packages/core/src/workers/..., packages/core/tests/pointsWorker*
Workers decode geometry with feature metadata, return feature codes from scans, support dict-only mappings, transfer code buffers, and enforce request timeouts.
Memory-cap and feature-selection engine
packages/layers/src/engine/..., packages/layers/tests/pointsDataEngine.spec.ts
PointsDataEngine tracks capped resident data, catalogs, row codes, matching scans, progressive results, cancellation, reuse, and code-space reconciliation.
Feature-coded rendering
packages/layers/src/points*.ts, packages/layers/tests/pointsFeatureColor*.spec.ts, packages/layers/tests/pointsRenderAttributes.spec.ts
Rendering now builds memoized binary attributes, preserves feature codes through filtering and caps, and applies deterministic CPU/GPU feature colors.
SpatialCanvas integration
packages/vis/src/SpatialCanvas/..., packages/vis/tests/pointsFeatureRowState.spec.ts
SpatialCanvas exposes feature state, memory-cap controls, feature filtering, matching-load status, row diagnostics, and worker-backed point rendering.
Parquet capability and roadmap documentation
docs/...
Documents parquet-wasm metadata and decoding limitations, a worker catalog performance finding, and the points redesign punch list.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SpatialCanvas
  participant PointsDataEngine
  participant VPointsSource
  participant pointsWorkerClient
  participant points-worker
  SpatialCanvas->>PointsDataEngine: ensureMatchingFeaturesLoaded
  PointsDataEngine->>VPointsSource: loadPointsMatchingFeatureCodesByChunk
  VPointsSource->>pointsWorkerClient: scanParquetByFeatureCodesInWorker
  pointsWorkerClient->>points-worker: feature-code scan request
  points-worker-->>pointsWorkerClient: matched geometry, featureCodes, progress
  pointsWorkerClient-->>VPointsSource: scan chunk
  VPointsSource-->>PointsDataEngine: progressive partialResult
  PointsDataEngine-->>SpatialCanvas: matching resource and load state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main changes: points feature filtering and off-thread loading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch points-feature-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

xinaesthete and others added 23 commits July 7, 2026 13:57
Investigating the feature-primary index (skip row groups whose feature range
doesn't overlap the selected genes) surfaced what the vendored Vitessce
parquet-wasm build actually exposes at runtime vs its narrow .d.ts:

- ColumnChunkMetaData exposes fileOffset/columnPath/compressedSize (column-chunk
  OFFSETS are available — the limitations doc was too pessimistic), but
- .statistics() does NOT exist — per-row-group min/max are unreachable.

So the feature index is blocked: picking the ~3 of 245 row groups a gene lives in
needs per-row-group feature_name_codes min/max, which the wasm won't give, and
reading them via row reads fetches whole row groups (the entire 449MB file to
index). Records the four ways to unblock (footer Thrift parse / hyparquet /
extend the wasm / writer sidecar index) in docs/parquet-wasm-limitations.md.

Also confirmed the data facts: plain `transcripts` is NOT feature-sorted (every
row group spans all genes) so it can't be fast-filtered at all; the
`transcripts_feature_then_morton` variant (12.16M rows, 245 row groups, single
file, has feature_name_codes) IS feature-ordered — a single gene touches ~3 row
groups — so it's the target once stats are reachable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mn min/max

Recovers per-row-group column Statistics (min/max) from the parquet footer's
Thrift-encoded FileMetaData — the values the vendored parquet-wasm build exposes
in ColumnChunkMetaData for offsets but NOT for statistics
(docs/parquet-wasm-limitations.md). A focused Thrift Compact Protocol reader
walks FileMetaData -> RowGroup -> ColumnChunk -> ColumnMetaData -> Statistics,
skipping everything else, and returns per-row-group {path, physicalType,
minValue, maxValue} plus decodeIntStat for integer columns.

This is the primitive for the feature-primary index: a feature-ordered points
file's per-row-group feature_name_codes min/max lets us skip the row groups that
can't contain the selected genes (one gene lives in ~3 of 245 row groups).

Unit-tested against a real footer (sorted codes, 2 rows/group) — recovers 4 row
groups with feature_name_codes ranges [0,0],[1,1],[2,2],[3,3] and string min/max.
Not yet wired into the scan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lter UX

Decouple the feature catalog from the resident batch and fix the reactivity
and render glitches the previous wiring exposed on feature-ordered files.

- Full-dataset catalog with counts. The geometry preload's catalog reflects
  only the resident batch, so on a feature-ordered file it listed just the
  loaded slice (145 of 541). It is now an instant *preview* superseded by the
  full-dataset `listFeaturesWithCounts` scan (`catalogComplete`), so the panel
  shows all features with per-feature counts and sorts by count.

- Reliable late-update reactivity. Re-render on engine cache mutations via
  `useSyncExternalStore` against a monotonic engine `version`, replacing the
  subscribe -> bump-a-counter -> pull-during-render effect that dropped late
  async completions (the full-catalog scan lands tens of seconds after the
  preload), leaving the panel stuck on the preview.

- No flash of all points on toggle. `resolveScatterBatch` fell back to the
  unfiltered batch whenever the filter signature was stale, so every toggle
  flashed every feature until the off-thread re-filter finished. Keep showing
  the previous *filtered* result instead, and never reuse the unfiltered batch
  once a selection is active.

- Honest resident-subset UX. Grey features absent from the loaded batch (with
  a count + hint) so selecting one that renders no points reads as "not loaded
  yet" rather than a glitch, and show a "loading the full feature list" hint
  while the scan refines the preview. Backed by new engine queries
  `getResidentFeatureCodes` / `isFeatureCatalogRefining`.

Note: on-demand loading of a selected non-resident feature is still unwired --
the render only filters the resident window. The feature-index render scan
(footer-stats row-group skipping) is the next piece.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emand)

Selecting a feature that wasn't in the resident preload window previously
rendered nothing (the render only filtered the first ~4M file-order rows). Wire
the whole-dataset scan so a selection loads exactly its points, fast, by
exploiting the feature-primary index.

- Footer-stats row-group skip. `loadPointsMatchingFeatureCodes` now reads each
  row group's `feature_name_codes` min/max from the part footers (via
  `parquetFooterStats`, no extra fetch — the bytes are already resident in the
  dataset metadata) and skips every row group whose range can't contain a
  selected code. For a feature-ordered file a gene lives in a handful of row
  groups, so it touches almost nothing; files without usable stats fall back to
  a full scan. Verified live: MALL (646,132 pts, absent from the resident set)
  loads in ~1s scanning ~700k of 12.16M rows.

- Exposed publicly on `PointsElement.loadPointsMatchingFeatureCodes`.

- Engine caches the matched batch keyed by the selection signature
  (`ensureMatchingFeaturesLoaded` / `getMatchingResource` / `isMatchingLoading`),
  builds a stable render resource from it, and reloads when the selection
  changes. The "surface loading" notify is deferred to a microtask because the
  scan is kicked from `getLayers` during render.

- `getLayers`: when a selection is active, render the whole-dataset matched
  batch (already filtered), showing the resident-filtered subset as an instant
  preview until the scan settles; the resident batch is the default when nothing
  is selected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the root cause of engine-driven UI updates never repainting, and add the
progressive load-state UX the fix unblocks.

Root cause: the React Compiler (enabled on `@spatialdata/vis`) memoizes the JSX
`SpatialCanvasInner` builds from engine getters (feature catalog, matching load
state, deck layers). Those getters read mutable engine state with no
compiler-tracked dependency, so a late async settle (catalog scan finishing,
feature-index scan progress) bumped the engine but the compiler reused stale
memoized JSX — the panel stayed frozen until the next interaction. This was the
"stuck at 145", "progress stuck at 0", and "features only load in when I click
around" behaviour throughout.

- Opt `SpatialCanvasInner` out with `'use no memo'`. Confirmed live: the catalog
  now auto-updates 145 → 541 and matched-point counts stream live with zero
  interaction, with the compiler still enabled everywhere else. A prior
  `useSyncExternalStore` here was mis-attributed as the fix and is reverted to a
  plain subscribe → counter (the primitive was never the problem).

- Progressive load stats. The feature-index scan reports `onProgress`; the engine
  tracks throttled matched/scanned counts and exposes `getMatchingLoadState`, so
  the panel shows "Loading selected features… N points so far" climbing live and
  then "N points loaded for this selection" — instead of waiting for the whole
  scan. Verified: MALL + CYP2B6 → 1,214,995 points, streamed and rendered.

- Reword the greyed-feature note now that selecting a non-resident feature loads
  it on demand rather than showing nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oaded features

Two fixes to the feature-index scan UX.

- No blank mid-scan. Changing the selection kicked a new whole-dataset scan and,
  until it settled, `getLayers` fell back to the resident-filtered batch — empty
  for a non-resident selection — so all points vanished for the (multi-second)
  scan. `getMatchingResource` now returns the LAST completed matched batch
  regardless of the current selection, so the render keeps showing the previous
  selection's points until the new scan lands. The scan applies its result only
  if it's still the latest requested (guards rapid selection changes), and
  re-selecting a resident selection abandons a superseded in-flight scan so it
  can't clobber the batch.

- Un-grey loaded features. Rows were greyed purely by resident-vs-not, so a
  non-resident feature stayed grey even after its scan loaded it. A feature is now
  "loaded" (not greyed) if it is resident OR part of the current selection whose
  scan has settled — so it un-greys once loaded and re-greys when deselected.
  Verified live: MALL (non-resident) greys → select → loads → un-greys → deselect
  → greys again; switching MALL→CYP2B6 keeps MALL's points on screen until
  CYP2B6's 568,863 land.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…batch

Foundation for colour-by-feature. Per-point feature codes were computed at
every source (loadPoints, worker filter) but dropped at the render-batch
boundary, so the GPU never saw them. Thread them through the resident path so
the render batch carries a row-aligned featureCodes array — no behavior change.

- ColumnarNdarrayPointsBatch (core + layers) gains optional featureCodes.
- PointsColumnarData + the columnar worker result carry featureCodes; the
  worker filter (handleFilterColumnar) forwards them.
- filterColumnarByFeatureCodes returns the kept subset's codes (and surfaces
  aligned source codes on the no-filter / all-kept paths).
- applyRenderCapToColumnar truncates featureCodes in lockstep with geometry.
- Layers: filterPreloadedBatch keeps filtered codes; the unfiltered and
  transient cappedPreloaded paths attach preloadedFeatureCodes.

Non-resident matching-scan codes (feature-index scan) are deliberately deferred
to a follow-up (A2): the scan protocol omits codes and needs its own change.

Tests: filterColumnarByFeatureCodes code alignment (filter/empty/no-filter);
applyRenderCapToColumnar code truncation. core 140, layers 90, vis 42 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion)

Replace the per-object getPosition accessor in renderColumnarScatterLayer with a
GPU-ready binary attribute (deck.gl perf guide: optimize-accessors). New
buildPointsAttributes(batch, use3d) seam interleaves x/y/z into a single
Float32Array and exposes feature codes as a float attribute, both memoized on
the batch identity so a stable batch hands deck the same buffer every render
(no re-upload, no per-frame CPU pass). The featureCode attribute is built here
but not yet consumed — Stage C's colour extension reads it.

This seam is where worker-emitted interleaved chunks slot in when streaming
lands (Stage D): the worker produces these buffers directly and the batch
carries them, making buildPointsAttributes a pass-through.

Covers both the preloaded and Morton-tile scatter paths (shared function).
Verified live: points render identically, no deck errors. Tests: interleave,
z-handling, code conversion, per-batch memoization. layers 95, core 140.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Colours points by their per-point feature code entirely on the GPU — no CPU
colour pass, no palette buffer. PointsFeatureColorExtension adds a `featureCode`
instance attribute and a vertex-shader hook mapping code → golden-angle hue,
overwriting vFillColor. Toggled by a "Colour by feature" checkbox
(PointsLayerConfig.colorByFeature, serializable Stack-Entry state).

Verified live on transcripts_feature_then_morton: distinct per-feature colours
across the tissue; toggling off reverts to the flat fill; no shader errors.

Several deck/luma invariants were load-bearing (each cost real debugging; locked
in by pointsFeatureColorExtension.spec.ts and documented in the extension):
- Attach the extension to EVERY scatter layer, not lazily on toggle: deck only
  runs an extension's initializeState at first mount, so a late-attached
  extension never registers its attribute. Colour is gated by the attribute
  value instead (getFeatureCode binary present → colour; absent → -1 default →
  shader leaves the flat colour), with getFeatureCode:-1 as the constant prop so
  withdrawing the buffer reverts cleanly.
- `in float featureCode` must be declared in a top-level vs:#decl inject (deck
  does not auto-declare it; a module's own inject does not apply to the host).
- defaultProps.getFeatureCode must be an accessor or deck treats the attribute
  as constant and ignores the binary buffer (as DataFilterExtension does).

Threading: colorByFeature flows config → PointsLayer props → strategy →
renderColumnarScatterLayer (both matched and resident paths in useLayerData).

Tests: layers 98, vis 42, core 140.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…esident selections)

Three follow-ups to the colour-by-feature work:

- Always-on (per request): colour-by-feature now applies by default whenever the
  batch carries codes (opt-out via colorByFeature:false, reserved for future
  palette/highlight customisation). Removed the "Colour by feature" checkbox.

- Feature-list swatches: each row in the feature-selection panel shows a colour
  swatch matching the point colour. New featureCodeToRgb/featureCodeToCssColor
  (packages/layers/src/pointsFeatureColor.ts) is a JS mirror of the shader's
  golden-angle hue — kept in lockstep so UI and GPU agree.

- A2 — retain per-point codes through the feature-index scan (fixes the
  "everything turns salmon after selecting features" regression): selecting a
  feature routes through the non-resident scan, whose matched batch previously
  carried no codes, so colour fell back to the flat fill. The scan now collects
  matched-row codes (scanTableByFeatureCodes → scan handler → columnarScan
  result → chunk concat → matched batch featureCodes → render attribute).
  PointsWorkerScanResult no longer omits featureCodes; PreloadedColumnarInput /
  toColumnarBatch / the resolve-resource cache carry them onto the batch.

Verified live: colour shows with no toggle; swatches match points; selecting
MALL (non-resident, 646,132 pts via scan) renders in its purple feature colour,
not salmon. No errors. Tests: core 140, layers 102, vis 42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…'s settled state

Fix greying inconsistency: adding a feature greyed the already-loaded ones until
the new scan finished. `isLoaded` keyed off `matchingLoadState.settled` for the
*current* selection, which flips to false the instant a new scan starts — so all
non-resident selected features greyed mid-scan, even though their points stay on
screen (getMatchingResource keeps the last-completed batch).

Now grey against what's actually rendered: new
PointsDataEngine.getLoadedMatchingFeatureCodes(key) returns the last-completed
matched selection's codes (parsed from entry.matching.signature), threaded to the
panel as `loadedMatchingCodes`. A feature is un-greyed if resident or in that set,
independent of any in-flight scan.

Verified live: with MALL loaded, adding TCIM keeps MALL at opacity 1 throughout
TCIM's scan (only TCIM greys); both settle to 1.

Also: the new getter is a PLAIN function, not useCallback. useLayerData is
processed by the React Compiler (no 'use no memo'), and adding a real hook to it
perturbed the compiled hook sequence — a Rules-of-Hooks violation (verified: the
useCallback form errored, the plain-function form is clean). The getter is only
called inline in the panel's render, so referential stability is unneeded.

Tests: layers 102, vis 42. No console errors on fresh reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ritative code space

On a plain `transcripts` element (dictionary-only feature_name, no
feature_name_codes column), selecting anything other than "all features"
emptied the render and never recovered. Three compounding defects, all
specific to app-assigned (non-file-backed) feature codes:

1. The whole-dataset feature-index scan derived EMPTY per-row codes for
   dict-only data (no featureCodeByName map threaded), so it matched 0 rows.
2. A 0-row matched batch still superseded the resident preview via
   getMatchingResource, locking the view empty with no recovery.
3. The resident-preview catalog and the full-dataset catalog each assign
   codes by first-seen order, so the same gene could get different codes in
   the panel's selection space vs. the render's per-row codes.

Fix (authoritative code space + guard + scan gate):
- PointsLoadResult.hasFeatureCodeColumn: set on ALL three loadPoints return
  paths (both worker-decode paths + main-thread fallback) — the worker path
  is the common one, so missing it there disabled the scan for real
  feature-indexed stores too.
- remapRowFeatureCodes(): translate row codes fromCatalog→toCatalog by name.
  The engine reconciles resident row codes into the full catalog's space on
  upgrade (reconcileRowCodes), keeping colour/filter/greying aligned with the
  panel's selection. No-op for authoritative file-backed codes.
- Empty-lock guard: getMatchingResource returns null for a 0-row matched
  batch, falling back to the resident filter instead of locking empty.
- Gate the whole-dataset scan on hasFeatureCodeColumn: dict-only datasets use
  in-memory resident filtering (correct within the preload cap); indexed
  stores keep the row-group-skipping scan that reaches beyond the cap.
- Panel: honest greyed-features text — "no feature index … raise the cap or
  rewrite with one" for dict-only vs. "loads it on demand" for indexed.

Verified live: plain transcripts stays populated when deselecting a feature
and shows correct sparse points for a single-gene selection; feature-indexed
transcripts_feature_then_morton still loads a non-resident gene (MALL, 646k
points) via the scan. Tests: core 144, layers 107, vis 42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The resident-window row cap was hard-coded (4M). Expose it as a per-layer
control and raise the default.

- pointsLimits: DEFAULT_POINTS_MEMORY_CAP 4M → 8M, decoupled from
  POINTS_PRELOAD_MAX_ROWS (which stays 4M as the catalog-strategy heuristic).
  8M keeps margin under deck.gl's 24-bit picking ceiling (~16.7M objects/layer)
  and ~200MB/layer of attributes while doubling the resident window.
- PointsLayerConfig.pointsMemoryCap: serializable per-layer override.
- Props panel: a discrete Memory cap <select> (1M/2M/4M/8M/16M) — one reload
  per choice, vs. a free number input that would reload on every keystroke.
- PointsDataEngine.ensureLoaded(target, memoryCap): reloads the resident window
  when the cap changes, dropping cap-dependent state (geometry, row codes,
  render resource, matched selection) but PRESERVING the full-dataset catalog so
  a cap change never triggers its ~30s re-scan. isLoadedWithCap() lets the host
  schedule the reload; loadPoints/ensureMatchingFeaturesLoaded get the cap.

Verified live on transcripts_feature_then_morton (~12M rows): 8M→16M loads the
full dataset (denser render), 16M→4M shrinks it and re-greys non-resident
features, both reversible with no console errors, catalog stays at 541
throughout. Tests: core 144, layers 110, vis 42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ture states

Removing a feature from a matched (scanned) selection used to re-scan the whole
dataset from parquet — even though the remaining features' rows were already in
the loaded batch — so the just-removed feature dropped and everything flashed
through "loading". That asymmetry (resident features filter in memory; matched
features re-fetch on any change) is what made the greying feel jarring.

- Engine: `ensureMatchingFeaturesLoaded` now REUSES the matched batch whenever
  the new selection ⊆ the codes it already covers (the removal fast path). No
  scan; the render filters the batch in memory. A scan runs only when the
  selection adds a code no loaded/in-flight batch covers. `getMatchingRowFeatureCodes`
  exposes the batch's per-row codes for that filter.
- Vis: the matched layer is passed the current selection + the batch's row codes,
  so it filters in the layer (like resident filtering). Skipped when the
  selection equals what was scanned (render whole).
- Panel: a single `describeFeatureRowState` classifier drives both the dimming
  and a multi-line diagnostic tooltip (state + reason + the raw resident/rendered/
  selected/scan signals that decided it). A deselected-but-loaded feature is now
  `in memory` (crisp, not greyed) — re-adding it is instant. Distinct opacity for
  `loading`. `covered` load-state so the "points in memory" line doesn't vanish
  on a removal.

Verified live (transcripts_feature_then_morton): selected 3 non-resident features
(scan), removed one → NO re-scan, the removed feature stays opacity 1 with tooltip
"in memory … re-adding it is instant", the other two stay on screen. Tests: layers
112, vis 50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pfc_codeToColor now sweeps the golden angle through OKLCh (fixed L=0.72,
  C=0.128) via an OKLab→sRGB path, instead of HSV. OKLab spaces hues
  perceptually evenly, so adjacent codes read as distinct as they are
  numerically (HSV's wide green arc collapsed many codes to "green"). The JS
  swatch mirror is updated in exact lockstep (same matrices, gamma, L/C).
- Add a `highlightFeatureCode` uniform (deck shader module + setShaderModuleProps
  in draw). When >= 0, points of other codes are desaturated toward luminance
  and dimmed. The uniform stores code+1 so the "off" state is 0 — which a
  zeroed/unbound UBO also reads, keeping the default a safe no-op. Prop wiring to
  config/UI is left for a follow-up.

Verified live: points render in varied OKLCh colours (no shader-compile errors),
swatches match, and the default highlight (-1) does not dim anything.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nge, honest labels

- Revert DEFAULT_POINTS_MEMORY_CAP 8M → 4M. On an UNINDEXED (dict-only, multipart)
  dataset the preload fetches WHOLE parts and only stops after accumulating the
  cap, so 8M pulled ~2× the bytes into the worker decode → OOM/hang → main-thread
  fallback OOM → tab crash. 4M is the safe default; higher caps stay selectable in
  the panel for indexed (row-group range-read) datasets where they're cheap.
- Abort superseded loads: PointsLoadOptions.signal, checked in loadPoints —
  notably BEFORE the expensive main-thread fallback (the crash seam) and
  re-thrown out of the worker catch so an abort isn't swallowed into a fallback.
  PointsDataEngine holds an AbortController per load and aborts it on a cap
  change; an aborted load stays quiet (no error status).
- Label: points geometry loading now reads "(worker)" not "(blocking)" — it
  decodes off the main thread; main-thread types keep "(blocking)".
- Feature tooltip: correct the resident reason — "shown by filtering the
  in-memory batch (no scan; a large batch can still take a moment)" instead of
  claiming instant.

Tests: core 144, layers 113, vis 50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 0.37

The shader and the JS swatch mirror each hard-coded the OKLCh lightness/chroma,
so bumping the shader's chroma left the swatches washed-out (0.128) while the
points were vivid — they'd silently drift. Now `pointsFeatureColor.ts` exports
`PFC_LIGHTNESS` / `PFC_CHROMA` / `PFC_GOLDEN_RATIO_CONJUGATE` and the extension
interpolates those same values into its GLSL (via a small `glslFloat`), so a
tweak in one place re-colours both.

Chroma raised 0.128 → 0.37 for the vivid look of the old HSV palette. Note: sRGB
tops out around OKLCh chroma ~0.32, so at 0.37 most hues are out of gamut and get
hard-clamped to the sRGB boundary (vivid, some hue distortion); values past ~0.32
change little. Documented, plus a TODO that CSS `oklch()` could replace the CPU
swatch conversion once the shader gamut-maps the way the browser does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… still fits

Changing the memory cap wiped the matched (feature-scan) batch, so it always
rescanned — even lowering the cap, where the loaded selection already fits and
nothing new is needed.

Now `resetCapDependentState` preserves the matched batch (it only resets the
resident preload state), and `ensureMatchingFeaturesLoaded` reuses it when it
both covers the selection AND still satisfies the cap: a COMPLETE batch always
does; a TRUNCATED one does while the new cap doesn't ask for more rows than it
holds. So lowering the cap (e.g. 4M→2M with a selection under 2M) never rescans;
only raising the cap past a truncated batch does, to fetch the extra rows.

Tests: layers 115, vis 50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ncated batch

Any memory-cap change reloaded the resident preload from scratch, blanking the
view. Now the resident batch is treated like the matched one:

- ensureLoaded reuses the loaded batch when it already satisfies the cap
  (isLoadedWithCap now uses batchAdequateForCap): a COMPLETE batch satisfies any
  cap, a TRUNCATED one satisfies caps up to the rows it holds. So lowering the
  cap never reloads, and raising it only reloads when the batch was truncated and
  more rows are actually wanted.
- On that raise-reload the previously-loaded data stays on screen (kept, then
  swapped atomically when the larger batch settles) instead of blanking.
- Track + surface truncation: PointsDataEngine.getResidentTruncation →
  getPointsResidentTruncation → a props-panel line ("Showing N of M points —
  capped; raise the cap for more" / "All N points loaded"), so the user can see
  when the resident window is a capped slice of the dataset.

Tests: layers 116, vis 50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes to the points feature-filter path:

1. Wild-type (dictionary-only) feature scan. Previously selecting a feature
   on a plain `transcripts` element only filtered the ~4M-row resident preload
   window in memory, so a gene's points beyond that window never rendered. Now
   the whole-dataset scan runs for dict-only elements too: the engine passes the
   catalog's name->code map (`featureCodeByName` -> worker `featureCodeEntries`)
   so the worker resolves each row's `feature_name` into the selection's code
   space. Routed dict-only scans through the parts path — the row-group projected
   read drops the dictionary column, and dict-only has no footer stats to skip on
   anyway. `supportsFeatureScan` gates on a real code column OR a loaded catalog.
   Verified live: ACE2 loads all 59,798 points across the full 12.2M-row dataset.

2. Shed resident rows to the cap on a lower. Lowering the memory cap below the
   resident row count now slices the batch down in memory (no re-fetch), so a 4M
   cap never keeps holding 8M rows. `isLoadedWithCap` reports work-needed when
   rows > cap; `ensureLoaded` slices via `sliceResidentBatch`.

Also: the props-panel truncation line is now selection-aware
(`getActiveTruncation`) — with a scanned selection it reports the matched batch
("Loaded all N matching points") instead of the stale resident "Showing 4M".

Includes a stray TODO comment in VTableSource (caching resolveParquetRowCount).

Tests: core 146, layers 120, vis 50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xinaesthete and others added 3 commits July 10, 2026 13:31
Replace the nine prop-drilled getPoints* getters (and useLayerData's
faked setLoadedDataRevision reactivity) with a single reactive hook.

useLayerData now exposes only the live pointsEngine plus a stable
resolvePointsTarget(layerId). A new PointsFeatureStateProvider puts the
engine + resolved target in context, and one consolidated hook
usePointsFeatureState(featureCodes?) runs a single useSyncExternalStore
against the engine version and returns every derived read (catalog,
loading flags, resident/matching codes, matching load state, truncation,
requestCatalog). PointsFeatureFilterPanel and ShowMatchingPoints consume
it; the provider + hook + type are exported for headless users.

The React Compiler can't see the engine's mutable method reads as
dependencies, so useSyncExternalStore alone isn't enough — the compiler
memoizes the stale JSX. Scoped 'use no memo' on the data hook and the two
leaf panels keeps them live; this retires the old canvas-wide escape
hatch on SpatialCanvasInner.

Provider pins the target's identity to key+element to avoid re-firing the
catalog-request effect (an ensureFeatureCatalog -> notify -> render loop).

Verified live on xenium transcripts_feature_then_morton: the mounted
panel flips not-loaded -> 541-feature list on its own as the scan settles;
selecting a non-resident gene updates the scan-progress and truncation
lines live. All four packages type-check; layers (120) + vis (50) tests
pass; Biome clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Refresh

PointsFeatureFilterPanel.tsx exported both the component and
describeFeatureRowState (a runtime function). That single non-component
runtime export disqualifies the module from Vite React Fast Refresh, so
every edit did a full reload and dropped React state (the selected
layer). Move the classifier + featureRowOpacity + their types to a
sibling featureRowState.ts; the panel and the test import from there.

(Only the runtime function had to move — type exports are erased and
don't affect Fast Refresh; the types moved with it for cohesion / to
avoid a circular import.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
packages/vis/src/SpatialCanvas/useLayerData.ts (1)

559-559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Leftover editing artifact.

//--- to be removed from here? reads as an unresolved note left in the source. Resolve or drop it before merge so the intent of the following engine block isn't ambiguous. Want me to open a tracking issue?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vis/src/SpatialCanvas/useLayerData.ts` at line 559, Remove the
leftover `//--- to be removed from here?` comment near the engine block in
`useLayerData`, or resolve it by replacing it with an intentional, descriptive
comment if clarification is required.
packages/layers/src/pointsScatterLayer.ts (1)

83-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop the as Float32Array by narrowing through a local.

colorByFeature already implies attributes.featureCodes !== undefined, but TS can't carry that through the derived boolean, so the assertion papers over it. Capturing the value in a local lets the compiler narrow it in the branch and removes the cast.

♻️ Suggested narrowing
-  const colorByFeature = props.colorByFeature !== false && attributes.featureCodes !== undefined;
+  const featureCodes = attributes.featureCodes;
+  const colorByFeature = props.colorByFeature !== false && featureCodes !== undefined;
@@
-        ...(colorByFeature
-          ? { getFeatureCode: { value: attributes.featureCodes as Float32Array, size: 1 } }
-          : {}),
+        ...(colorByFeature
+          ? { getFeatureCode: { value: featureCodes, size: 1 } }
+          : {}),

As per coding guidelines: "Avoid type assertions (as); use satisfies, as const, discriminated unions, and small helpers that return precise types."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layers/src/pointsScatterLayer.ts` around lines 83 - 95, Remove the
Float32Array assertion in the ScatterplotLayer construction by storing
attributes.featureCodes in a local variable and narrowing that value alongside
the colorByFeature condition. Update the conditional getFeatureCode attribute to
use the narrowed local directly, preserving the existing behavior without type
assertions.

Source: Coding guidelines

packages/layers/src/pointsFeatureColorExtension.ts (1)

69-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add explanatory comments for deck.gl boundary type assertions.

Per the coding guidelines, type assertions at external boundaries should include a short comment explaining why the compiler cannot prove the type. The three assertions here are at deck.gl's extension API boundary and are likely unavoidable, but the comments don't satisfy the guideline:

  • Line 71: The comment explains the runtime null/absence guard but not why the return type of super.getShaders() doesn't expose modules.
  • Line 129: No comment explaining why highlightFeatureCode isn't on this.props (deck.gl's Layer type doesn't reflect custom defaultProps in TypeScript).
  • Line 131: No comment explaining why setShaderModuleProps requires a double assertion (not in deck.gl's public type surface).
📝 Suggested comments
     // The base returns null, and the module list may be absent — guard both.
-    const shaders = (super.getShaders(extension) ?? {}) as { modules?: unknown[] };
+    // deck.gl's LayerExtension.getShaders return type doesn't expose `modules`
+    // as optional, so we assert the shape we know the runtime provides.
+    const shaders = (super.getShaders(extension) ?? {}) as { modules?: unknown[] };
   draw(this: Layer): void {
-    const highlight = (this.props as { highlightFeatureCode?: number }).highlightFeatureCode ?? -1;
+    // deck.gl's Layer props type doesn't reflect custom defaultProps in TS;
+    // assert the narrow shape we declared in defaultProps above.
+    const highlight = (this.props as { highlightFeatureCode?: number }).highlightFeatureCode ?? -1;
-    (this as unknown as { setShaderModuleProps(props: unknown): void }).setShaderModuleProps({
+    // setShaderModuleProps is a protected Layer method not in deck.gl's public
+    // type declarations; double-cast to reach it.
+    (this as unknown as { setShaderModuleProps(props: unknown): void }).setShaderModuleProps({

Also applies to: 128-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layers/src/pointsFeatureColorExtension.ts` around lines 69 - 71,
Update the assertions in getShaders and the related shader-property handling
around highlightFeatureCode and setShaderModuleProps with concise comments
explaining the deck.gl type-boundary limitations: super.getShaders() does not
expose modules despite the runtime shape, Layer.props omits custom defaultProps
such as highlightFeatureCode, and setShaderModuleProps is unavailable in
deck.gl’s public type surface, requiring the double assertion.

Source: Coding guidelines

packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx (1)

287-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an accessible label to the search input.

The search <input> at line 288 has only a placeholder, which is not a substitute for an accessible label. Screen readers may not announce the field's purpose. Adding aria-label="Search features" is a one-line fix.

♿ Proposed fix
       <input
         type="search"
         placeholder="Search features…"
         value={searchQuery}
         onChange={(event) => setSearchQuery(event.target.value)}
+        aria-label="Search features"
         style={searchStyle}
       />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx` around lines 287
- 295, Add an accessible label to the search input rendered by the showSearch
branch in PointsFeatureFilterPanel by adding aria-label="Search features"
alongside its existing type, placeholder, value, and onChange props.
packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx (1)

71-99: 📐 Maintainability & Code Quality | 🔵 Trivial

Offer to help: t.loaded display semantics are known to be misleading.

The NOTE at lines 79–81 correctly flags that t.loaded is the covered-batch size, not the count matching the current selection. This means the "Showing X of Y" message can overstate what's actually rendered for a subset selection. Would you like me to open an issue or draft a fix that threads the actual matching count through getActiveTruncation?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx` around lines 71 - 99,
Update ShowMatchingPoints and the engine’s getActiveTruncation filtered branch
to expose the actual count of points matching the current selection, rather than
the covered-batch size in t.loaded; use that matching count for the rendered
“Showing” and “Loaded all” messages while preserving total and truncation
behavior, and remove the obsolete NOTE.
packages/layers/src/engine/PointsDataEngine.ts (1)

234-251: 📐 Maintainability & Code Quality | 🔵 Trivial

Type assertion in sliceArray would benefit from a short justifying comment.

The as unknown as { slice?: ... } cast is local and boundary-motivated (duck-typing an ArrayLike<number> that may or may not have a native .slice), but per guidelines it should carry a brief comment explaining why the compiler can't prove this instead of relying on the reader to infer it.

As per coding guidelines: "if an assertion is unavoidable at an external boundary, keep it local and add a short comment explaining why the compiler cannot prove it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layers/src/engine/PointsDataEngine.ts` around lines 234 - 251, The
local double assertion in sliceResidentBatch’s sliceArray helper lacks
justification. Add a brief comment immediately before the cast explaining that
ArrayLike<number> may optionally provide a native slice method, which TypeScript
cannot express or verify, while keeping the duck-typed assertion local.

Source: Coding guidelines

packages/core/src/pointsLimits.ts (1)

55-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Undocumented type assertions in capFeatureCodes.

Both the subarray cast and the slice.call(...) as ArrayLike<number> are reasonable here (no TS type captures "ArrayBufferView with subarray"), but per the coding guideline these boundary assertions should carry a short comment explaining why the compiler can't prove it.

♻️ Optional: document the assertions
   if (ArrayBuffer.isView(featureCodes) && 'subarray' in featureCodes) {
+    // TS's ArrayBufferView type doesn't expose `subarray` (only typed arrays
+    // do, not DataView), so the runtime `in` check can't be reflected in the
+    // type system without this local assertion.
     return (featureCodes as { subarray(begin: number, end: number): ArrayLike<number> }).subarray(
       0,
       count
     );
   }

As per coding guidelines: "if an assertion is unavoidable at an external boundary, keep it local and add a short comment explaining why the compiler cannot prove it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/pointsLimits.ts` around lines 55 - 72, Add brief comments
directly above both type assertions in capFeatureCodes, explaining that
TypeScript cannot express the narrowed ArrayBufferView with subarray and cannot
infer the ArrayLike<number> result of Array.prototype.slice.call; keep the
assertions local and unchanged.

Source: Coding guidelines

packages/core/src/models/VPointsSource.ts (1)

359-452: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoidable type assertion for featureKey.

configuredFeatureKey as string at line 367 can't be proven by the compiler because the narrowing check lives in a separate wantFeatures boolean rather than the ternary condition itself. Inlining the check removes the assertion entirely.

♻️ Proposed fix
-    const configuredFeatureKey = zattrs.spatialdata_attrs?.feature_key;
-    const wantFeatures =
-      options.includeFeatureCodes === true &&
-      typeof configuredFeatureKey === 'string' &&
-      configuredFeatureKey.length > 0;
-    const featureKey = wantFeatures ? (configuredFeatureKey as string) : undefined;
+    const configuredFeatureKey = zattrs.spatialdata_attrs?.feature_key;
+    const featureKey =
+      options.includeFeatureCodes === true &&
+      typeof configuredFeatureKey === 'string' &&
+      configuredFeatureKey.length > 0
+        ? configuredFeatureKey
+        : undefined;

Rest of the reworked preload/fallback logic (worker path, abort guard, catch handling) looks correct.

As per coding guidelines: "Avoid type assertions (as ...) in TypeScript when a library overload, local type guard... can express the same fact."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/models/VPointsSource.ts` around lines 359 - 452, Remove the
unnecessary configuredFeatureKey type assertion in the featureKey
initialization. Inline the includeFeatureCodes and nonempty-string checks
directly in the ternary so TypeScript narrows configuredFeatureKey to string,
while preserving the existing wantFeatures behavior used by the surrounding
preload logic.

Source: Coding guidelines

packages/core/src/models/VTableSource.ts (1)

438-438: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache loadParquetDatasetMetadata to eliminate the repeated 404 probing.

The comment correctly identifies the symptom (points.parquet/points.4.parquet 404s), but the root cause is that loadParquetDatasetMetadata itself isn't memoized — every call re-runs part discovery (loadParquetPartMetadata looped until a 404) from scratch. Given this PR adds many new call sites into points loading (loadPoints, loadPointsMatchingFeatureCodes, listPointsFeatures, tiling metadata, feature counts, etc.), the redundant round-trips compound. The class already has a precedent for this pattern (parquetTableCache: Record<string, Promise<ArrowTable>>).

Want me to add a Record<string, Promise<ParquetDatasetMetadata | null>> cache keyed by parquetPath (analogous to parquetTableCache), or open an issue to track it?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/models/VTableSource.ts` at line 438, Memoize
loadParquetDatasetMetadata using a class-level Record<string,
Promise<ParquetDatasetMetadata | null>> keyed by parquetPath, following the
existing parquetTableCache pattern. Store and reuse the in-flight promise so
concurrent callers share one part-discovery sequence, including repeated 404
probing, while preserving the existing null/error behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/parquet-wasm-limitations.md`:
- Around line 78-83: Revise the limitation describing column-chunk offsets to
clarify that the normalized wrapper exposes these offsets as a prerequisite for
a future sparse reader, but offsets alone do not enable projected fetching.
Retain that readParquetRowGroup currently requires contiguous row-group bytes
and that concatenating selected chunks invalidates footer offsets; apply the
same clarification to the corresponding repeated section.

In `@docs/plans/points-mvp-and-roadmap.md`:
- Around line 136-149: Update the roadmap section’s header/status to indicate
that implementation is in progress, or explicitly clarify that “not started”
applies only to the roadmap plan; ensure it no longer contradicts the documented
current worker behavior and ongoing performance work.

In `@packages/layers/src/engine/PointsDataEngine.ts`:
- Around line 694-709: Move the JSDoc describing resident distinct feature codes
from above hasFeatureCodeColumn to directly above getResidentFeatureCodes,
preserving its content. Keep the existing hasFeatureCodeColumn documentation
immediately above that method and ensure each comment documents only its
corresponding method.
- Around line 30-36: Update ensureRowFeatureCodes to accept a memory-cap
argument and pass entry.memoryCap from the fallback call site, ensuring it uses
the same resident window as ensureLoaded. Locate the relevant invocation in the
PointsDataEngine loading path and thread the value through all affected helper
signatures and calls.

In `@packages/vis/src/SpatialCanvas/featureRowState.ts`:
- Around line 89-111: Update the feature-row state logic so the selected,
non-scanning, non-resident, non-rendered, on-demand-load case does not use the
misleading “select it to fetch” reason. In the function containing the
selected/scanning and notLoaded branches, add a selected-specific pending-scan
branch with loading or equivalent pending tone/label and wording that indicates
the feature is already selected and its feature-index scan is about to begin;
keep the existing notLoaded message only for unselected features.

---

Nitpick comments:
In `@packages/core/src/models/VPointsSource.ts`:
- Around line 359-452: Remove the unnecessary configuredFeatureKey type
assertion in the featureKey initialization. Inline the includeFeatureCodes and
nonempty-string checks directly in the ternary so TypeScript narrows
configuredFeatureKey to string, while preserving the existing wantFeatures
behavior used by the surrounding preload logic.

In `@packages/core/src/models/VTableSource.ts`:
- Line 438: Memoize loadParquetDatasetMetadata using a class-level
Record<string, Promise<ParquetDatasetMetadata | null>> keyed by parquetPath,
following the existing parquetTableCache pattern. Store and reuse the in-flight
promise so concurrent callers share one part-discovery sequence, including
repeated 404 probing, while preserving the existing null/error behavior.

In `@packages/core/src/pointsLimits.ts`:
- Around line 55-72: Add brief comments directly above both type assertions in
capFeatureCodes, explaining that TypeScript cannot express the narrowed
ArrayBufferView with subarray and cannot infer the ArrayLike<number> result of
Array.prototype.slice.call; keep the assertions local and unchanged.

In `@packages/layers/src/engine/PointsDataEngine.ts`:
- Around line 234-251: The local double assertion in sliceResidentBatch’s
sliceArray helper lacks justification. Add a brief comment immediately before
the cast explaining that ArrayLike<number> may optionally provide a native slice
method, which TypeScript cannot express or verify, while keeping the duck-typed
assertion local.

In `@packages/layers/src/pointsFeatureColorExtension.ts`:
- Around line 69-71: Update the assertions in getShaders and the related
shader-property handling around highlightFeatureCode and setShaderModuleProps
with concise comments explaining the deck.gl type-boundary limitations:
super.getShaders() does not expose modules despite the runtime shape,
Layer.props omits custom defaultProps such as highlightFeatureCode, and
setShaderModuleProps is unavailable in deck.gl’s public type surface, requiring
the double assertion.

In `@packages/layers/src/pointsScatterLayer.ts`:
- Around line 83-95: Remove the Float32Array assertion in the ScatterplotLayer
construction by storing attributes.featureCodes in a local variable and
narrowing that value alongside the colorByFeature condition. Update the
conditional getFeatureCode attribute to use the narrowed local directly,
preserving the existing behavior without type assertions.

In `@packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx`:
- Around line 287-295: Add an accessible label to the search input rendered by
the showSearch branch in PointsFeatureFilterPanel by adding aria-label="Search
features" alongside its existing type, placeholder, value, and onChange props.

In `@packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx`:
- Around line 71-99: Update ShowMatchingPoints and the engine’s
getActiveTruncation filtered branch to expose the actual count of points
matching the current selection, rather than the covered-batch size in t.loaded;
use that matching count for the rendered “Showing” and “Loaded all” messages
while preserving total and truncation behavior, and remove the obsolete NOTE.

In `@packages/vis/src/SpatialCanvas/useLayerData.ts`:
- Line 559: Remove the leftover `//--- to be removed from here?` comment near
the engine block in `useLayerData`, or resolve it by replacing it with an
intentional, descriptive comment if clarification is required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 840748e6-c2f7-4cdc-a99a-062385e13dd7

📥 Commits

Reviewing files that changed from the base of the PR and between ab1b809 and e2de7d2.

📒 Files selected for processing (50)
  • docs/parquet-wasm-limitations.md
  • docs/plans/points-mvp-and-roadmap.md
  • packages/core/src/index.ts
  • packages/core/src/models/VPointsSource.ts
  • packages/core/src/models/VTableSource.ts
  • packages/core/src/models/index.ts
  • packages/core/src/parquetFooterStats.ts
  • packages/core/src/pointsFeatures.ts
  • packages/core/src/pointsLimits.ts
  • packages/core/src/pointsLoadOptions.ts
  • packages/core/src/pointsLoader.ts
  • packages/core/src/pointsTiling.ts
  • packages/core/src/spatialViewFit.ts
  • packages/core/src/workers/index.ts
  • packages/core/src/workers/points-worker.ts
  • packages/core/src/workers/pointsWorkerClient.ts
  • packages/core/src/workers/pointsWorkerProtocol.ts
  • packages/core/src/workers/pointsWorkerScan.ts
  • packages/core/tests/parquetFooterStats.spec.ts
  • packages/core/tests/pointsFeatures.spec.ts
  • packages/core/tests/pointsLimits.spec.ts
  • packages/core/tests/pointsTiling.spec.ts
  • packages/core/tests/pointsWorker.spec.ts
  • packages/core/tests/pointsWorkerScan.spec.ts
  • packages/core/tests/remapRowFeatureCodes.spec.ts
  • packages/layers/src/PointsLayer.ts
  • packages/layers/src/engine/PointsDataEngine.ts
  • packages/layers/src/index.ts
  • packages/layers/src/pointsFeatureColor.ts
  • packages/layers/src/pointsFeatureColorExtension.ts
  • packages/layers/src/pointsLoader.ts
  • packages/layers/src/pointsRenderAttributes.ts
  • packages/layers/src/pointsScatterLayer.ts
  • packages/layers/src/preloadedScatterStrategy.ts
  • packages/layers/src/resolvePointsRenderResource.ts
  • packages/layers/tests/pointsDataEngine.spec.ts
  • packages/layers/tests/pointsFeatureColor.spec.ts
  • packages/layers/tests/pointsFeatureColorExtension.spec.ts
  • packages/layers/tests/pointsRenderAttributes.spec.ts
  • packages/layers/tsconfig.json
  • packages/vis/demo/src/main.tsx
  • packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx
  • packages/vis/src/SpatialCanvas/PointsFeatureState.tsx
  • packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx
  • packages/vis/src/SpatialCanvas/featureRowState.ts
  • packages/vis/src/SpatialCanvas/index.tsx
  • packages/vis/src/SpatialCanvas/public.ts
  • packages/vis/src/SpatialCanvas/types.ts
  • packages/vis/src/SpatialCanvas/useLayerData.ts
  • packages/vis/tests/pointsFeatureRowState.spec.ts

Comment on lines +78 to +83
- **Column-chunk *offsets* ARE available** (`column(j).fileOffset()` +
`compressedSize()` + `columnPath()`). A projected byte range per column is
computable. What still blocks a projected *fetch* is #804: `readParquetRowGroup`
needs the *contiguous* row-group bytes, and hand-concatenating a subset of column
chunks breaks the footer offsets — so we can compute the ranges but not feed a
sparse buffer back in for decode.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify that column offsets alone are not sufficient for projected fetching.

The runtime probe above says column offsets are already available, yet the current decoder still requires contiguous row-group bytes. Reword this item as exposing offsets through the normalized wrapper—a prerequisite for a future sparse reader, not something that independently unlocks projected fetching.

Suggested wording
-1. **Column-chunk offsets in the metadata** — expose `ColumnChunkMetaData`
+1. **Column-chunk offsets in the normalized metadata** — expose `ColumnChunkMetaData`
   (`file_offset`, `total_compressed_size`, `data_page_offset`,
-  `dictionary_page_offset`) so we can compute per-column byte ranges. This alone
-  unlocks projected fetching.
+  `dictionary_page_offset`) so callers can compute per-column byte ranges.
+  This is a prerequisite for projected fetching, but still requires a decoder
+  that accepts sparse column-chunk buffers.

Also applies to: 100-107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/parquet-wasm-limitations.md` around lines 78 - 83, Revise the limitation
describing column-chunk offsets to clarify that the normalized wrapper exposes
these offsets as a prerequisite for a future sparse reader, but offsets alone do
not enable projected fetching. Retain that readParquetRowGroup currently
requires contiguous row-group bytes and that concatenating selected chunks
invalidates footer offsets; apply the same clarification to the corresponding
repeated section.

Comment on lines +136 to +149
**Finding (2026-07-06, live on a real Xenium `transcripts`):** the current
worker catalog path (`readParquetWorkerPayload` with `fullPartsForFallback` →
`scanParquetFeatureCatalogInWorker`) fetches the **entire** parquet file
before scanning, whereas the main-thread path does a *projected* single-column
range read of just the feature column. For a transcripts element with **no
`{feature_key}_codes` column** (so the cheap row-group *dictionary-page* scan
can't run), enabling the worker regressed catalog build from ~20s to >150s.
The request timeout in `pointsWorkerClient` (added this cycle) makes a silent
worker fall back safely, but does **not** fix this — the fetch is before the
worker call. **Next perf task:** give the worker a *projected/dictionary-only*
payload path (fetch only the feature column, or read dictionary pages) so
worker-offload is a win, not a regression — only then enable the worker in the
demo for catalog building. Until then the demo keeps the (blocking but faster)
main-thread path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the roadmap status now that implementation work is underway.

This section documents live behavior from the current worker implementation, which conflicts with the header’s “implementation not started” status. Change the status to “in progress” or explicitly scope that status to the roadmap plan rather than the implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/points-mvp-and-roadmap.md` around lines 136 - 149, Update the
roadmap section’s header/status to indicate that implementation is in progress,
or explicitly clarify that “not started” applies only to the roadmap plan;
ensure it no longer contradicts the documented current worker behavior and
ongoing performance work.

Comment on lines +30 to +36
* Alignment invariant (load-bearing): `getRowFeatureCodes(key)` is row-aligned
* with the resident batch from `ensureLoaded`. Both the geometry preload
* (`element.loadPoints()`) and the row codes (`element.loadRowFeatureCodes()`)
* read the first `min(rowCount, memoryCap)` rows in *file order* under the same
* default memory cap, so index i in the codes array names the feature of point i
* in the batch. If a configurable memory cap is ever threaded, it MUST go to
* both calls identically or the filter mask will be misaligned.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
git ls-files 'packages/layers/src/engine/PointsDataEngine.ts' 'packages/core/src/models/VPointsSource.ts' 'packages/core/src/**' | sed -n '1,200p'

echo '--- PointsDataEngine outline ---'
ast-grep outline packages/layers/src/engine/PointsDataEngine.ts --view expanded || true

echo '--- VPointsSource outline ---'
ast-grep outline packages/core/src/models/VPointsSource.ts --view expanded || true

echo '--- Search for loadRowFeatureCodes/loadPointsRowFeatureCodes/ensureLoaded/memoryCap ---'
rg -n "loadRowFeatureCodes|loadPointsRowFeatureCodes|ensureLoaded|memoryCap" packages/layers/src/engine/PointsDataEngine.ts packages/core/src/models/VPointsSource.ts packages/core/src -g '!**/dist/**' -g '!**/build/**' || true

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 15036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant sections with line numbers
echo '--- PointsDataEngine.ts (selected lines) ---'
nl -ba packages/layers/src/engine/PointsDataEngine.ts | sed -n '1,220p'

echo '--- VPointsSource.ts (selected lines) ---'
nl -ba packages/core/src/models/VPointsSource.ts | sed -n '1,260p'

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

files = [
    Path('packages/layers/src/engine/PointsDataEngine.ts'),
    Path('packages/core/src/models/VPointsSource.ts'),
]
for p in files:
    print(f'=== {p} ===')
    text = p.read_text()
    for needle in ['loadRowFeatureCodes', 'loadPointsRowFeatureCodes', 'ensureLoaded', 'memoryCap', 'loadPoints(']:
        if needle in text:
            print(f'-- hits for {needle} --')
            for i, line in enumerate(text.splitlines(), 1):
                if needle in line:
                    print(f'{i}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 3310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the concrete implementations of the relevant methods
rg -n "class VPointsSource|loadRowFeatureCodes|loadPointsRowFeatureCodes|loadPoints\(" packages/core/src/models/VPointsSource.ts packages/core/src -g '!**/dist/**' -g '!**/build/**' -A 12 -B 6 || true

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 13397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f'--- {path} {start}-{end} ---')
    lines = Path(path).read_text().splitlines()
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')

show('packages/layers/src/engine/PointsDataEngine.ts', 540, 860)
show('packages/core/src/models/VPointsSource.ts', 335, 910)
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 42047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/core/src/models/VPointsSource.ts')
lines = path.read_text().splitlines()

# Focus on the row-code loader internals.
for start, end in [(839, 910), (335, 410), (780, 830)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 9387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/layers/src/engine/PointsDataEngine.ts')
lines = path.read_text().splitlines()

for start, end in [(545, 660), (820, 860)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 8130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f'--- {path} {start}-{end} ---')
    lines = Path(path).read_text().splitlines()
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')

show('packages/layers/src/engine/PointsDataEngine.ts', 545, 860)
show('packages/core/src/models/VPointsSource.ts', 335, 910)
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 41694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f'--- {path} {start}-{end} ---')
    lines = Path(path).read_text().splitlines()
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')

show('packages/layers/src/engine/PointsDataEngine.ts', 545, 860)
show('packages/core/src/models/VPointsSource.ts', 835, 910)
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 19189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [
    'packages/core/src/pointsLoadOptions.ts',
    'packages/core/src/models/VPointsSource.ts',
    'packages/layers/src/engine/PointsDataEngine.ts',
]:
    text = Path(path).read_text().splitlines()
    print(f'--- {path} ---')
    for i, line in enumerate(text, 1):
        if 'resolvePointsMemoryCap' in line or 'DEFAULT_POINTS_MEMORY_CAP' in line or 'memoryCap' in line and ('resolve' in line or 'DEFAULT' in line):
            print(f'{i:4d}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/layers/src/engine/PointsDataEngine.ts')
lines = path.read_text().splitlines()
for start, end in [(730, 770), (834, 858), (545, 650)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 8983


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in ['packages/core/src/pointsLoadOptions.ts', 'packages/core/src/models/VPointsSource.ts']:
    lines = Path(path).read_text().splitlines()
    print(f'--- {path} ---')
    for i, line in enumerate(lines, 1):
        if 'resolvePointsMemoryCap' in line or 'DEFAULT_POINTS_MEMORY_CAP' in line:
            print(f'{i:4d}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/layers/src/engine/PointsDataEngine.ts')
lines = path.read_text().splitlines()
for start, end in [(730, 780), (834, 860), (545, 660)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 9890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/core/src/pointsLoadOptions.ts')
lines = path.read_text().splitlines()
print(f'--- {path} ---')
for i, line in enumerate(lines, 1):
    if 'resolvePointsMemoryCap' in line or 'DEFAULT_POINTS_MEMORY_CAP' in line or 'memoryCap' in line:
        print(f'{i:4d}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

files = [Path('packages/layers/src/engine/PointsDataEngine.ts')] + list(Path('packages').rglob('*.ts')) + list(Path('packages').rglob('*.tsx'))
targets = ['ensureRowFeatureCodes(', 'getRowFeatureCodes(', 'hasRowFeatureCodes(']
for t in targets:
    print(f'=== {t} ===')
    for p in files:
        try:
            text = p.read_text()
        except Exception:
            continue
        if t in text:
            for i, line in enumerate(text.splitlines(), 1):
                if t in line:
                    print(f'{p}:{i}: {line.strip()}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 3749


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for needle in ['ensureRowFeatureCodes(', 'getRowFeatureCodes(', 'hasRowFeatureCodes(']:
    print(f'=== {needle} ===')
    for p in Path('packages').rglob('*.ts'):
        try:
            lines = p.read_text().splitlines()
        except Exception:
            continue
        for i, line in enumerate(lines, 1):
            if needle in line:
                print(f'{p}:{i}: {line.strip()}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 3290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "ensureRowFeatureCodes\\(|getRowFeatureCodes\\(|hasRowFeatureCodes\\(" packages -g '*.ts' -g '*.tsx' || true

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 3301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for start, end in [(1380, 1445), (1445, 1485)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 6335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if 'ensureRowFeatureCodes' in line or 'ensureLoaded' in line or 'hasRowFeatureCodes' in line:
        print(f'{i:4d}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for start, end in [(1390, 1435), (1435, 1465)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 4617


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for start, end in [(830, 870), (1388, 1422)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 4654


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/core/src/pointsLimits.ts')
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if 'DEFAULT_POINTS_MEMORY_CAP' in line or 'resolvePointsMemoryCap' in line or 'exceedsPointsPreloadLimit' in line:
        print(f'{i:4d}: {line}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 496


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for start, end in [(840, 870), (1390, 1422)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 4020


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('packages/vis/src/SpatialCanvas/useLayerData.ts')
lines = path.read_text().splitlines()
for start, end in [(850, 865)]:
    print(f'--- {path} {start}-{end} ---')
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4d}: {lines[i-1]}')
PY

Repository: Taylor-CCB-Group/SpatialData.js

Length of output: 1104


Thread entry.memoryCap into ensureRowFeatureCodes. ensureLoaded(target, cap) can use a layer-specific resident window, but this fallback still uses the loader default. Pass entry.memoryCap here to keep the row codes aligned with the resident batch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layers/src/engine/PointsDataEngine.ts` around lines 30 - 36, Update
ensureRowFeatureCodes to accept a memory-cap argument and pass entry.memoryCap
from the fallback call site, ensuring it uses the same resident window as
ensureLoaded. Locate the relevant invocation in the PointsDataEngine loading
path and thread the value through all affected helper signatures and calls.

Comment on lines +694 to +709
/**
* The distinct feature codes actually present in the resident batch (the
* preload cap means a feature-ordered file only loads a slice of its features).
* The panel greys features outside this set so selecting one that isn't loaded
* — which would render no points — is understandable rather than a glitch.
* Returns `undefined` when the row codes are not yet resident. Memoized against
* the row-codes identity so the O(rows) scan runs once per batch.
*/
/**
* True when the element has a file-backed feature code column — a real feature
* index whose codes are globally authoritative. False for dictionary-only
* feature columns (codes app-assigned, only stable within one catalog build) or
* an element with no feature codes. Undefined-safe: false until the resident
* batch has loaded. Gates the whole-dataset feature-index scan.
*/
hasFeatureCodeColumn(key: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Misplaced/duplicate JSDoc: this comment describes getResidentFeatureCodes, not hasFeatureCodeColumn.

The comment block at lines 694-701 ("distinct feature codes actually present in the resident batch… Memoized against the row-codes identity so the O(rows) scan runs once per batch") exactly matches getResidentFeatureCodes's behavior (line 754), which currently has no doc comment of its own. It's stranded above the unrelated hasFeatureCodeColumn, which already has its own correct doc directly below (702-708). This is confusing for future readers.

✏️ Proposed fix: move the stray doc to its rightful method
-  /**
-   * The distinct feature codes actually present in the resident batch (the
-   * preload cap means a feature-ordered file only loads a slice of its features).
-   * The panel greys features outside this set so selecting one that isn't loaded
-   * — which would render no points — is understandable rather than a glitch.
-   * Returns `undefined` when the row codes are not yet resident. Memoized against
-   * the row-codes identity so the O(rows) scan runs once per batch.
-   */
   /**
    * True when the element has a file-backed feature code column — a real feature
    * index whose codes are globally authoritative. False for dictionary-only
    * feature columns (codes app-assigned, only stable within one catalog build) or
    * an element with no feature codes. Undefined-safe: false until the resident
    * batch has loaded. Gates the whole-dataset feature-index scan.
    */
   hasFeatureCodeColumn(key: string): boolean {

and re-add the moved doc directly above getResidentFeatureCodes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layers/src/engine/PointsDataEngine.ts` around lines 694 - 709, Move
the JSDoc describing resident distinct feature codes from above
hasFeatureCodeColumn to directly above getResidentFeatureCodes, preserving its
content. Keep the existing hasFeatureCodeColumn documentation immediately above
that method and ensure each comment documents only its corresponding method.

Comment on lines +89 to +111
if (selected && scanning) {
return {
tone: 'loading',
greyed: true,
label: 'loading',
reason: 'Selected — its feature-index scan is in progress.',
};
}
if (!supportsOnDemandLoad) {
return {
tone: 'noIndex',
greyed: true,
label: 'not in sample',
reason:
'Beyond the resident window, and this dataset has no feature index, so it can’t be fetched on demand. Raise the memory cap or rewrite the dataset with an index.',
};
}
return {
tone: 'notLoaded',
greyed: true,
label: 'not loaded',
reason: 'Beyond the resident window; select it to fetch its points via the feature-index scan.',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Select it to fetch" message is misleading when the feature is already selected.

When selected && !scanning && !resident && !rendered && supportsOnDemandLoad, the function falls through to the notLoaded branch with reason "select it to fetch its points via the feature-index scan." But the feature IS already selected — this transient state occurs between selection and scan start. The message should reflect that a scan is pending, not that the user needs to select it.

The author noted "This is up for review" at line 46, so this is a known gap.

💡 Suggested fix
   if (selected && scanning) {
     return {
       tone: 'loading',
       greyed: true,
       label: 'loading',
       reason: 'Selected — its feature-index scan is in progress.',
     };
   }
+  if (selected && supportsOnDemandLoad) {
+    return {
+      tone: 'loading',
+      greyed: true,
+      label: 'queued',
+      reason: 'Selected — waiting for the feature-index scan to start.',
+    };
+  }
   if (!supportsOnDemandLoad) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (selected && scanning) {
return {
tone: 'loading',
greyed: true,
label: 'loading',
reason: 'Selected — its feature-index scan is in progress.',
};
}
if (!supportsOnDemandLoad) {
return {
tone: 'noIndex',
greyed: true,
label: 'not in sample',
reason:
'Beyond the resident window, and this dataset has no feature index, so it can’t be fetched on demand. Raise the memory cap or rewrite the dataset with an index.',
};
}
return {
tone: 'notLoaded',
greyed: true,
label: 'not loaded',
reason: 'Beyond the resident window; select it to fetch its points via the feature-index scan.',
};
if (selected && scanning) {
return {
tone: 'loading',
greyed: true,
label: 'loading',
reason: 'Selected — its feature-index scan is in progress.',
};
}
if (selected && supportsOnDemandLoad) {
return {
tone: 'loading',
greyed: true,
label: 'queued',
reason: 'Selected — waiting for the feature-index scan to start.',
};
}
if (!supportsOnDemandLoad) {
return {
tone: 'noIndex',
greyed: true,
label: 'not in sample',
reason:
'Beyond the resident window, and this dataset has no feature index, so it can’t be fetched on demand. Raise the memory cap or rewrite the dataset with an index.',
};
}
return {
tone: 'notLoaded',
greyed: true,
label: 'not loaded',
reason: 'Beyond the resident window; select it to fetch its points via the feature-index scan.',
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vis/src/SpatialCanvas/featureRowState.ts` around lines 89 - 111,
Update the feature-row state logic so the selected, non-scanning, non-resident,
non-rendered, on-demand-load case does not use the misleading “select it to
fetch” reason. In the function containing the selected/scanning and notLoaded
branches, add a selected-specific pending-scan branch with loading or equivalent
pending tone/label and wording that indicates the feature is already selected
and its feature-index scan is about to begin; keep the existing notLoaded
message only for unselected features.

xinaesthete and others added 8 commits July 12, 2026 09:52
The feature-index scan generator now accumulates matched chunks and emits
each progress with a partialResult that is the GROWING buffer of everything
matched so far, so a consumer can render points that accumulate as the scan
runs instead of only seeing the settled batch.

- New shared helper pointsScanChunkProgress() builds {chunk, progress} from
  the accumulated chunks; both scan branches (row-group / parts) use it, and
  it's available to other VPointsSource scans that want progressive display.
- The collector reuses the last partialResult (already the full buffer) rather
  than re-accumulating/concatenating, stamping authoritative totals from the
  generator's return summary. Empty selection returns that same summary shape.
- Engine: getMatchingPartialResource() resolves the latest partial buffer to a
  render resource, cached on the partial's identity. getLayers draws it as a
  separate overlay sub-layer above the base (resident / prior matched batch),
  so the base doesn't blank while points fill in.

Verified live on xenium transcripts_feature_then_morton: the overlay buffer
grows monotonically to the full 646,132 matched points and renders, then hands
off to the settled batch. Type-checks clean; core (146) / layers (120) / vis
(50) tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderPointsLayer + PointsLayerRenderConfig had zero importers (superseded
by PointsLayer); only the barrel re-exported them, and nothing consumed that.
Remove the file and the re-export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The progressive-scan overlay drew its growing buffer unfiltered, unlike the
settled matched layer. So deselecting a feature whose scan is still in flight
(the smaller selection stays "covered", so the scan keeps running) left that
feature's points rendered by the overlay until the scan settled, then they
vanished. Pass the current featureCodes + the partial buffer's own per-row
codes (new getMatchingPartialRowFeatureCodes) so the overlay filters to the
selection like the settled layer does.

(Also carries an in-code note flagging the ensureMatchingFeaturesLoaded
mutable-state approach as a redesign target — see docs/plans punch-list.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ShowMatchingPoints asserted "Loaded all N matching points", but t.loaded is
the covered-batch size, which overstates the selection when it filters that
batch in memory. Reword to state what's actually true — the batch held in
memory and that the view is filtered to the selection. A precise per-selection
count needs the engine to track it (deferred to the redesign).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Categorised inventory of known points issues split fix-before-merge vs
defer-to-redesign, so the merge line is explicit and nothing is lost across
the boundary. Also carries an in-code pointer marking the matching-scan loader
as a candidate for a generator/streaming version (deferred).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F1 fixed deselected-feature lingering, but partial-load visibility logic and
the per-chunk resource rebuild (which flashes the __partial layer) remain —
a stable growing GPU buffer is the real fix, deferred to the engine redesign.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xinaesthete
xinaesthete marked this pull request as ready for review July 13, 2026 13:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/plans/points-redesign-punchlist.md (1)

49-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider paraphrasing the quoted profanity in D1.

The verbatim quote from PointsDataEngine.ts includes profanity ("shit-show"). While it reproduces an existing code comment, including it in documentation perpetulates unprofessional language. Consider paraphrasing (e.g., "given the difficulty of agent debugging…") or referencing the comment location without reproducing the exact wording.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/points-redesign-punchlist.md` around lines 49 - 53, Update the D1
entry to remove the quoted profanity from the PointsDataEngine.ts reference,
paraphrasing it as the difficulty of agent debugging or referencing the comment
location without reproducing the exact wording; preserve the surrounding context
and intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/plans/points-redesign-punchlist.md`:
- Around line 49-53: Update the D1 entry to remove the quoted profanity from the
PointsDataEngine.ts reference, paraphrasing it as the difficulty of agent
debugging or referencing the comment location without reproducing the exact
wording; preserve the surrounding context and intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d7dbecc-5bcd-4cd1-bbe0-548477d376cc

📥 Commits

Reviewing files that changed from the base of the PR and between e2de7d2 and ee58000.

📒 Files selected for processing (10)
  • docs/plans/points-redesign-punchlist.md
  • packages/core/src/models/VPointsSource.ts
  • packages/core/src/models/index.ts
  • packages/core/src/pointsLoadOptions.ts
  • packages/layers/src/engine/PointsDataEngine.ts
  • packages/layers/src/pointsScatterLayer.ts
  • packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx
  • packages/vis/src/SpatialCanvas/renderers/index.ts
  • packages/vis/src/SpatialCanvas/renderers/pointsRenderer.ts
  • packages/vis/src/SpatialCanvas/useLayerData.ts
💤 Files with no reviewable changes (2)
  • packages/vis/src/SpatialCanvas/renderers/pointsRenderer.ts
  • packages/vis/src/SpatialCanvas/renderers/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/core/src/models/index.ts
  • packages/layers/src/pointsScatterLayer.ts
  • packages/core/src/models/VPointsSource.ts
  • packages/layers/src/engine/PointsDataEngine.ts
  • packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx

@xinaesthete
xinaesthete merged commit b2d9358 into main Jul 13, 2026
3 checks passed
@xinaesthete
xinaesthete deleted the points-feature-filter branch July 13, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant