Skip to content

Commit 46941be

Browse files
committed
fix: implement log scale toggle for heatmaps and patterns; enhance reference rescaling
1 parent 8effae2 commit 46941be

2 files changed

Lines changed: 138 additions & 26 deletions

File tree

plaid/plaid.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
# - Crop data option? Perhaps save cropped .h5 copy?
6565
# - Restructure data loading
6666
# > Update plots on the fly during reading?
67+
# - FIX reference scaling when toggling between linear and log scale
6768

6869

6970
ALLOW_EXPORT_ALL_PATTERNS = True
@@ -1123,7 +1124,7 @@ def rescale_reference(self,index,name):
11231124
def get_user_input_energy(self):
11241125
"""Prompt the user to input the energy if not already set."""
11251126
E = self.azint_data.user_E_dialog()
1126-
if E is not None and self.azint_data.fnames[0] in self.aux_data:
1127+
if E is not None and self.azint_data.fnames is not None and self.azint_data.fnames[0] in self.aux_data:
11271128
self.aux_data[self.azint_data.fnames[0]].set_energy(E)
11281129
return E
11291130

@@ -1627,6 +1628,8 @@ def set_diffraction_map(self,roi):
16271628
z = np.zeros(self.azint_data.shape[0])
16281629
else:
16291630
I = self.azint_data.get_I()[:, roi]
1631+
# if self.pattern.get_log_mode():
1632+
# I = np.log10(I, where=(I>0), out=np.zeros_like(I))
16301633
if self.pattern.linear_region_ignore_negative:
16311634
I[I<0] = 0
16321635
elif self.pattern.linear_region_linear_background:
@@ -1731,7 +1734,11 @@ def keyReleaseEvent(self, event):
17311734
I = self.azint_data.get_I()
17321735
x = self.heatmap.x
17331736
# y = np.arange(I.shape[0])
1734-
self.heatmap.set_data(x, I.T)
1737+
if I is not None:
1738+
self.heatmap.set_data(x, I.T)
1739+
1740+
self.pattern.toggle_log_y(self.heatmap.use_log_scale)
1741+
17351742
elif event.key() == QtCore.Qt.Key.Key_C:
17361743
# Show/hide the correlation map
17371744
self.show_correlation_map()

plaid/plot_widgets.py

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def __init__(self, parent=None):
8383
self.histogram.setImageItem(self.image_item)
8484
self.histogram.item.gradient.loadPreset('viridis')
8585
layout.addWidget(self.histogram,0)
86+
87+
# get the context menu of the plot widget and add an option to transpose the map
88+
menu = self.plot_widget.getPlotItem().getViewBox().getMenu(None)
89+
action_logscale = QAction("Log Scale",self)
90+
menu.insertAction(menu.actions()[1],action_logscale)
91+
action_logscale.triggered.connect(self.toggle_log_scale)
92+
8693

8794
self.plot_widget.getPlotItem().mouseDoubleClickEvent = self.image_double_clicked
8895

@@ -120,6 +127,15 @@ def removeHLine(self, index=-1):
120127
self.sigHLineRemoved.emit(index)
121128
self.active_line = self.h_lines[-1] if self.h_lines else None # Set the active line to the last one if available
122129

130+
def toggle_log_scale(self):
131+
"""Toggle logarithmic scale for the heatmap."""
132+
if self.image_item.image is not None:
133+
im = self.image_item.image
134+
if self.use_log_scale:
135+
im = 10**im
136+
self.use_log_scale = not self.use_log_scale
137+
self.set_data(self.x, im)
138+
123139
def set_data(self, x,z,y=None):
124140
"""Set the data for the heatmap."""
125141
self.n = z.shape[1]
@@ -496,11 +512,12 @@ def __init__(self, parent=None):
496512
self.plot_widget.getPlotItem().addItem(self.hkl_text_item)
497513
self.hkl_text_item.setVisible(False) # Hide the text item by default
498514
# initialize the plot limits
499-
self.plot_widget.setLimits(xMin=-1, xMax=181)
515+
self.plot_widget.setLimits(xMin=-1, xMax=181, yMin=-1e4, yMax=1e8)
500516

501517
self.plot_widget.sigXRangeChanged.connect(self.xrange_changed)
502518

503519
self.plot_widget.getPlotItem().vb.hoverEvent = self.hover_event
520+
self.plot_widget.getPlotItem().vb.mouseDoubleClickEvent = self.mouse_double_clicked
504521

505522
self.set_xlabel("radial axis")
506523
self.set_ylabel("intensity")
@@ -578,6 +595,8 @@ def __init__(self, parent=None):
578595
action.setMenu(menu)
579596
action.triggered.connect(lambda: menu.exec(QCursor.pos()))
580597

598+
self.plot_widget.getPlotItem().ctrl.logYCheck.toggled.connect(self.rescale_all_references)
599+
581600

582601
def add_pattern(self):
583602
"""Add a new pattern item to the plot."""
@@ -695,12 +714,10 @@ def _set_reference_data(self, item, x, I):
695714
"""Set the data for a reference pattern item."""
696715
x = np.repeat(x,2)
697716
I = np.repeat(I,2)
698-
I[::2] = 0 # Set the intensity to 0 for the first point of each pair
699-
if self.y is None:
700-
scale = 100
701-
else:
702-
scale = self.y.max() if self.y.max()>0 else 1.
703-
item.setData(x, I*scale) # Update with new data
717+
I[::2] = 1e-10
718+
item.setData(x, I) # Update with new data
719+
self._rescale_reference(item) # Rescale the reference pattern to match the current y-axis scale
720+
704721

705722
def remove_reference(self, index=-1):
706723
"""Remove a reference pattern from the plot."""
@@ -714,16 +731,46 @@ def toggle_reference(self, index, is_checked):
714731
reference_item.setVisible(is_checked)
715732
self.hkl_text_item.setVisible(False) # Hide the text item when toggling reference visibility
716733

734+
def rescale_all_references(self):
735+
"""Rescale the intensity of all reference patterns to the current y-max."""
736+
for reference_item in self.reference_items:
737+
self._rescale_reference(reference_item)
738+
717739
def rescale_reference(self,index):
718740
"""Rescale the intensity of the indexed reference to the current y-max"""
719741
reference_item = self.reference_items[index]
742+
self._rescale_reference(reference_item)
743+
744+
def _rescale_reference(self, reference_item):
720745
x, I = reference_item.getData()
721-
I /= I.max() # Normalize the intensity to the maximum value
722-
if self.y is None or len(self.y) == 0:
723-
scale = 100
746+
# check if the yxais is in log mode
747+
_y_axis = self.get_axis('y')
748+
if _y_axis.logMode:
749+
# bring back to linear scale
750+
I = 10**I
751+
I = I-I.min()
752+
# normalize
753+
I /= I.max()
754+
# scale
755+
if self.y is None or len(self.y) == 0:
756+
_y_min, _y_max = [10**r for r in _y_axis.range]
757+
else:
758+
_y = self.y[self.y > 0]
759+
_y_min = _y.min()*.9
760+
_y_max = _y.max()
761+
I = I*(_y_max-_y_min)+_y_min
762+
763+
reference_item.setData(x, I) # Rescale the reference pattern
764+
724765
else:
725-
scale = self.y.max()
726-
reference_item.setData(x, I*scale) # Rescale the reference pattern
766+
I /= I.max() # Normalize the intensity to the maximum value
767+
I[::2] = 1e-10
768+
if self.y is None or len(self.y) == 0:
769+
scale = 100
770+
else:
771+
scale = self.y.max()
772+
reference_item.setData(x, I*scale) # Rescale the reference pattern
773+
727774

728775
def reference_clicked(self, item, event):
729776
"""Handle the click event on a reference pattern."""
@@ -741,8 +788,17 @@ def reference_clicked(self, item, event):
741788
# Show the hkl indices in the text item
742789
self.hkl_text_item.setColor(color)
743790
self.hkl_text_item.setText(f"({hkl})")
744-
self.hkl_text_item.setPos(x_hkls[idx], 0)
791+
self.hkl_text_item.setPos(x_hkls[idx], y)
745792
self.hkl_text_item.setVisible(True) # Show the text item
793+
794+
def toggle_log_y(self, checked):
795+
"""Toggle logarithmic scale for the y-axis."""
796+
if checked:
797+
self.plot_widget.setLogMode(y=True)
798+
self.plot_widget.setLimits(yMin=-8, yMax=8)
799+
else:
800+
self.plot_widget.setLogMode(y=False)
801+
self.plot_widget.setLimits(yMin=-1e4, yMax=1e8)
746802

747803
def set_avg_data(self, y_avg):
748804
"""Set the average data for the pattern."""
@@ -753,11 +809,11 @@ def set_avg_data(self, y_avg):
753809

754810
def set_xlabel(self, label):
755811
"""Set the x-axis label."""
756-
self.plot_widget.getPlotItem().getAxis('bottom').setLabel(label)
812+
self.get_axis('x').setLabel(label)
757813

758814
def set_ylabel(self, label):
759815
"""Set the y-axis label."""
760-
self.plot_widget.getPlotItem().getAxis('left').setLabel(label)
816+
self.get_axis('y').setLabel(label)
761817

762818
def set_xrange(self, x_range):
763819
"""Set the x-axis range."""
@@ -769,7 +825,6 @@ def set_xrange(self, x_range):
769825
self.plot_widget.setXRange(x_min, x_max, padding=0)
770826
# reconnect the signal
771827
self.plot_widget.sigXRangeChanged.connect(self.xrange_changed)
772-
#self.plot_widget.getPlotItem().getAxis('bottom').setRange(x_min, x_max)
773828

774829
def xrange_changed(self,vb, x_range):
775830
"""Handle the x-axis range change."""
@@ -813,7 +868,10 @@ def update_fill_area(self):
813868
if roi is None or np.sum(roi) == 0:
814869
return
815870
y = self.y[roi]
816-
y0 = np.zeros_like(y)
871+
if self.get_log_mode('y'):
872+
y0 = self.y[self.y > 0].min() * np.ones_like(y)
873+
else:
874+
y0 = np.zeros_like(y)
817875
if self.linear_region_ignore_negative:
818876
y = np.clip(y, 0, None)
819877
if self.linear_region_linear_background:
@@ -867,6 +925,17 @@ def toggle_linear_region_options(self,button):
867925
self.update_fill_area()
868926
self.sigLinearRegionChangedFinished.emit(self.get_linear_region_roi())
869927

928+
def get_axis(self, axis):
929+
"""Get the specified axis of the plot."""
930+
axis_names = {'x': 'bottom', 'bottom': 'bottom', 'y': 'left', 'left': 'left'}
931+
if axis in axis_names:
932+
axis = axis_names[axis]
933+
return self.plot_widget.getPlotItem().getAxis(axis)
934+
935+
def get_log_mode(self, axis='y'):
936+
"""Get the log mode of the specified axis."""
937+
return self.get_axis(axis).logMode
938+
870939
def hover_event(self, event):
871940
"""Handle the hover event on the plot item."""
872941
if not event.isExit():
@@ -878,12 +947,26 @@ def hover_event(self, event):
878947
pos = self.plot_widget.getPlotItem().vb.mapToView(pos)
879948
x = pos.x()
880949
y = pos.y()
950+
if self.get_log_mode('y'):
951+
y = 10**y
881952
# Emit the signal with the x and y coordinates
882953
self.sigPatternHovered.emit((x, y))
883954
else:
884955
# emit None to indicate the mouse is no longer hovering
885956
self.sigPatternHovered.emit(None) # Emit None to indicate no hover
886957

958+
def mouse_double_clicked(self, event):
959+
"""Handle the mouse double click event on the plot item."""
960+
if self.lr.isVisible():
961+
pos = event.pos()
962+
# # Convert the position to the plot item's coordinates
963+
x = self.plot_widget.getPlotItem().vb.mapToView(pos).x()
964+
x_min, x_max = self.lr.getRegion()
965+
width = x_max - x_min
966+
new_x_min = x - width / 2
967+
new_x_max = x + width / 2
968+
self.lr.setRegion([new_x_min, new_x_max])
969+
887970
def set_color_cycle(self,color_cycle):
888971
"""Set the color cycle for the plot items."""
889972
self.color_cycle = color_cycle
@@ -922,8 +1005,8 @@ def updateForeground(self):
9221005
Update the foreground color of the plot widget to the current default
9231006
from pyqtgraph configOptions.
9241007
"""
925-
x_axis = self.plot_widget.getPlotItem().getAxis('bottom')
926-
y_axis = self.plot_widget.getPlotItem().getAxis('left')
1008+
x_axis = self.get_axis('x')
1009+
y_axis = self.get_axis('y')
9271010

9281011
x_axis.setPen()
9291012
x_axis.setTextPen()
@@ -1127,7 +1210,8 @@ class BasicMapWidget(QWidget):
11271210
def __init__(self, parent=None):
11281211
super().__init__(parent)
11291212
self.fnames = [] # used to keep track of which dataset is used
1130-
self.transpose = False
1213+
self.transposed = False
1214+
self.log_scale = False
11311215

11321216
# Create a layout
11331217
vlayout = QVBoxLayout(self)
@@ -1181,13 +1265,21 @@ def __init__(self, parent=None):
11811265
menu.insertAction(menu.actions()[1],action_transpose)
11821266
action_transpose.triggered.connect(self.toggle_transpose)
11831267

1268+
# add a log scale action to the context menu
1269+
action_log_scale = QAction("Log scale",self)
1270+
action_log_scale.setCheckable(True)
1271+
menu.insertAction(menu.actions()[2],action_log_scale)
1272+
action_log_scale.triggered.connect(self.toggle_log_scale)
1273+
11841274

11851275
def set_data(self, im):
11861276
"""Set the data for the map."""
11871277
if im is None:
11881278
return
1189-
if self.transpose:
1279+
if self.transposed:
11901280
im = im.T
1281+
if self.log_scale:
1282+
im = np.log10(im, where=(im>0), out=np.zeros_like(im))
11911283
self.image_item.setImage(im)
11921284

11931285
def image_double_clicked(self, event):
@@ -1245,14 +1337,27 @@ def autoRange(self):
12451337
def toggle_transpose(self):
12461338
"""Transpose the map."""
12471339
# new transpose state
1248-
_transpose = not self.transpose
1340+
_transpose = not self.transposed
12491341
# always set transpose to True when updating the data
12501342
# to either transpose or "un-transpose" the image
1251-
self.transpose = True
1343+
self.transposed = True
12521344
im = self.image_item.image
12531345
self.set_data(im)
12541346
# set the transpose state
1255-
self.transpose = _transpose
1347+
self.transposed = _transpose
1348+
1349+
def toggle_log_scale(self, checked):
1350+
"""Toggle logarithmic scale for the color map."""
1351+
im = self.image_item.image
1352+
# if already in log scale, bring back to linear scale before updating the data
1353+
if self.log_scale:
1354+
im = 10**im
1355+
# if already transposed, bring back to original orientation before updating the data
1356+
if self.transposed:
1357+
im = im.T
1358+
self.log_scale = checked
1359+
self.set_data(im)
1360+
12561361

12571362
class CorrelationMapWidget(BasicMapWidget):
12581363
"""

0 commit comments

Comments
 (0)