Skip to content

Add filtering and unsimulated action pins to overview diagram - #105

Merged
marota merged 12 commits into
mainfrom
claude/add-action-overview-filters-OE36Z
Apr 22, 2026
Merged

marota merged 12 commits into
mainfrom
claude/add-action-overview-filters-OE36Z

Conversation

@marota

@marota marota commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds comprehensive filtering capabilities to the Remedial Action overview diagram and introduces support for displaying unsimulated (scored-but-not-yet-simulated) actions as dimmed, dashed pins. The filtering state is now shared between the overview pins and the sidebar action feed to keep both views in sync.

Key Changes

Filtering Infrastructure

  • Shared filter state: Introduced ActionOverviewFilters type containing category toggles (green/orange/red/grey), threshold cap, unsimulated visibility flag, and action-type chip selection
  • Filter predicates: Extracted actionPassesOverviewFilter() utility so both the overview pins and action feed cards apply identical filtering logic
  • Action-type classification: Created new actionTypes.ts module with classifyActionType() and matchesActionTypeFilter() to unify action categorization across three UI surfaces (overview, feed, explore pairs)

Overview Diagram Enhancements

  • Category + threshold filtering: Pins are now hidden when their severity category is disabled or their max_rho exceeds the threshold
  • Protected constituent pins: When a combined action passes the filter, its constituent unitary pins remain visible (dimmed) even if they individually fail the filter, preserving context
  • Filter UI controls: Added category toggle chips, "Select All/None" buttons, threshold slider, and unsimulated visibility checkbox
  • Unsimulated pin layer: New buildUnsimulatedActionPins() function renders scored-but-not-simulated actions as dimmed, dashed pins with hover tooltips containing score metadata
  • Double-click simulation: Unsimulated pins can be double-clicked to trigger manual simulation via onSimulateUnsimulatedAction callback

Action Feed Integration

  • Synchronized filtering: Action cards in Suggested/Rejected/Selected lists now respect the same category/threshold filters as the overview
  • Shared action-type chips: Replaced inline classification logic with calls to classifyActionType() so the feed and overview stay in lock-step

Supporting Changes

  • New component: ActionTypeFilterChips — reusable single-select chip row for action-type filtering (used by overview, feed, and explore pairs)
  • App.tsx state management: Centralized overviewFilters state and computed unsimulatedActionIds/unsimulatedActionInfo from action_scores
  • Interaction logging: Added new event types for filter changes and unsimulated pin interactions

Implementation Details

  • Three-pass pin building: Unfiltered unitary pins → filtered combined pins → protected constituent detection → final filtered unitary list with dimmed-by-filter flag
  • Threshold semantics: Actions with null max_rho (divergent/islanded) bypass the threshold check so non-numeric outcomes remain visible when grey category is enabled
  • Deduplication: Unsimulated action ids are deduplicated across score-table types and filtered to exclude already-simulated actions
  • Backward compatibility: Default filter values ensure legacy call sites that predate new fields don't crash

https://claude.ai/code/session_01TJCZcgr9ZJ5WfqnpbEovNJ

claude and others added 12 commits April 21, 2026 08:37
…ed pins

Category toggles (solves / low margin / still overloaded / divergent-
islanded) plus All / None bulk toggles, a Max-loading slider (default
150%), and a "Show unsimulated" switch now live in the Remedial
Actions overview header. The filters drive both the pin layer on the
overview diagram and the Selected / Suggested / Rejected card lists
in the sidebar ActionFeed via a shared ActionOverviewFilters state
owned by App.tsx, so the two views stay in lock-step.

When "Show unsimulated" is on, every scored-but-not-yet-simulated
action (from result.action_scores) renders as a dimmed, dashed pin
on the N-1 map. Double-clicking a dimmed pin streams a manual
simulation via /api/simulate-and-variant-diagram — the same code
path the Manual Selection dropdown uses — and promotes the action
into the normal Simulated Actions list.

Three replay-log event types were added (overview_filter_changed,
overview_unsimulated_toggled, overview_unsimulated_pin_simulated)
and registered in both the TS and Python spec-conformance SPEC
tables.
Two follow-ups on the action-overview filters feature:

1. Popover placement in detached windows: the above/below + clamp
   heuristics used to fall back to `window.innerWidth/innerHeight`,
   which in a detached popup still returned the MAIN window's
   dimensions — so a pin clicked near the popup's bottom edge
   produced a popover that hid the pin. We now read
   `containerRef.current?.ownerDocument?.defaultView` and capture the
   popup's own viewport at click time so `decidePopoverPlacement` /
   `computePopoverStyle` reflect where the pin actually sits on
   screen.

2. Combined-action pin protection: filtering out an underlying
   unitary action would previously leave a combined-pin curve
   dangling without its endpoint. We now keep any unitary pin
   referenced by a passing combined pin visible with a new
   `dimmedByFilter` flag (rendered at opacity 0.4 with the washed-
   out severity palette), so the operator still sees the combined
   pin's two constituent actions as context. When both the combined
   pin AND the unitary fail the filter, the unitary is dropped as
   before.

Adds 3 popoverPlacement viewport tests + 2 detached-window popover
placement tests + 3 combined-action protection tests. Full suite:
973/973.
…-double-click

After double-clicking an unsimulated pin, `handleSimulateUnsimulatedAction`
in App.tsx already mirrors the Manual Selection flow: it streams the
simulation, primes the action-variant diagram, and calls
`wrappedManualActionAdded` which inserts the new entry into
`result.actions` and adds the id to `selectedActionIds` — same state
machine as the search-dropdown Add path.

Three new tests assert the user-observable outcome:
 - the dimmed dashed "unsimulated" pin disappears,
 - a fully-coloured pin (no `dimmedByFilter`, no `data-unsimulated`,
   no opacity override) replaces it at the same anchor,
 - and, when the parent also starred the new entry (as Manual Selection
   does), the pin picks up the selected styling (gold star).

Also hardens the stream reader in App.tsx to flush any trailing bytes
after the final read — belt-and-braces in case a future backend change
omits the final \n.
…adata

Hovering a dimmed "un-simulated" pin now shows the same triage data
the Manual Selection dropdown's score table exposes: action type,
score, rank inside the type bucket (e.g. "rank 3 of 12"), max score
in the bucket, plus MW start for load-shedding / renewable-curtailment
or tap start (with the allowed range) for PST actions.

App.tsx computes `unsimulatedActionInfo` alongside
`unsimulatedActionIds` from `result.action_scores`, ranking each
bucket by descending score so rank 1 is the top candidate. The
resulting `Record<id, UnsimulatedActionScoreInfo>` is threaded
through VisualizationPanel to ActionOverviewDiagram and handed to
`buildUnsimulatedActionPins`, which formats the multi-line SVG
`<title>`. When score info is absent (older session reloads, tests
that stub only the id list), the original generic tooltip still
applies.

Tests: 6 new svgUtils unit tests covering each tooltip branch
(generic fallback, scored, MW start, tap start with range, missing
lookup), plus 1 component-level test verifying the rendered
`<title>` carries the enriched text. Full suite: 982/982.
…elper

New single-select chip row (ALL / DISCO / RECO / LS / RC / OPEN /
CLOSE / PST) above the overview diagram — same affordance as the
Explore Pairs filter, now extracted into a shared component used
by both. Choosing a chip hides every pin (unitary, combined,
un-simulated) AND every card in the sidebar ActionFeed that
doesn't classify into the matching bucket.

Filter semantics mirror the existing category + threshold filters:
a combined-action pin stays visible when EITHER of its constituents
matches the chip, so filtering by DISCO doesn't hide a disco-plus-
reco combined action.

Wiring:
 - new `utils/actionTypes.ts` — `classifyActionType` + `matchesActionTypeFilter`
   + `ACTION_TYPE_FILTER_TOKENS`. Classifier now correctly distinguishes
   "Ouverture du poste" (open coupling) from "Ouverture de la ligne"
   (disco), fixing a desc-based mis-classification ActionFeed had too.
 - new `components/ActionTypeFilterChips.tsx` — presentational chip
   row, test-id-friendly, reused by ExplorePairsTab and
   ActionOverviewDiagram.
 - `types.ts` — `actionType: ActionTypeFilterToken` added to
   `ActionOverviewFilters`.
 - ActionOverviewDiagram reads / writes the filter through
   `onFiltersChange`, logs an `overview_filter_changed` event with
   `kind: 'action_type'`, and filters its three pin layers.
 - ActionFeed honours the shared filter for Selected / Suggested /
   Rejected card lists (combined-action cards use the same
   either-constituent-matches rule as the overview pins).
 - App.tsx seeds the initial filter with `actionType: 'all'`.
 - ExplorePairsTab now renders the same chip component.
 - Spec tables (TS + Python) accept `action_type` inside
   `overview_filter_changed.details`.

Tests: new classifier unit tests (incl. the coupling-vs-disco
regression), chip component tests, and 5 overview-level tests
covering chip render + filter-by-type + un-simulated pin filtering
by score-info type. Full suite: 1009/1009.
…ng classifier

Two small follow-ups on the filter work:

1. Header layout: the "📍 Remedial actions overview — N pins on the
   N-1 network" title and the dedicated action-type chip sub-banner
   were each taking ~30 px of vertical space on the visualisation
   panel. Merged into a single row: a compact 📍<count> counter
   (with the full phrasing as a tooltip), then the severity chips,
   All/None, threshold slider, Show-unsimulated toggle, a vertical
   separator, and the DISCO/RECO/LS/RC/OPEN/CLOSE/PST chip row —
   all inline. Claims back one row of diagram area.

2. Classifier: `node_merging_*` actions (which the recommender emits
   as close-coupling operations) were classified 'unknown' and
   disappeared when the operator picked the CLOSE chip — "node
   merging" contains neither "close_coupling" nor "fermeture", so
   none of the existing heuristics matched. The classifier now
   maps `node_merging` → close coupling and `node_splitting` → open
   coupling, from either the id or the score-table type.

Tests: updated the pin-counter assertion to read the new counter
test-id / tooltip, and added four classifier cases covering
node_merging / node_splitting via id and type. Full suite:
1012/1012.
…N coupling

The classifier used `(desc.includes('poste') && desc.includes('ouverture'))`
as a coupling signal. That pattern matches BOTH a coupling open
("Ouverture du poste 'X'") and a line breaker open
("Ouverture LINE DJ_OC dans le poste POSTE") — so line-disconnection
actions with a substation-qualified description were landing in the
OPEN bucket and, symmetrically, disappearing under the DISCO chip.

Two-step fix:

1. `utils/actionTypes.ts` now computes a separate `isCouplingSignal`
   based on id tokens (`coupling`, `busbar`, `noeud`, `node_merging`,
   `node_splitting`), score-table types, description markers (`coupl`
   / `busbar`) and the specific `"du poste 'X'"` phrasing where the
   poste itself is the target. OPEN/CLOSE are gated by this signal;
   DISCO/RECO only apply when the signal is absent.

2. ActionFeed and ExplorePairsTab had their own inline copies of the
   bucket heuristics with the same bug. They now both delegate to
   `classifyActionType` / `matchesActionTypeFilter`, so the three
   chip filters (manual selection dropdown, score table in that
   dropdown, Explore Pairs, action-overview, sidebar ActionFeed)
   share a single source of truth.

Tests: 6 new classifier cases covering line-vs-coupling with the
"dans le poste" and "du poste 'X'" phrasings, plus a line-reco
mirror and a close-coupling reco case. Full suite: 1018/1018.
Reconciles the action-overview filter / un-simulated-pin feature branch
with main's parallel svgUtils.ts → `./svg/*` decomposition.

The only conflict was `frontend/src/utils/svgUtils.ts` (both sides
rewrote it). Resolution: accept main's barrel file and port the
branch's additions into the decomposed modules:

- `svg/actionPinData.ts`: add optional `unsimulated` and
  `dimmedByFilter` fields on `ActionPinInfo`; export
  `actionPassesOverviewFilter`; extend `buildActionOverviewPins` with
  an optional `overviewFilter` param; add `buildUnsimulatedActionPins`
  + its internal tooltip helper.
- `svg/actionPinRender.ts`: extend `ApplyPinsOptions` with
  `unsimulatedPins` + `onUnsimulatedPinDoubleClick`; teach
  `resolvePinFill` / `renderUnitaryPin` about `dimmedByFilter`
  (washed-out fill, 0.4 opacity, `data-dimmed-by-filter` attr);
  add `renderUnsimulatedPin` for dashed grey previews;
  wire them into `applyActionOverviewPins`.
- `svgUtils.ts` barrel: re-export `actionPassesOverviewFilter` and
  `buildUnsimulatedActionPins`.

All 1079 frontend tests pass; typecheck + lint clean.

https://claude.ai/code/session_0176eivSfH1GiV9CPc1iGqMo
Extends the action-overview doc with the features landed in PR #105:

- Overview paragraph + ASCII diagram updated to show the consolidated
  single-row header (counter + category chips + All/None + threshold
  slider + Show-unsimulated + action-type chips) and the un-simulated
  pin glyph.
- New `Un-simulated action pin` sub-section under Pin anatomy: SVG
  structure, anchor fallback via minimal stub, grey palette, dashed
  stroke, single/double-click semantics, tooltip enrichment contract.
- New `Filter-dimmed constituent pin` sub-section: `dimmedByFilter`
  flag, `data-dimmed-by-filter` attribute, 0.4 opacity rationale.
- New top-level `Filtering` section: ActionOverviewFilters shape and
  defaults, severity categories, threshold slider semantics
  (including the null-max_rho bypass), action-type chip classifier
  rules (coupling signal, commits f356c2e + d479516 regressions),
  three-pass protected-constituent algorithm, shared predicate
  contract with ActionFeed, interaction-logging events.
- Auto-fit dependency list updated to include un-simulated pins.
- New interaction entry: double-click on un-simulated pin → manual
  simulation.
- Files table refreshed: svgUtils.ts is now a barrel, new rows for
  actionPinData.ts, actionPinRender.ts, ActionTypeFilterChips.tsx,
  actionTypes.ts.
- Test-coverage table refreshed with the new filter / un-simulated /
  action-type / protected-constituent / re-colour regression cases.
- Performance notes: filter memo granularity + single-append
  ordering for un-simulated render.

https://claude.ai/code/session_0176eivSfH1GiV9CPc1iGqMo
Refactor: Extract stateless helpers from mixins into focused modules
The committed `dist-standalone/standalone_v0.6.html` snapshot was
taken on Apr 20 (v0.6 release prep) and predates the action-overview
features that emit `overview_filter_changed`,
`overview_unsimulated_pin_simulated`, and `overview_unsimulated_toggled`.
The parity script defaulted to the (gitignored) unversioned
`standalone.html`, falling back to the legacy mirror when absent, so
CI reported the 3 events as missing against a stale artifact.

- Regenerate `standalone_v0.6.html` from the current React source via
  `npm run build:standalone` so the tracked bundle picks up the new
  events and matches the 54-event frontend inventory.
- Teach `check_standalone_parity.py` to pick the latest
  `dist-standalone/standalone_v*.html` when no unversioned build is
  present, so CI runs against the committed snapshot instead of the
  legacy file.
- Apply the `_DEFERRED_TYPES` exemption symmetrically: the auto-gen
  bundle now uses the same `buildConfigInteractionDetails()` pattern
  as the React source, so `config_loaded` / `settings_applied` must be
  deferred on both sides (was FE-only, silently masked while the
  legacy mirror was the de-facto target).
Refactor action overview pins and add filtering/unsimulated actions
@marota
marota merged commit 1884a71 into main Apr 22, 2026
8 checks passed
marota pushed a commit that referenced this pull request Apr 22, 2026
… dynamic fix

Captures the post-0.6.0 work: svgPatch DOM-recycling (PR #108),
Action Overview filters + unsimulated pins (PR #105, #107),
code-quality gate + 5 decomposition passes (PR #104, #106),
docs reorganisation (PR #103), App.tsx hook extraction
(PR #109), and the dynamic reco_ reconnection-action fix on
the current branch.

https://claude.ai/code/session_01Tzp2fdUas3Y9vNZy6dxxuC
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.

2 participants