Points feature filter (MVP step 2) + responsive off-thread loads - #81
Conversation
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>
|
Warning Review limit reached
Next review available in: 40 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughChangesThis 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
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
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>
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>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (9)
packages/vis/src/SpatialCanvas/useLayerData.ts (1)
559-559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLeftover 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 winDrop the
as Float32Arrayby narrowing through a local.
colorByFeaturealready impliesattributes.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); usesatisfies,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 valueAdd 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 exposemodules.- Line 129: No comment explaining why
highlightFeatureCodeisn't onthis.props(deck.gl'sLayertype doesn't reflect customdefaultPropsin TypeScript).- Line 131: No comment explaining why
setShaderModulePropsrequires 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 winAdd an accessible label to the search input.
The search
<input>at line 288 has only aplaceholder, which is not a substitute for an accessible label. Screen readers may not announce the field's purpose. Addingaria-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 | 🔵 TrivialOffer to help:
t.loadeddisplay semantics are known to be misleading.The NOTE at lines 79–81 correctly flags that
t.loadedis 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 throughgetActiveTruncation?🤖 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 | 🔵 TrivialType assertion in
sliceArraywould benefit from a short justifying comment.The
as unknown as { slice?: ... }cast is local and boundary-motivated (duck-typing anArrayLike<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 valueUndocumented type assertions in
capFeatureCodes.Both the
subarraycast and theslice.call(...) as ArrayLike<number>are reasonable here (no TS type captures "ArrayBufferView withsubarray"), 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 valueAvoidable type assertion for
featureKey.
configuredFeatureKey as stringat line 367 can't be proven by the compiler because the narrowing check lives in a separatewantFeaturesboolean 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 winCache
loadParquetDatasetMetadatato eliminate the repeated 404 probing.The comment correctly identifies the symptom (
points.parquet/points.4.parquet404s), but the root cause is thatloadParquetDatasetMetadataitself isn't memoized — every call re-runs part discovery (loadParquetPartMetadatalooped 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 byparquetPath(analogous toparquetTableCache), 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
📒 Files selected for processing (50)
docs/parquet-wasm-limitations.mddocs/plans/points-mvp-and-roadmap.mdpackages/core/src/index.tspackages/core/src/models/VPointsSource.tspackages/core/src/models/VTableSource.tspackages/core/src/models/index.tspackages/core/src/parquetFooterStats.tspackages/core/src/pointsFeatures.tspackages/core/src/pointsLimits.tspackages/core/src/pointsLoadOptions.tspackages/core/src/pointsLoader.tspackages/core/src/pointsTiling.tspackages/core/src/spatialViewFit.tspackages/core/src/workers/index.tspackages/core/src/workers/points-worker.tspackages/core/src/workers/pointsWorkerClient.tspackages/core/src/workers/pointsWorkerProtocol.tspackages/core/src/workers/pointsWorkerScan.tspackages/core/tests/parquetFooterStats.spec.tspackages/core/tests/pointsFeatures.spec.tspackages/core/tests/pointsLimits.spec.tspackages/core/tests/pointsTiling.spec.tspackages/core/tests/pointsWorker.spec.tspackages/core/tests/pointsWorkerScan.spec.tspackages/core/tests/remapRowFeatureCodes.spec.tspackages/layers/src/PointsLayer.tspackages/layers/src/engine/PointsDataEngine.tspackages/layers/src/index.tspackages/layers/src/pointsFeatureColor.tspackages/layers/src/pointsFeatureColorExtension.tspackages/layers/src/pointsLoader.tspackages/layers/src/pointsRenderAttributes.tspackages/layers/src/pointsScatterLayer.tspackages/layers/src/preloadedScatterStrategy.tspackages/layers/src/resolvePointsRenderResource.tspackages/layers/tests/pointsDataEngine.spec.tspackages/layers/tests/pointsFeatureColor.spec.tspackages/layers/tests/pointsFeatureColorExtension.spec.tspackages/layers/tests/pointsRenderAttributes.spec.tspackages/layers/tsconfig.jsonpackages/vis/demo/src/main.tsxpackages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsxpackages/vis/src/SpatialCanvas/PointsFeatureState.tsxpackages/vis/src/SpatialCanvas/PointsLayerPanel.tsxpackages/vis/src/SpatialCanvas/featureRowState.tspackages/vis/src/SpatialCanvas/index.tsxpackages/vis/src/SpatialCanvas/public.tspackages/vis/src/SpatialCanvas/types.tspackages/vis/src/SpatialCanvas/useLayerData.tspackages/vis/tests/pointsFeatureRowState.spec.ts
| - **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. |
There was a problem hiding this comment.
🎯 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.
| **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. |
There was a problem hiding this comment.
📐 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.
| * 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. |
There was a problem hiding this comment.
🗄️ 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/**' || trueRepository: 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}')
PYRepository: 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 || trueRepository: 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)
PYRepository: 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]}')
PYRepository: 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]}')
PYRepository: 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)
PYRepository: 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)
PYRepository: 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}')
PYRepository: 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]}')
PYRepository: 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}')
PYRepository: 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]}')
PYRepository: 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}')
PYRepository: 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()}')
PYRepository: 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()}')
PYRepository: 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' || trueRepository: 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]}')
PYRepository: 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}')
PYRepository: 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]}')
PYRepository: 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]}')
PYRepository: 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}')
PYRepository: 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]}')
PYRepository: 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]}')
PYRepository: 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.
| /** | ||
| * 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 { |
There was a problem hiding this comment.
📐 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.
| 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.', | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| 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.
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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/plans/points-redesign-punchlist.md (1)
49-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider paraphrasing the quoted profanity in D1.
The verbatim quote from
PointsDataEngine.tsincludes 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
📒 Files selected for processing (10)
docs/plans/points-redesign-punchlist.mdpackages/core/src/models/VPointsSource.tspackages/core/src/models/index.tspackages/core/src/pointsLoadOptions.tspackages/layers/src/engine/PointsDataEngine.tspackages/layers/src/pointsScatterLayer.tspackages/vis/src/SpatialCanvas/PointsLayerPanel.tsxpackages/vis/src/SpatialCanvas/renderers/index.tspackages/vis/src/SpatialCanvas/renderers/pointsRenderer.tspackages/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
Points feature filter (MVP step 2) + responsive off-thread loads
Follow-up to #80. Adds the Points feature filter on the
@spatialdata/layersPointsLayercomposite and makes the geometry / catalog / codes loadingoff-thread, so large transcripts layers stay responsive while loading,
filtering, and colouring.
points-feature-filter→main.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.mdrather than patched here (see Deferred below).
What's in it
PointsDataEngineowns the feature catalog + per-row codes — a React-free,headless-tested cache in
@spatialdata/layerswithsubscribe/notify.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 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.
counts), not just the resident preview; the instant resident subset is
superseded by the full scan when it settles.
transcriptsstores with afeature_namedict column and no
*_codescolumn are matched by resolving each row's nameagainst one authoritative code space (the full catalog).
attributes; a GPU shader extension colours by an OKLCh golden-angle palette
(single source for the L/C constants, JS swatch mirror in lockstep).
pointsMemoryCap(1M–16M, default 4M); loweringsheds resident rows in memory (no refetch), raising reloads only to grow a
truncated batch; an
AbortControllerper load cancels superseded fetches.usePointsFeatureState(oneuseSyncExternalStoreover the engine) replaces the prop-drilled getters; the'use no memo'escape hatch is now scoped to two leaf panels instead of thewhole canvas shell.
growing buffer that renders as an overlay while it loads (spike quality — see
Deferred).
Verified
core/layers/vis; tests core 146 · layers120 · vis 50.
transcripts_feature_then_mortonlayer: load + togglesstay 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 thanloaded from
cdn.vitessce.ioat runtime. That build exposesreadMetadata/readParquetRowGroup(the row-group APIs fromkylebarron/parquet-wasm#804)
which
parquet-wasm@0.6.1on npm lacks.docs/parquet-wasm-limitations.mdrecords 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. (
readParquetRowGroupalso mis-decodes dictionarycolumns like
feature_name, so those decode via whole parts usingreadParquet.)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:
PointsEntryis an imperative mutable recordmutated 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:
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).
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 nowreports the honest in-memory batch size instead).
🤖 Generated with Claude Code
Summary by CodeRabbit