Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions graphs/gui/canvasPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,60 @@
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


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:
# Reverse matchup means target ship becomes attacker and vice versa.
reverse_src_key = _graph_item_key(t.item)
reverse_tgt_key = _graph_item_key(s.item)
if reverse_src_key is None or reverse_tgt_key is None:
continue
rev_s = src_by_key.get(reverse_src_key)
rev_t = tgt_by_key.get(reverse_tgt_key)
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

Expand Down Expand Up @@ -116,9 +163,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)

Expand Down
40 changes: 40 additions & 0 deletions graphs/gui/ctrlPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions graphs/gui/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@


# noinspection PyPackageRequirements
from contextlib import nullcontext

import wx
from logbook import Logger

import gui.display
import gui.globalEvents as GE
import gui.mainFrame
from eos.saveddata.fit import Fit as EosFit
from graphs.data.base import FitGraph
from graphs.events import RESIST_MODE_CHANGED
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.fit import Fit
from service.settings import GraphSettings
from . import canvasPanel
from .ctrlPanel import GraphControlPanel
Expand Down Expand Up @@ -239,7 +243,32 @@ def getView(self, idx=None):
def clearCache(self, reason, extraData=None):
self.getView().clearCache(reason, extraData)

def _ensureGraphFitsRecalculated(self):
"""
Recalculate every fit shown in the graph when multiple ships are listed.

The main window only runs a full local calculation for the active tab. Other
loaded fits can keep stale ship attributes for incoming projections (mutual
projected effects) until they become active, which breaks multi-fit graphs.
"""
ctrl = self.ctrlPanel
sFit = Fit.getInstance()
seen = set()
fits = []
for wrapper in ctrl.sources + ctrl.targets:
if not wrapper.isFit or wrapper.item.ID in seen:
continue
seen.add(wrapper.item.ID)
fits.append(wrapper.item)
if len(fits) < 2:
return
suspend_ctx = getattr(EosFit, 'suspendVictimCalcReset', None)
with (suspend_ctx() if callable(suspend_ctx) else nullcontext()):
for fit in fits:
sFit.recalc(fit)

def draw(self):
self._ensureGraphFitsRecalculated()
self.canvasPanel.draw()

def resetXMark(self):
Expand Down
52 changes: 33 additions & 19 deletions graphs/gui/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down