From ee87fd94900a3662b094ee4a43c88d8bb9bed788 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 28 Oct 2015 11:32:02 +0000 Subject: [PATCH] TimeSeriesControlPanel refactorings, should now be less buggy. New option on TimeSeriesPanel - plotMelodicICs, which allows melodic images to be treated as regular 4D images. --- fsl/data/strings.py | 2 + .../controls/timeseriescontrolpanel.py | 139 +++++++++++------- fsl/fsleyes/plotting/timeseries.py | 6 +- fsl/fsleyes/tooltips.py | 5 + fsl/fsleyes/views/timeseriespanel.py | 52 +++++-- 5 files changed, 136 insertions(+), 68 deletions(-) diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 6405b52ac..27ec5ed61 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -384,6 +384,8 @@ properties = TypeDict({ 'TimeSeriesPanel.plotMode' : 'Plotting mode', 'TimeSeriesPanel.usePixdim' : 'Use pixdims', + 'TimeSeriesPanel.plotMelodicICs' : 'Plot component time courses for ' + 'Melodic images', 'TimeSeriesPanel.showMode' : 'Time series to plot', 'TimeSeriesPanel.plotFullModelFit' : 'Plot full model fit', 'TimeSeriesPanel.plotResiduals' : 'Plot residuals', diff --git a/fsl/fsleyes/controls/timeseriescontrolpanel.py b/fsl/fsleyes/controls/timeseriescontrolpanel.py index 683a989ec..4d755f2b7 100644 --- a/fsl/fsleyes/controls/timeseriescontrolpanel.py +++ b/fsl/fsleyes/controls/timeseriescontrolpanel.py @@ -15,6 +15,7 @@ import props import pwidgets.widgetlist as widgetlist import fsl.fsleyes.panel as fslpanel +import fsl.fsleyes.displaycontext as fsldisplay import fsl.fsleyes.plotting.timeseries as timeseries import fsl.fsleyes.tooltips as fsltooltips import fsl.data.strings as strings @@ -23,7 +24,7 @@ import fsl.data.strings as strings class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): """The ``TimeSeriesControlPanel`` is a :class:`.FSLEyesPanel` which allows the user to configure a :class:`.TimeSeriesPanel`. It contains controls - which are linked to the properties of the :class:`.TImeSeriesPanel`, + which are linked to the properties of the ``TimeSeriesPanel``, (which include properties defined on the :class:`.PlotPanel` base class), and the :class:`.TimeSeries` class. @@ -79,7 +80,8 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): tsProps = ['showMode', 'plotMode', - 'usePixdim'] + 'usePixdim', + 'plotMelodicICs'] plotProps = ['xLogScale', 'yLogScale', 'smooth', @@ -162,8 +164,9 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): self.__selectedOverlayChanged) # This attribute keeps track of the currently - # selected overlay, but only if said overlay - # is a FEATImage. + # selected overlay, so the widget list group + # names can be updated if the overlay name + # changes. self.__selectedOverlay = None self.__selectedOverlayChanged() @@ -185,15 +188,21 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): def __selectedOverlayNameChanged(self, *a): """Called when the :attr:`.Display.name` property for the currently - selected overlay changes. Only called if the current overlay is a - :class:`.FEATImage`. Updates the display name of the *FEAT plot - settings* section. + selected overlay changes. Updates the display name of the *FEAT plot + settings* and *current time course* sections if necessary. """ display = self._displayCtx.getDisplay(self.__selectedOverlay) - self.__widgets.RenameGroup( - 'currentFEATSettings', - strings.labels[self, 'currentFEATSettings'].format( - display.name)) + + if self.__widgets.HasGroup('currentFEATSettings'): + self.__widgets.RenameGroup( + 'currentFEATSettings', + strings.labels[self, 'currentFEATSettings'].format( + display.name)) + + if self.__widgets.HasGroup('currentSettings'): + self.__widgets.RenameGroup( + 'currentSettings', + strings.labels[self, 'currentSettings'].format(display.name)) def __selectedOverlayChanged(self, *a): @@ -205,95 +214,121 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): # We're assuminbg that the TimeSeriesPanel has # already updated its current TimeSeries for # the newly selected overlay. - if self.__selectedOverlay is not None: - display = self._displayCtx.getDisplay(self.__selectedOverlay) - display.removeListener('name', self._name) + try: + display = self._displayCtx.getDisplay(self.__selectedOverlay) + display.removeListener('name', self._name) + + # The overlay may have been + # removed from the overlay list + except fsldisplay.InvalidOverlayError: + pass + self.__selectedOverlay = None + if self.__widgets.HasGroup('currentSettings'): + self.__widgets.RemoveGroup('currentSettings') + if self.__widgets.HasGroup('currentFEATSettings'): self.__widgets.RemoveGroup('currentFEATSettings') overlay = self._displayCtx.getSelectedOverlay() - ts = self.__tsPanel.getTimeSeries(overlay) - - self.__showSettingsForCurrentTimeSeries() - if ts is None or not isinstance(ts, timeseries.FEATTimeSeries): + if overlay is None: return - display = self._displayCtx.getDisplay(overlay) + ts = self.__tsPanel.getTimeSeries(overlay) + + if ts is None: + return self.__selectedOverlay = overlay + display = self._displayCtx.getDisplay(overlay) + display.addListener('name', self._name, self.__selectedOverlayNameChanged) - self.__widgets.AddGroup( + self.__showSettingsForTimeSeries(ts) + + if isinstance(ts, timeseries.FEATTimeSeries): + self.__showFEATSettingsForTimeSeries(ts) + + + def __showFEATSettingsForTimeSeries(self, ts): + """(Re-)crates the *FEAT settings* section for the given + :class:`.FEATTimeSeries` instance. + """ + + overlay = ts.overlay + display = self._displayCtx.getDisplay(overlay) + widgets = self.__widgets + + widgets.AddGroup( 'currentFEATSettings', displayName=strings.labels[self, 'currentFEATSettings'].format( display.name)) - full = props.makeWidget( self.__widgets, ts, 'plotFullModelFit') - res = props.makeWidget( self.__widgets, ts, 'plotResiduals') - evs = props.makeListWidgets(self.__widgets, ts, 'plotEVs') - pes = props.makeListWidgets(self.__widgets, ts, 'plotPEFits') - copes = props.makeListWidgets(self.__widgets, ts, 'plotCOPEFits') - partial = props.makeWidget( self.__widgets, ts, 'plotPartial') - data = props.makeWidget( self.__widgets, ts, 'plotData') + full = props.makeWidget( widgets, ts, 'plotFullModelFit') + res = props.makeWidget( widgets, ts, 'plotResiduals') + evs = props.makeListWidgets(widgets, ts, 'plotEVs') + pes = props.makeListWidgets(widgets, ts, 'plotPEFits') + copes = props.makeListWidgets(widgets, ts, 'plotCOPEFits') + partial = props.makeWidget( widgets, ts, 'plotPartial') + data = props.makeWidget( widgets, ts, 'plotData') - self.__widgets.AddWidget( + widgets.AddWidget( data, displayName=strings.properties[ts, 'plotData'], tooltip=fsltooltips.properties[ts, 'plotData'], groupName='currentFEATSettings') - self.__widgets.AddWidget( + widgets.AddWidget( full, displayName=strings.properties[ts, 'plotFullModelFit'], tooltip=fsltooltips.properties[ts, 'plotFullModelFit'], groupName='currentFEATSettings') - self.__widgets.AddWidget( + widgets.AddWidget( res, displayName=strings.properties[ts, 'plotResiduals'], tooltip=fsltooltips.properties[ts, 'plotResiduals'], groupName='currentFEATSettings') - self.__widgets.AddWidget( + widgets.AddWidget( partial, displayName=strings.properties[ts, 'plotPartial'], tooltip=fsltooltips.properties[ts, 'plotPartial'], groupName='currentFEATSettings') - self.__widgets.AddSpace(groupName='currentFEATSettings') + widgets.AddSpace(groupName='currentFEATSettings') for i, ev in enumerate(evs): - evName = ts.overlay.evNames()[i] - self.__widgets.AddWidget( + evName = overlay.evNames()[i] + widgets.AddWidget( ev, displayName=strings.properties[ts, 'plotEVs'].format( i + 1, evName), tooltip=fsltooltips.properties[ts, 'plotEVs'], groupName='currentFEATSettings') - self.__widgets.AddSpace(groupName='currentFEATSettings') + widgets.AddSpace(groupName='currentFEATSettings') for i, pe in enumerate(pes): - evName = ts.overlay.evNames()[i] - self.__widgets.AddWidget( + evName = overlay.evNames()[i] + widgets.AddWidget( pe, displayName=strings.properties[ts, 'plotPEFits'].format( i + 1, evName), tooltip=fsltooltips.properties[ts, 'plotPEFits'], groupName='currentFEATSettings') - self.__widgets.AddSpace(groupName='currentFEATSettings') + widgets.AddSpace(groupName='currentFEATSettings') copeNames = overlay.contrastNames() for i, (cope, name) in enumerate(zip(copes, copeNames)): - self.__widgets.AddWidget( + widgets.AddWidget( cope, displayName=strings.properties[ts, 'plotCOPEFits'].format( i + 1, name), @@ -301,22 +336,14 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): groupName='currentFEATSettings') - def __showSettingsForCurrentTimeSeries(self): + def __showSettingsForTimeSeries(self, ts): """Called by the :meth:`__selectedOverlayChanged` method. Refreshes - the *Settings for the current time course* section. + the *Settings for the current time course* section, for the given + :class:`.TimeSeries` instance. """ - widgets = self.__widgets - tsPanel = self.__tsPanel - overlay = self._displayCtx.getSelectedOverlay() - ts = tsPanel.getTimeSeries(overlay) - - if widgets.HasGroup('currentSettings'): - widgets.RemoveGroup('currentSettings') - - if ts is None: - return - + overlay = ts.overlay display = self._displayCtx.getDisplay(overlay) + widgets = self.__widgets self.__widgets.AddGroup( 'currentSettings', @@ -332,22 +359,22 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel): 'lineStyle', labels=strings.choices['DataSeries.lineStyle']) - self.__widgets.AddWidget( + widgets.AddWidget( colour, displayName=strings.properties[ts, 'colour'], tooltip=fsltooltips.properties[ts, 'colour'], groupName='currentSettings') - self.__widgets.AddWidget( + widgets.AddWidget( alpha, displayName=strings.properties[ts, 'alpha'], tooltip=fsltooltips.properties[ts, 'alpha'], groupName='currentSettings') - self.__widgets.AddWidget( + widgets.AddWidget( lineWidth, displayName=strings.properties[ts, 'lineWidth'], tooltip=fsltooltips.properties[ts, 'lineWidth'], groupName='currentSettings') - self.__widgets.AddWidget( + widgets.AddWidget( lineStyle, displayName=strings.properties[ts, 'lineStyle'], tooltip=fsltooltips.properties[ts, 'lineStyle'], diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py index 32966e672..c60cfdb99 100644 --- a/fsl/fsleyes/plotting/timeseries.py +++ b/fsl/fsleyes/plotting/timeseries.py @@ -586,7 +586,7 @@ class FEATEVTimeSeries(TimeSeries): def getData(self): """Returns the time course of the EV specified in the constructor. """ - data = self.overlay.getDesign()[:, self.idx] + data = np.array(self.overlay.getDesign()[:, self.idx]) return TimeSeries.getData(self, ydata=data) @@ -629,7 +629,7 @@ class FEATResidualTimeSeries(TimeSeries): return [], [] x, y, z = voxel - data = self.overlay.getResiduals().data[x, y, z, :] + data = np.array(self.overlay.getResiduals().data[x, y, z, :]) return TimeSeries.getData(self, ydata=data) @@ -755,5 +755,5 @@ class MelodicTimeSeries(TimeSeries): """Returns the time course of the current Melodic component. """ component = self.getComponent() - ydata = self.overlay.getComponentTimeSeries(component) + ydata = np.array(self.overlay.getComponentTimeSeries(component)) return TimeSeries.getData(self, ydata=ydata) diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py index 2c61a79b6..ca71b292e 100644 --- a/fsl/fsleyes/tooltips.py +++ b/fsl/fsleyes/tooltips.py @@ -256,6 +256,11 @@ properties = TypeDict({ 'scaled by the time dimension pixdim ' 'value specified in the NIFTI1 ' 'header.', + 'TimeSeriesPanel.plotMelodicICs' : 'If checked, the component time ' + 'courses are plotted for Melodic ' + 'images. If not checked, Melodic ' + 'images are treated as regular 4D ' + 'images.', 'TimeSeriesPanel.showMode' : 'Choose which time series to plot - ' 'you can choose to plot the time ' 'series for the currently selected ' diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py index 37093abda..02d2c6dce 100644 --- a/fsl/fsleyes/views/timeseriespanel.py +++ b/fsl/fsleyes/views/timeseriespanel.py @@ -152,6 +152,14 @@ class TimeSeriesPanel(plotpanel.PlotPanel): """ + plotMelodicICs = props.Boolean(default=True) + """If ``True``, the component time courses are plotted for + :class:`.MelodicImage` overlays (using a :class:`.MelodicTimeSeries` + instance). Otherwise, ``MelodicImage`` overlays are treated as regular + 4D :class:`.Image` overlays (a :class:`.TimeSeries` instance is used). + """ + + def __init__(self, parent, overlayList, displayCtx): """Create a ``TimeSeriesPanel``. @@ -185,9 +193,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel): self .addListener('usePixdim', self._name, self.draw) self .addListener('showMode', self._name, self.draw) displayCtx .addListener('selectedOverlay', self._name, self.draw) + self .addListener('plotMelodicICs', + self._name, + self.__plotMelodicICsChanged) overlayList.addListener('overlays', self._name, - self.__overlayListChanged) + self.__overlayListChanged) # The currentTss attribute is a dictionary of # @@ -304,18 +315,40 @@ class TimeSeriesPanel(plotpanel.PlotPanel): ds.destroy() for overlay in list(self.__currentTss.keys()): - - ts = self.__currentTss.pop(overlay) + if overlay not in self._overlayList: + self.__clearCacheForOverlay(overlay) + + self.__updateCurrentTimeSeries() + self.draw() + + + def __clearCacheForOverlay(self, overlay): + """Destroys the internally cached :class:`.TimeSeries` for the given + overlay. + """ + + ts = self.__currentTss .pop(overlay, None) + targets, propNames = self.__refreshProps.pop(overlay, ([], [])) + + if ts is not None: ts.destroy() - for overlay, (targets, propNames) in list(self.__refreshProps.items()): - if overlay not in self._overlayList: - self.__refreshProps.pop(overlay) + for t, p in zip(targets, propNames): + t.removeListener(p, self._name) + + + def __plotMelodicICsChanged(self, *a): + """Called when the :attr:`plotMelodicICs` property changes. Re-creates + the internally cached :class:`.TimeSeries` instances for all + :class:`.MelodicImage` overlays in the :class:`.OverlayList`. + """ - for t, p in zip(targets, propNames): - t.removeListener(p, self._name) + for overlay in self._overlayList: + if isinstance(overlay, fslmelimage.MelodicImage): + self.__clearCacheForOverlay(overlay) self.__updateCurrentTimeSeries() + self.draw() def __updateCurrentTimeSeries(self, *a): @@ -373,7 +406,8 @@ class TimeSeriesPanel(plotpanel.PlotPanel): targets = [self._displayCtx] propNames = ['location'] - elif isinstance(overlay, fslmelimage.MelodicImage): + elif isinstance(overlay, fslmelimage.MelodicImage) and \ + self.plotMelodicICs: ts = timeseries.MelodicTimeSeries(self, overlay, self._displayCtx) targets = [self._displayCtx.getOpts(overlay)] propNames = ['volume'] -- GitLab