Skip to content

feat(web): multi-select relationship-type + ontology filters#369

Merged
aaronsb merged 3 commits into
mainfrom
feat/multi-select-filters
May 16, 2026
Merged

feat(web): multi-select relationship-type + ontology filters#369
aaronsb merged 3 commits into
mainfrom
feat/multi-select-filters

Conversation

@aaronsb
Copy link
Copy Markdown
Owner

@aaronsb aaronsb commented May 16, 2026

What

Wires UI + filter application for the universal relationshipTypes / ontologies filters (Task #20). The shared graphStore already declared these fields (string[], default []); this completes them, mirroring the existing universal minConfidence pattern.

Changes

  • graphStore: filterOptions {relationshipTypes, ontologies} + setter — the available values (vs filters.* = selected). Ephemeral, not persisted.
  • ForceGraph: publishes distinct e.type / n.category from the engine data (same strings the filter compares against — raw API data would mismatch since the raw→engine transform maps empty ontology to 'Unknown'). filteredData now narrows by relationship type (edges) and ontology (nodes, dropping orphaned edges). Empty = show all.
  • SettingsPanel: two checkbox-lists in the Filters section; options from filterOptions, selection via setFilters — same shared-store, universal-across-explorers model as minConfidence.

Scope

  • Force Graph + universal store, per the task. Document Explorer parity (separate data path) deferred.
  • No select-all/none control (not requested).
  • Pre-existing eslint debt in touched files (unused RawGraphNode/RawGraphLink, onNodeClick, mergeRawGraphData, one any) left as-is — not introduced here, not CI-gated, out of scope.

Verification

  • tsc --noEmit clean, 10/10 web tests pass, no new lint errors.
  • Browser verification pending (filter a loaded graph by relationship type / ontology in the Force Graph settings panel; confirm it also reflects in other explorers reading the shared store).

aaronsb added 2 commits May 15, 2026 22:36
Task #20. The shared store already declared filters.relationshipTypes
and filters.ontologies (string[], default []); this wires the UI and
filter application, mirroring the existing universal minConfidence
pattern.

- graphStore: add filterOptions {relationshipTypes, ontologies} +
  setter — the *available* values (distinct from filters.* which are
  *selected*). Ephemeral, not persisted (partialize unchanged).
- ForceGraph: publish distinct e.type / n.category from the engine
  data (the SAME strings the filter compares against — deriving from
  raw API data would mismatch, since the raw->engine transform maps
  empty ontology to 'Unknown'). filteredData now also narrows by
  relationship type (edges) and ontology (nodes, dropping orphaned
  edges). Empty = show all, matching convention.
- SettingsPanel: two checkbox-lists in the Filters section, options
  from store filterOptions, selection via setFilters — same
  shared-store, universal-across-explorers model as minConfidence.

Scoped to Force Graph + universal store per the task; Document
Explorer parity (separate data path) deferred. No select-all/none
control (not requested). Pre-existing eslint debt in the touched files
(unused RawGraphNode/RawGraphLink import, onNodeClick/mergeRawGraphData,
one any) left as-is — out of scope, not introduced here, not CI-gated.

Typecheck clean, 10 web tests pass, no new lint errors.
Bounded consolidation step (the pin system, colour schemes, Legend
reconciliation, and the raw-vs-grouped effector-model decision are
deferred — captured in a design note for a future build-and-learn
session, not an ADR; pre-build UX ADRs ossify into contrarian
artifacts since feel is only known once built).

- graphStore: filterOptions entries are now FilterOption {value,color}
  instead of bare strings.
- ForceGraph: publishes each option's rendered colour from the SAME
  palette the scene uses — relationship type -> category colour
  (mirrors the edgeColors 'type' branch), ontology -> palette(ontology)
  — so swatch == on-screen colour.
- SettingsPanel: each row shows its colour swatch (50 identical rows
  were unreadable — explicit user constraint), plus per-list
  all / none shortcuts. 'none' clears to [] = the store's documented
  empty-means-show-all (true hide-all is part of the deferred
  effector-model decision).

tsc clean, 10/10 web tests, no new eslint.
Swatch dot alone wasn't enough — the checkbox (accent-color) and the
label text now also render in the value's rendered colour, matching
how the Legend frame reads. tsc clean, 10/10 tests.
@aaronsb
Copy link
Copy Markdown
Owner Author

aaronsb commented May 16, 2026

Code Review — PR #369

What this changes: Wires UI + filter application for the already-declared filters.relationshipTypes / filters.ontologies shared-store fields, mirroring the universal minConfidence / visibleEdgeCategories pattern. Adds ephemeral filterOptions (available values + rendered colour) published by the active explorer, colour-faithful checkbox selectors with select all/none, and orphaned-edge dropping when ontology filtering removes nodes. +159/−10, 3 files, all web/src/.

Assessment: Solid, faithful to stated intent. Verified clean: tsc --noEmit passes, 10/10 tests pass, exactly 5 eslint errors — all on lines outside this PR's diff hunks (graphStore.ts:23/246, ForceGraph.tsx:41/76), so the documented pre-existing baseline of 5 is preserved with no new errors.

Correctness — verified sound

  • Option-string ↔ filter-target match: The publish effect derives options from engine data (e.type, n.category) — the exact same strings filteredData compares against. Deriving from raw API data would mismatch (raw→engine normalizes empty ontology to 'Unknown'). Correct, and the rationale is well-documented in-code.
  • Colour fidelity: relationship-type colour getCategoryColor(vocabStore.getCategory(value) || undefined) mirrors the edgeColors 'type' branch (ForceGraph.tsx:266–267) line-for-line; ontology colour palette(value) matches the node base colour (palette(n.category), :296). Swatches will match the screen.
  • Orphaned-edge drop: EngineEdge.from/.to are node-id strings (types.ts:33/35), so keptIds.has(e.from) / keptIds.has(e.to) is the right identity check. keptIds only built when an ontology filter is active — correct short-circuit.
  • No render-loop / thrash: palette is useMemo-keyed on data?.nodes; vocabStore is the no-selector whole-store ref (stable unless vocab state changes — pre-existing convention also used by edgeCategory/edgeColors). Effect deps [data, setFilterOptions, vocabStore, palette] are complete; setFilterOptions is a stable zustand action. The effect does not depend on filterOptions, so setting it cannot re-trigger the effect.
  • filteredData memo: dep array complete (edgeCategory is useCallback-stable). Empty selection = show all, consistent with minConfidence/visibleEdgeCategories convention.
  • FilterOption type: exported from graphStore.ts, imported as type in SettingsPanel — correct placement (lives with the store slice it describes).
  • SettingsPanel: React key={opt.value} is stable/unique; controlled-checkbox toggle logic (checked ? filter-out : append) is correct; the none button writing [] is the documented "show all" convention.
  • Persistence: confirmed partialize (graphStore.ts:617) persists only explorationSession + similarityThresholdfilters is not persisted, so it resets to defaults (empty = show all) on reload. No stale-cross-dataset "everything hidden" hazard. filterOptions ephemerality is coherent with the whole filters slice being ephemeral. PR description's "partialize unchanged" is accurate.

Non-blocking suggestions (consider for #370 or a follow-up)

  1. setFilterOptions allocates a fresh object every effect run even when distinct values/colours are unchanged (e.g. data/palette ref change with identical content). SettingsPanel reads filterOptions via a single selector, so each run forces a SettingsPanel re-render. Not a loop — just churn. A shallow-equal guard before setFilterOptions (or keying the effect on data?.nodes/data?.edges instead of whole data) would eliminate it. Pure polish.
  2. No direct test for the new filteredData ontology/orphan paths or the publish effect. Consistent with the existing untested minConfidence filter, so not a gate — but a small memo-level unit test (ontology filter drops nodes + orphaned edges; empty = passthrough) would lock in the orphan-drop invariant cheaply.

Quality / SOLID

checkboxList is a focused render helper (~55 lines, single responsibility, no added nesting depth). graphStore additions are minimal and follow the existing slice shape. No file-length / monolith concerns introduced. Scope is disciplined — the deferred pin/colour-scheme/Legend-reconciliation work is correctly held to #370, no creep observed. Known deferrals (Legend edge-category duplication, none=show-all vs true hide-all, Document Explorer parity, pre-existing eslint debt) are explicitly tracked and out of scope here.

Recommendation: merge-with-followups

Wiring is correct and faithful to the documented intent; types/tests/lint all clean against baseline; deferrals are explicit and bounded. The two non-blocking items (re-render churn, missing memo test) are good candidates to fold into #370 or a quick follow-up but do not gate this merge.


AI-assisted review via Claude Code (code-reviewer)

@aaronsb aaronsb merged commit f8ee93b into main May 16, 2026
3 checks passed
@aaronsb aaronsb deleted the feat/multi-select-filters branch May 16, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant