Skip to content

refactor: control bar cleanup and filtering improvements#259

Draft
peymanvahidi wants to merge 20 commits into
mainfrom
refactor/control-bar-and-filtering
Draft

refactor: control bar cleanup and filtering improvements#259
peymanvahidi wants to merge 20 commits into
mainfrom
refactor/control-bar-and-filtering

Conversation

@peymanvahidi
Copy link
Copy Markdown
Collaborator

@peymanvahidi peymanvahidi commented May 14, 2026

Removes the 3D projection "Plane" (XY/XZ/YZ) dropdown and all projectionPlane plumbing from the web frontend. 3D Parquet files continue to load, z is silently discarded and only x / y are rendered. The Python protspace CLI is unaffected.

Changes:

  1. control-bar: remove plane selector UI, handlePlaneChange, projection-plane-change event, the projectionPlane property, the cross-element assignment that pushed plane onto the scatter-plot, and projectionPlane? from ScatterplotElementLike.
  2. scatter-plot: remove projectionPlane property, xz/yz coord remap, point.z write, and projectionPlane from change-detection gates. DataProcessor.processVisualizationData is now called with four args.
  3. data-processor: remove the projectionPlane parameter and the xz/yz branch. PlotDataPoint loses its optional z? field. Tests updated: xz/yz mapping tests removed, "preserves z" test replaced with "drops the z coordinate for 3D projections."
  4. control-bar helpers: delete the now-unused isProjection3D and getProjectionPlane helpers and their tests.
  5. scatter-plot (chore): drop stale "x/y/z" from the _updatePlotDataCoordinates fast-path comment, it now correctly says "x/y" after PlotDataPoint.z was removed in commit 3.

…lumbing

Drops the Plane (XY/XZ/YZ) <select>, handlePlaneChange, the
projection-plane-change CustomEvent, the projectionPlane @Property,
and the cross-element assignment that pushed the plane onto the
scatter-plot. Also drops projectionPlane? from ScatterplotElementLike.

Helpers isProjection3D and getProjectionPlane are no longer imported
here; they are deleted in a follow-up commit. The scatter-plot still
owns its projectionPlane property in this commit; cleaned up next.

Part of refactor for issue #196.
…gnment

Removes the projectionPlane Lit property, the xz/yz coordinate remap,
the point.z = coords[2] write (a write-only assignment with no readers),
and the projectionPlane entries in the _processData change-detection
gates. The data-processor call now passes four args; the unused fifth
parameter on data-processor itself is removed in a follow-up commit.

3D Parquet inputs now render using coords[0] / coords[1] only, matching
the historical default plane ('xy') that the property defaulted to in
production.

Part of refactor for issue #196.
Removes the projectionPlane parameter from
DataProcessor.processVisualizationData and the conditional xz/yz
coordinate remap. 3D projections now collapse to coords[0] / coords[1]
on every code path. PlotDataPoint loses its optional z field; nothing
consumed it (scatter-plot's only write was removed in the previous
commit).

Test changes:
- Delete xz and yz plane-mapping tests.
- Replace 'preserves z coordinate for 3D projections' with
  'drops the z coordinate for 3D projections (rendered as 2D)',
  pinning the new contract.

Part of refactor for issue #196.
…e helpers

After the plane selector removal in earlier commits, these helpers
have zero remaining callers in packages/ or app/. Removed both
functions and their describe blocks from the test file.

Part of refactor for issue #196.
Commit 7ae6f74 removed the optional z field from PlotDataPoint. The
fast-path comment in _updatePlotDataCoordinates still claimed
'overwrite x/y/z on existing PlotDataPoints' — it now correctly says
'overwrite x/y'. Comment-only; no behavior change.

Part of refactor for issue #196.
@peymanvahidi peymanvahidi marked this pull request as draft May 14, 2026 10:45
@peymanvahidi peymanvahidi marked this pull request as ready for review May 18, 2026 08:14
…ders

The numeric filter's placeholder text showed the data's min/max
("min (4)", "max (1630)"). Removed at user request; placeholders are
now just "min" / "max". This also drops the now-unused
computeNumericBounds helper and the _dataBounds/_boundsAnnotation state
that only existed to feed those hints.
@tsenoner tsenoner marked this pull request as draft May 18, 2026 20:44
@tsenoner
Copy link
Copy Markdown
Owner

The filtering logic needs a complete revisit.
BUG reproduction:

…257)

The query builder evaluates against the full materialized dataset, but
`_handleQueryApply` mapped the matched indices through `getCurrentData()` — the
isolated subset after a prior apply — then stacked another `isolateSelection()`
layer. Re-applying the same query resolved wrong/undefined ids and shrank the
result each time (546 -> 19 -> only fading): the open #257 report.

Route the query through the dedicated, idempotent `filteredProteinIds` /
`filtersActive` channel the scatter plot already consumes, mapping indices
through `getMaterializedData()` so they align with the array the query was
evaluated against. A filter is no longer a selection or an isolation:
re-applying is a no-op, manual isolation is left untouched, and the dead
`isolate-data` / `reset-isolation` dispatches are dropped.

Clear the filter on dataset swap (scatter-plot `updated()`, `applyPlotState`,
and the control-bar data-change handler) so a stale filtered-id set can't blank
the new plot or leave a stale Filter badge. Relabel "Apply & Isolate" ->
"Apply Filter".

Add control-bar.query-apply.test.ts pinning the idempotent-apply / reset /
no-isolation contract.
…er NOT

Three numeric-filter correctness fixes:

- `_selectAnnotation` keyed on `kind === 'numeric'`, but the currently-coloured
  numeric annotation is materialized to `kind:'categorical'` (it keeps
  `sourceKind:'numeric'`), so picking it offered a categorical bin picker instead
  of the numeric range input. Use the shared `isNumericAnnotation()` so a numeric
  annotation is filtered numerically whether or not it is currently materialized.

- An unconfigured numeric condition evaluated to the empty set, so NOT-wrapping it
  isolated EVERY protein (complement of nothing) while Apply stayed enabled —
  asymmetric with an empty categorical condition. Treat an unconfigured numeric
  condition as a no-op (matches all), mirroring categorical, so `NOT(unconfigured)`
  matches nothing and Apply stays disabled.

- Switching operator left the now-hidden bound populated; switching back silently
  resurrected it and re-constrained the filter. Null the unused bound on operator
  change.

Also give the operator select and min/max inputs accessible names (aria-label),
and add component tests for the controlled round-trip, operator switch, numeric
NOT-symmetry, and annotation kind detection.
- docs(explore): replace the removed XY/XZ/YZ plane-selector tip with a note
  that 3D projections render as their X/Y view (completes the #196 removal).
- test(scatter-plot): drop z:0 from style-getters mock points; PlotDataPoint.z
  was removed in this PR.
- refactor(query-types): stop seeding createGroup with a leading logicalOp:'AND'
  so a first-position group can't carry a spurious operator (mirrors
  createCondition; the builder sets the op for non-first groups).
- test(control-bar): pin the flat left-to-right operator precedence
  ((A OR B) AND C) so the known limitation stays an intentional choice.
… follow-up)

Routing query filters through the filteredProteinIds channel (df07c96) exposed a
latent rendering bug. _processData built _plotData from _getCurrentDisplayData(),
which COMPACTS the matched subset, so each PlotDataPoint.originalIndex became a
slice-local 0..N index. But the style getters (_buildStyleGetters uses the full
materialized data) and the tooltip path (this.data) resolve annotation values by
originalIndex against the FULL dataset. For any non-prefix filter (e.g.
protein_family=X, which keeps a scattered subset) every kept point was painted
with the WRONG protein's colour/shape/opacity and showed the wrong tooltip — and
points whose mis-resolved value was legend-hidden vanished. The channel was
dormant before df07c96, so query-apply is what first made this reachable.

Fix: build _plotData from the full materialized data and apply the matched-id
set as a membership filter AFTER the map (a new optional `visibleProteinIds` arg
on DataProcessor.processVisualizationData), exactly as isolation already does, so
originalIndex stays a GLOBAL index. The style getters and tooltips — both written
for global indices — now resolve correctly with no change. The fast path is
gated off whenever a filter is active (it already rebuilt every time under the
old slice-by-reference approach, so no perf change). getCurrentData()/data-change
keep slicing for the legend and export, which want the filtered protein list.

Add scatter-plot.filter-render.test.ts pinning per-point colour for a non-prefix
filter, plus the prefix and unfiltered cases.
@peymanvahidi
Copy link
Copy Markdown
Collaborator Author

@tsenoner I think the issues are fixed

Brings in the multi-annotation hover tooltip, cross-projection duplicate
badge grouping, and the includeShapes/shape-rotation removal from main.

Conflict resolved in control-bar.ts: main inserted the `tooltipAnnotations`
property immediately before the `projectionPlane` property this branch
removed (3D plane selector teardown), so the two edits collided on adjacent
lines. Kept main's `tooltipAnnotations`; dropped `projectionPlane`.

The other five overlapping files (data-renderer.ts, control-bar/types.ts,
scatter-plot.ts, style-getters.test.ts, utils/types.ts) auto-merged as
disjoint hunks. Verified post-merge: type-check clean, 871 unit tests pass,
and a per-file 3-way semantic audit confirmed each result equals the union
of both branches' intent with no dropped or duplicated logic.
@peymanvahidi peymanvahidi marked this pull request as ready for review June 1, 2026 12:30
@peymanvahidi peymanvahidi requested a review from tsenoner June 1, 2026 12:30
@tsenoner
Copy link
Copy Markdown
Owner

tsenoner commented Jun 1, 2026

Hi @peymanvahidi, great progress.
There are still a bunch of bugs in the filtering logic. Please investigate them:

  • applying a filter-> open the filter again -> "Reset All" -> "cancel": This will update the legend but not the visualization
  • Applying an empty filter without a condition shows as if we have a filter applied in the control bar. Fix this by disallowing "Apply Filter" without a condition. Therefore, it must be closed.
  • The behavior of the "Cancel" button is odd in general. When a successful filter is applied, edited, and then canceled, the edited changes are shown when reopening (which is misleading, as they aren't applied). Therefore, I think cancel should always restore the state it was in before (except when pressing "Reset All", when all Filters should be removed + the legend & canvas updated).
  • Filtering on multilabel still doesn't work properly. Choose keyword -> When selecting a category, the numbers shown don't represent the numbers actually shown in the legend (see image). When selecting this number, they are filtered by it, not the actual number (e.g., Secreted only displays 41 instead of the 6169 proteins).
  • Display the number of proteins that are displayed in the lower left corner, the same way we do when isolating. Ideally, we want to display Y/X proteins. Thinking about it, we always want to display the number of Proteins; so, simply put, we want to always show Y/X proteins. Where Y and X are the same when no filtering or isolation is applied.
  • When a filter has been applied, and the filter menu gets reopened, it takes quite a long time for the texts: "1663 of 6183 proteins matched" and "1,663 proteins match" to be displayed. Please investigate the source of the delay and how to improve it. Also, let's be consistent and either show or not to show the thousand separator throughout (I'm in favor of displaying it everywhere).

@tsenoner tsenoner marked this pull request as draft June 1, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants