From 55e7279586397abd7d24ae3574e12f0494a16cf1 Mon Sep 17 00:00:00 2001 From: Skybladev2 Date: Fri, 3 Apr 2026 22:48:53 +0300 Subject: [PATCH] filter graphs on selection --- graphs/gui/canvasPanel.py | 52 +++++++++++++++++++++++++++++++++++++-- graphs/gui/ctrlPanel.py | 40 ++++++++++++++++++++++++++++++ graphs/gui/lists.py | 52 +++++++++++++++++++++++++-------------- 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/graphs/gui/canvasPanel.py b/graphs/gui/canvasPanel.py index 4c862f6001..3fb57f1946 100644 --- a/graphs/gui/canvasPanel.py +++ b/graphs/gui/canvasPanel.py @@ -30,6 +30,8 @@ from logbook import Logger +from eos.saveddata.fit import Fit +from eos.saveddata.targetProfile import TargetProfile from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES, hsl_to_hsv from gui.utils.numberFormatter import roundToPrec @@ -37,6 +39,50 @@ pyfalog = Logger(__name__) +def _graph_item_key(item): + if isinstance(item, Fit): + return ('fit', item.ID) + if isinstance(item, TargetProfile): + return ('profile', item.ID) + return None + + +def expand_reverse_filtered_matchups(ctrl, base_pairs): + """ + For each (attacker, target) in base_pairs, optionally add (targetShip as attacker, attackerShip as target) + using the SourceWrapper / TargetWrapper instances from the full lists when present. + """ + if not ctrl.showReverseFilteredMatchups: + return base_pairs + src_by_key = {} + for w in ctrl.sources: + k = _graph_item_key(w.item) + if k: + src_by_key[k] = w + tgt_by_key = {} + for w in ctrl.targets: + k = _graph_item_key(w.item) + if k: + tgt_by_key[k] = w + seen = set((id(s), id(t)) for s, t in base_pairs) + out = list(base_pairs) + for s, t in base_pairs: + ks = _graph_item_key(t.item) + kt = _graph_item_key(s.item) + if ks is None or kt is None: + continue + rev_s = src_by_key.get(ks) + rev_t = tgt_by_key.get(kt) + if rev_s is None or rev_t is None: + continue + key = (id(rev_s), id(rev_t)) + if key in seen: + continue + seen.add(key) + out.append((rev_s, rev_t)) + return out + + try: import matplotlib as mpl @@ -116,9 +162,11 @@ def draw(self, accurateMarks=True): mainInput, miscInputs = self.graphFrame.ctrlPanel.getValues() view = self.graphFrame.getView() - sources = self.graphFrame.ctrlPanel.sources + ctrl = self.graphFrame.ctrlPanel + sources = ctrl.filteredSources if view.hasTargets: - iterList = tuple(itertools.product(sources, self.graphFrame.ctrlPanel.targets)) + base_pairs = list(itertools.product(sources, ctrl.filteredTargets)) + iterList = tuple(expand_reverse_filtered_matchups(ctrl, base_pairs)) else: iterList = tuple((f, None) for f in sources) diff --git a/graphs/gui/ctrlPanel.py b/graphs/gui/ctrlPanel.py index 418bbe468d..819eb089d8 100644 --- a/graphs/gui/ctrlPanel.py +++ b/graphs/gui/ctrlPanel.py @@ -76,6 +76,13 @@ def __init__(self, graphFrame, parent): self.showY0Cb.SetValue(True) self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change) commonOptsSizer.Add(self.showY0Cb, 0, wx.EXPAND | wx.TOP, 5) + self.reverseFilteredMatchupsCb = wx.CheckBox( + self, wx.ID_ANY, _t('Include reverse matchups when filtered'), wx.DefaultPosition, wx.DefaultSize, 0) + self.reverseFilteredMatchupsCb.SetValue(False) + self.reverseFilteredMatchupsCb.Bind(wx.EVT_CHECKBOX, self.OnReverseFilteredMatchupsChange) + self.reverseFilteredMatchupsCb.SetToolTip(wx.ToolTip(_t( + 'Also plot target→attacker for the same ships. Add each ship to both attacker and target lists.'))) + commonOptsSizer.Add(self.reverseFilteredMatchupsCb, 0, wx.EXPAND | wx.TOP, 5) optsSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.RIGHT, 10) graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -158,6 +165,7 @@ def updateControls(self, layout=True): # Source and target list self.refreshColumns(layout=False) self.targetList.Show(view.hasTargets) + self.reverseFilteredMatchupsCb.Show(view.hasTargets) # Inputs self._updateInputs(storeInputs=False) @@ -327,6 +335,10 @@ def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() + def OnReverseFilteredMatchupsChange(self, event): + event.Skip() + self.graphFrame.draw() + def OnYTypeUpdate(self, event): event.Skip() self._updateInputs() @@ -417,6 +429,34 @@ def sources(self): def targets(self): return self.targetList.wrappers + @property + def filteredSources(self): + srcs = self.sources + selected = self.sourceList.getSelectedWrappers() + if not selected: + return srcs + sel = set(selected) + return [w for w in srcs if w in sel] + + @property + def filteredTargets(self): + tgts = self.targets + selected = self.targetList.getSelectedWrappers() + if not selected: + return tgts + sel = set(selected) + return [w for w in tgts if w in sel] + + @property + def isGraphFiltered(self): + return bool(self.sourceList.getSelectedWrappers()) or bool(self.targetList.getSelectedWrappers()) + + @property + def showReverseFilteredMatchups(self): + if not self.graphFrame.getView().hasTargets or not self.isGraphFiltered: + return False + return self.reverseFilteredMatchupsCb.GetValue() + # Fit events def OnFitRenamed(self, event): self.sourceList.OnFitRenamed(event) diff --git a/graphs/gui/lists.py b/graphs/gui/lists.py index a63efebcd8..2b682074c8 100644 --- a/graphs/gui/lists.py +++ b/graphs/gui/lists.py @@ -44,8 +44,11 @@ def __init__(self, graphFrame, parent): self.hoveredRow = None self.hoveredColumn = None + self._graphSelectionRedrawPending = False self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnGraphListSelectionChanged) + self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnGraphListSelectionChanged) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) self.Bind(wx.EVT_MOTION, self.OnMouseMove) @@ -56,6 +59,16 @@ def wrappers(self): # Sort fits first, then target profiles return sorted(self._wrappers, key=lambda w: not w.isFit) + def OnGraphListSelectionChanged(self, event): + event.Skip() + if not self._graphSelectionRedrawPending: + self._graphSelectionRedrawPending = True + wx.CallAfter(self._flushGraphSelectionRedraw) + + def _flushGraphSelectionRedraw(self): + self._graphSelectionRedrawPending = False + self.graphFrame.draw() + # UI-related stuff @property def defaultTTText(self): @@ -121,23 +134,24 @@ def handleDrag(self, type, fitID): def OnLeftDown(self, event): row, _ = self.HitTest(event.Position) - if row != -1: - pickers = { - self.getColIndex(GraphColor): ColorPickerPopup, - self.getColIndex(GraphLightness): LightnessPickerPopup, - self.getColIndex(GraphLineStyle): LineStylePickerPopup} - # In case we had no index for some column, remove None - pickers.pop(None, None) - col = self.getColumn(event.Position) - if col in pickers: - picker = pickers[col] - wrapper = self.getWrapper(row) - if wrapper is not None: - win = picker(parent=self, wrapper=wrapper) - pos = wx.GetMousePosition() - win.Position(pos, (0, 0)) - win.Popup() - return + if row == -1: + self.unselectAll() + event.Skip() + return + pickers = { + self.getColIndex(GraphColor): ColorPickerPopup, + self.getColIndex(GraphLightness): LightnessPickerPopup, + self.getColIndex(GraphLineStyle): LineStylePickerPopup} + pickers.pop(None, None) + col = self.getColumn(event.Position) + if col in pickers: + wrapper = self.getWrapper(row) + if wrapper is not None: + win = pickers[col](parent=self, wrapper=wrapper) + pos = wx.GetMousePosition() + win.Position(pos, (0, 0)) + win.Popup() + return event.Skip() def OnLineStyleChange(self): @@ -310,7 +324,7 @@ def spawnMenu(self, event): @property def defaultTTText(self): - return _t('Drag a fit into this list to graph it') + return _t('Drag a fit into this list to graph it. Select rows to filter attackers (Shift/Ctrl); click empty space to show all attackers.') class TargetWrapperList(BaseWrapperList): @@ -367,7 +381,7 @@ def OnResistModeChanged(self, event): @property def defaultTTText(self): - return _t('Drag a fit into this list to have your fits graphed against it') + return _t('Drag a fit into this list to have your fits graphed against it. Select rows to filter targets (Shift/Ctrl); click empty space to show all targets.') # Context menu handlers def addProfile(self, profile):