Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f1e7831
Reformats slicer files
jellybean2004 Nov 13, 2025
37a8351
Enables multiple slicers on the same plot simultaneously
jellybean2004 Nov 13, 2025
beaada9
Creates a new plot for a new slicer of the same type
jellybean2004 Nov 13, 2025
54bc9ec
Fixes bug not properly removing slicers, and adds exception handling …
jellybean2004 Nov 13, 2025
29fb7d1
Patches slicer issues
jellybean2004 Nov 14, 2025
87e35b6
Adds slicer list and selection in params window
jellybean2004 Nov 14, 2025
15b1d1f
Auto selection of current slider in slicers list
jellybean2004 Nov 14, 2025
74e9166
Updates docs
jellybean2004 Nov 14, 2025
2c733c0
Implements single slicer delete button in slicer param window
jellybean2004 Nov 15, 2025
ca79d2d
Differentiate slicers with (colour-blind friendly!) colours
jellybean2004 Nov 15, 2025
313c4c4
Fixes create new slicer button
jellybean2004 Nov 15, 2025
77aaa27
Enables replacement of slicers
jellybean2004 Nov 15, 2025
80266a2
Changes slicer list to radio buttons
jellybean2004 Nov 15, 2025
daa8a4d
Removes unrequried try excepts
jellybean2004 Nov 15, 2025
a11dfc4
Moves Delete button in Slicer Params to Slicers list
jellybean2004 Nov 15, 2025
44ba688
Deletion bug fix
jellybean2004 Nov 15, 2025
69a61d8
Proper implementation of individual deletion
jellybean2004 Nov 15, 2025
da0e9e9
Check implementation from comments
jellybean2004 Nov 16, 2025
2243ec2
Patch for box deletion issues
jellybean2004 Nov 16, 2025
56b2e80
Refactor out the unique plot ID generation for slicers
jellybean2004 Nov 16, 2025
ff94b84
Combobox goes back to None after slicer is created from SP window
jellybean2004 Nov 17, 2025
0d7d807
Updates docs
jellybean2004 Nov 17, 2025
7a50785
Proper box sum handling
jellybean2004 Nov 17, 2025
d877cd7
Proper handling of slicer removal
jellybean2004 Nov 24, 2025
b5a78b6
Makes BoxSum and other slicer mutually exclusive
jellybean2004 Nov 24, 2025
e85c4de
Fixes slicer param startup not checking correct RB
jellybean2004 Nov 24, 2025
2613a19
None does not clear slicers
jellybean2004 Nov 24, 2025
7a501e2
Removes commented code
jellybean2004 Nov 24, 2025
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
12 changes: 9 additions & 3 deletions src/sas/qtgui/MainWindow/media/graph_help.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,14 @@ and the third is to send all sliced data to a single batch fit window.
Clicking *Apply* will create a slicer for each selected plot with the parameters entered in the 'Slicer Parameters' window of the 'Slicer' tab.
Depending on the options selected the data may then be saved, loaded as separate data sets in the data manager panel, and finally sent to fitting.

To remove a 'slicer', bring back the *Dataset menu* and select *Clear Slicer*. Alternatively, you can select *None* in the 'Slicer type' dropdown
list in the 'Slicer Parameters' dialog.
To remove a 'slicer', bring back the *Dataset menu* and select *Clear Slicers*.

If a slicer is active, another slicer can be added to the same plot by repeating the process above. *Clear Slicers* will remove all slicers from the plot.
Individual slicers can be removed by selecting the slicer and clicking on *Delete* in the Slicer Parameters window. Additional slicers can also be added by selecting a slicer from the drop down list labelled *Create slicer*.
Changing the slicer on the drop down list labelled *Change slicer* will change the currently selected slicer to the one selected from the drop down list.

.. note::
The Delete slicer action cannot be undone.

Unmasked circular average
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -402,4 +408,4 @@ is, the central value of each bin on the x-axis.

.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

.. note:: This help document was last modified by Paul Butler, 05 September, 2016
.. note:: This help document was last modified by Sujaya Shrestha, 17 November 2025
1 change: 0 additions & 1 deletion src/sas/qtgui/Plotting/BoxSum.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ def __init__(self, parent=None, model=None):

# Handle the Close button click
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(lambda:self.closeWidgetSignal.emit())

262 changes: 186 additions & 76 deletions src/sas/qtgui/Plotting/Plotter2D.py

Large diffs are not rendered by default.

216 changes: 212 additions & 4 deletions src/sas/qtgui/Plotting/SlicerParameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def __init__(self, parent=None, model=None, active_plots=None, validate_method=N
# Initially, Apply is disabled
self.cmdApply.setEnabled(False)

# Store models for each slicer - add this line
self.slicer_models = {}

# Mapping combobox index -> slicer module
self.callbacks = {
0: None,
Expand Down Expand Up @@ -109,6 +112,9 @@ def __init__(self, parent=None, model=None, active_plots=None, validate_method=N
# Set up plots list
self.setPlotsList()

# Set up slicers list - add this line
self.setSlicersList()

def setParamsList(self):
"""
Create and initially populate the list of parameters
Expand All @@ -128,6 +134,88 @@ def updatePlotList(self):
self.active_plots = self.parent.getActivePlots()
self.setPlotsList()

def getCurrentSlicerDict(self):
"""
Returns a dictionary of currently shown slicers
{slicer_name:checkbox_status}
"""
current_slicers = {}
for row in range(self.lstSlicers.count()):
item = self.lstSlicers.item(row)
isChecked = item.checkState() != QtCore.Qt.Unchecked
slicer = item.text()
current_slicers[slicer] = isChecked
return current_slicers

def setSlicersList(self):
"""
Create and initially populate the list of slicers with radio button behavior
"""
current_slicer_text = self.getCheckedSlicer() # Already returns string now

self.lstSlicers.clear()

# Create a button group for radio button behavior
if not hasattr(self, 'slicerButtonGroup'):
self.slicerButtonGroup = QtWidgets.QButtonGroup(self)
self.slicerButtonGroup.setExclusive(True)

# Determine which slicer should be selected based on the current model
slicer_to_select = None
if self.model is not None:
# Find which slicer has this model
for slicer_name, slicer_obj in self.parent.slicers.items():
if hasattr(slicer_obj, '_model') and slicer_obj._model is self.model:
slicer_to_select = slicer_name
break

# Fill out list of slicers
for idx, item in enumerate(self.parent.slicers):
# Create a widget to hold the radio button
widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(widget)
layout.setContentsMargins(5, 2, 5, 2)

# Create radio button
radio = QtWidgets.QRadioButton(str(item))

# Check if this should be selected
should_select = (slicer_to_select == str(item)) or (slicer_to_select is None and idx == 0)
radio.setChecked(should_select)

# Add to button group
self.slicerButtonGroup.addButton(radio, idx)

layout.addWidget(radio)
layout.addStretch()

# Create list item
listItem = QtWidgets.QListWidgetItem(self.lstSlicers)
listItem.setSizeHint(widget.sizeHint())

# Add to list
self.lstSlicers.addItem(listItem)
self.lstSlicers.setItemWidget(listItem, widget)

# Store the slicer's model
if item in self.parent.slicers:
slicer_obj = self.parent.slicers[item]
if hasattr(slicer_obj, '_model'):
self.slicer_models[str(item)] = slicer_obj._model

# Connect radio button to update handler
radio.toggled.connect(lambda checked, name=str(item): self.onSlicerRadioToggled(checked, name))

def getCheckedSlicer(self):
"""
Returns the currently checked slicer (radio button)
"""
if not hasattr(self, 'slicerButtonGroup'):
return None

checked_button = self.slicerButtonGroup.checkedButton()
return checked_button.text() if checked_button else None

def getCurrentPlotDict(self):
"""
Returns a dictionary of currently shown plots
Expand Down Expand Up @@ -181,6 +269,9 @@ def setSlots(self):
# Apply slicer to selected plots
self.cmdApply.clicked.connect(self.onApply)

# Delete slicer
self.cmdDelete.clicked.connect(self.onDelete)

# Initialize slicer combobox to the current slicer
current_slicer = type(self.parent.slicer)
for index in self.callbacks:
Expand All @@ -190,6 +281,9 @@ def setSlots(self):
# change the slicer type
self.cbSlicer.currentIndexChanged.connect(self.onSlicerChanged)

# Replace slicer type
self.cbSlicerReplace.currentIndexChanged.connect(self.onSlicerReplaceChanged)

# Updates to the slicer moder must propagate to all plotters
if self.model is not None:
self.model.itemChanged.connect(self.onParamChange)
Expand All @@ -204,16 +298,41 @@ def onFocus(self, row, column):
self.lstParams.setSelectionModel(selection_model)
self.lstParams.setCurrentIndex(self.model.index(row, column))

def onSlicerChanged(self, index):
def onSlicerChanged(self, index: int):
"""change the parameters based on the slicer chosen"""
if index == 0: # No interactor
self.parent.onClearSlicer()
self.setModel(None)
self.onGeneratePlots(False)
return
# self.parent.onClearSlicer()
# self.setModel(None)
# self.onGeneratePlots(False)
else:
slicer = self.callbacks[index]
if self.active_plots.keys():
self.parent.setSlicer(slicer=slicer)
# Reset combo box back to "None" after setting slicer
self.cbSlicer.blockSignals(True)
self.cbSlicer.setCurrentIndex(0)
self.cbSlicer.blockSignals(False)
self.onParamChange()

def onSlicerReplaceChanged(self, index: int):
"""replace the slicer with the one chosen"""
if index == 0: # No interactor
return
# self.parent.onClearSlicer()
# self.setModel(None)
# self.onGeneratePlots(False)
else:
# delete the currently selected slicer
self.onDelete()
# add the new slicer
slicer = self.callbacks[index]
if self.active_plots.keys():
self.parent.setSlicer(slicer=slicer)
# Reset combo box back to "None" after setting slicer
self.cbSlicer.blockSignals(True)
self.cbSlicer.setCurrentIndex(0)
self.cbSlicer.blockSignals(False)
self.onParamChange()

def onGeneratePlots(self, isChecked):
Expand Down Expand Up @@ -289,6 +408,46 @@ def onApply(self):
self.save1DPlotsForPlot(plots)
pass # debug anchor

def onDelete(self):
"""
Delete the current slicer
"""
# Pop up a confirmation dialog
reply = QtWidgets.QMessageBox.question(self, 'Delete Slicer',
'Are you sure you want to delete this slicer?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
# Get the current slicer name
slicer_item = self.getCheckedSlicer()
# Remove the slicer from the dictionary
if slicer_item in self.parent.slicers:
slicer_obj = self.parent.slicers[slicer_item]
# Clear only this slicer's markers without affecting other slicers
# Use clear_markers() which uses connect.clear(*markers) instead of clearall()
self._clear_slicer_markers(slicer_obj)
# Remove from dictionary
del self.parent.slicers[slicer_item]
# Remove from slicer_models cache
if slicer_item in self.slicer_models:
del self.slicer_models[slicer_item]
# update the canvas
self.parent.canvas.draw()
# update the slicer list
self.updateSlicersList()
# Select the next remaining slicer if any exist
if len(self.parent.slicers) > 0:
# Get the first remaining slicer
next_slicer_name = next(iter(self.parent.slicers.keys()))
# Update self.parent.slicer to point to the remaining slicer
self.parent.slicer = self.parent.slicers[next_slicer_name]
self.checkSlicerByName(next_slicer_name)
else:
# No slicers left, clear the model and slicer reference
self.parent.slicer = None
self.setModel(None)

def applyPlotter(self, plot):
"""
Apply the current slicer to a plot
Expand Down Expand Up @@ -437,6 +596,55 @@ def onHelp(self):
url = "/user/qtgui/MainWindow/graph_help.html#d-data-averaging"
self.manager.parent.showHelp(url)

def _clear_slicer_markers(self, slicer_obj):
"""
Clear the slicer by calling its clear() method.
The slicer's clear() method handles all cleanup for that specific slicer type.
"""
if hasattr(slicer_obj, 'clear'):
slicer_obj.clear()

def updateSlicersList(self):
"""
Update the slicers list when slicers are added or removed
"""
self.setSlicersList()

def checkSlicerByName(self, slicer_name):
"""
Check (select) a slicer radio button by name
"""
if not hasattr(self, 'slicerButtonGroup'):
return

# Find and check the radio button with this slicer name
for button in self.slicerButtonGroup.buttons():
if button.text() == slicer_name:
button.setChecked(True)
break

def onSlicerRadioToggled(self, checked, slicer_name):
"""
Update parameter list when a slicer radio button is toggled
"""
if not checked:
return

# Check if "None" was selected - don't clear everything, just update the view
if slicer_name not in self.parent.slicers:
# No valid slicer selected, but don't clear
return

# Update the parameter model to show this slicer's parameters
if slicer_name in self.slicer_models:
self.setModel(self.slicer_models[slicer_name])
elif slicer_name in self.parent.slicers:
# Get the model from the slicer object
slicer_obj = self.parent.slicers[slicer_name]
if hasattr(slicer_obj, '_model'):
if slicer_name not in self.slicer_models:
self.slicer_models[slicer_name] = slicer_obj._model
self.setModel(slicer_obj._model)

class ProxyModel(QtCore.QIdentityProxyModel):
"""
Expand Down
Loading