def setup_toolbar(self): # These don't work for 3D plots # "None" removes the separators button_blacklist = [None, 'Pan', 'Zoom', 'Subplots', 'Customize'] self.toolbar = NavigationToolbar(self.canvas, self.ui, False, button_blacklist) self.ui.toolbar_layout.addWidget(self.toolbar) self.ui.toolbar_layout.setAlignment(self.toolbar, Qt.AlignCenter) # Make sure our ranges editor gets updated any time matplotlib # might have modified the ranges underneath. self.toolbar.after_home_callback = self.update_ranges_gui self.toolbar.after_back_callback = self.update_ranges_gui self.toolbar.after_forward_callback = self.update_ranges_gui
def allocate_toolbars(self): parent = self.parent() while len(self.toolbars) != len(self.image_canvases): # The new one to add idx = len(self.toolbars) tb = NavigationToolbar(self.image_canvases[idx], parent, False) # Current detector name = self.image_names[idx] sb = ImageSeriesToolbar(name, self) # This will put it at the bottom of the central widget toolbar = QHBoxLayout() toolbar.addWidget(tb) toolbar.addWidget(sb.widget) parent.layout().addLayout(toolbar) parent.layout().setAlignment(toolbar, Qt.AlignCenter) self.toolbars.append({'tb': tb, 'sb': sb})
def setup_plot(self): # Create the figure and axes to use canvas = FigureCanvas(Figure(tight_layout=True)) # Get the canvas to take up the majority of the screen most of the time canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) fig = canvas.figure ax = fig.add_subplot() ax.set_title('Eta Omega Maps') ax.set_xlabel(r'$\eta$ ($\deg$)') ax.set_ylabel(r'$\omega$ ($\deg$)') ax.format_coord = self.format_coord self.ui.canvas_layout.addWidget(canvas) self.toolbar = NavigationToolbar(canvas, self.ui, coordinates=True) self.ui.canvas_layout.addWidget(self.toolbar) # Center the toolbar self.ui.canvas_layout.setAlignment(self.toolbar, Qt.AlignCenter) self.fig = fig self.ax = ax self.canvas = canvas
class FitGrainsResultsDialog(QObject): finished = Signal() def __init__(self, data, material=None, parent=None): super().__init__() if material is None: # Assume the active material is the correct one. # This might not actually be the case, though... material = HexrdConfig().active_material if material: # Warn the user so this is clear. print(f'Assuming material of {material.name} for stress ' 'computations') self.ax = None self.cmap = hexrd.ui.constants.DEFAULT_CMAP self.data = data self.data_model = FitGrainsResultsModel(data) self.material = material self.canvas = None self.fig = None self.scatter_artist = None self.colorbar = None loader = UiLoader() self.ui = loader.load_file('fit_grains_results_dialog.ui', parent) flags = self.ui.windowFlags() self.ui.setWindowFlags(flags | Qt.Tool) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 10) self.setup_tableview() self.load_cmaps() self.reset_glyph_size(update_plot=False) # Add columns for equivalent strain and hydrostatic strain eqv_strain = np.zeros(self.num_grains) hydrostatic_strain = np.zeros(self.num_grains) for i, grain in enumerate(self.data): epsilon = vecMVToSymm(grain[ELASTIC_SLICE], scale=False) deviator = epsilon - (1/3) * np.trace(epsilon) * np.identity(3) eqv_strain[i] = 2 * np.sqrt(np.sum(deviator**2)) / 3 hydrostatic_strain[i] = 1 / 3 * np.trace(epsilon) self.data = np.hstack((self.data, eqv_strain[:, np.newaxis])) self.data = np.hstack((self.data, hydrostatic_strain[:, np.newaxis])) self.setup_gui() def setup_gui(self): self.update_selectors() self.setup_plot() self.setup_toolbar() self.setup_view_direction_options() self.setup_connections() self.update_plot() self.backup_ranges() self.update_ranges_gui() self.update_enable_states() @property def num_grains(self): return self.data.shape[0] @property def converted_data(self): # Perform conversions on the data to the specified types. # For instance, use stress instead of strain if that is set. tensor_type = self.tensor_type data = copy.deepcopy(self.data) if self.cylindrical_reference: for grain in data: x, y, z = grain[COORDS_SLICE] rho = np.sqrt(x**2 + z**2) phi = np.arctan2(z, x) grain[COORDS_SLICE] = (rho, phi, y) if tensor_type == 'stress': for grain in data: # Convert strain to stress # Multiply last three numbers by factor of 2 grain[ELASTIC_OFF_DIAGONAL_SLICE] *= 2 grain[ELASTIC_SLICE] = np.dot(self.compliance, grain[ELASTIC_SLICE]) # Compute the equivalent stress sigma = vecMVToSymm(grain[ELASTIC_SLICE], scale=False) deviator = sigma - (1/3) * np.trace(sigma) * np.identity(3) grain[EQUIVALENT_IND] = 3 * np.sqrt(np.sum(deviator**2)) / 2 # Compute the hydrostatic stress grain[HYDROSTATIC_IND] = 1 / 3 * np.trace(sigma) return data @property def axes_labels(self): if self.cylindrical_reference: return ('ρ', 'φ', 'Y') return ('X', 'Y', 'Z') @property def cylindrical_reference(self): return self.ui.cylindrical_reference.isChecked() @cylindrical_reference.setter def cylindrical_reference(self, v): self.ui.cylindricalreference.setChecked(v) @property def stiffness(self): try: # Any of these could be an attribute error return self.material.unitcell.stiffness except AttributeError: return None @property def compliance(self): try: # Any of these could be an attribute error return self.material.unitcell.compliance except AttributeError: return None def update_enable_states(self): has_stiffness = self.stiffness is not None self.ui.convert_strain_to_stress.setEnabled(has_stiffness) def clear_artists(self): # Colorbar must be removed before the scatter artist if self.colorbar is not None: self.colorbar.remove() self.colorbar = None if self.scatter_artist is not None: self.scatter_artist.remove() self.scatter_artist = None def on_colorby_changed(self): self.update_plot() def update_plot(self): data = self.converted_data column = self.ui.plot_color_option.currentData() colors = data[:, column] coords = data[:, COORDS_SLICE].T sz = self.ui.glyph_size_slider.value() # I could not find a way to update scatter plot marker colors and # the colorbar mappable. So we must re-draw both from scratch... self.clear_artists() kwargs = { 'c': colors, 'cmap': self.cmap, 's': sz, 'depthshade': self.depth_shading, } self.scatter_artist = self.ax.scatter3D(*coords, **kwargs) self.colorbar = self.fig.colorbar(self.scatter_artist, shrink=0.8) self.draw_idle() def on_export_button_pressed(self): selected_file, selected_filter = QFileDialog.getSaveFileName( self.ui, 'Export Fit-Grains Results', HexrdConfig().working_dir, 'Output files (*.out)|All files(*.*)') if selected_file: HexrdConfig().working_dir = os.path.dirname(selected_file) name, ext = os.path.splitext(selected_file) if not ext: selected_file += '.out' self.data_model.save(selected_file) def on_export_stresses_button_pressed(self): if self.tensor_type != 'stress': raise Exception('Tensor type must be stress') selected_file, selected_filter = QFileDialog.getSaveFileName( self.ui, 'Export Fit-Grains Stresses', HexrdConfig().working_dir, 'Npz Files (*.npz)') if not selected_file: return stresses = self.converted_data[:, ELASTIC_SLICE] HexrdConfig().working_dir = str(Path(selected_file).parent) ext = Path(selected_file).suffix if ext != '.npz': selected_file += '.npz' np.savez_compressed(selected_file, stresses=stresses) def on_sort_indicator_changed(self, index, order): """Shows sort indicator for sortable columns, hides for all others.""" horizontal_header = self.ui.table_view.horizontalHeader() if index in SORTABLE_COLUMNS: horizontal_header.setSortIndicatorShown(True) horizontal_header.setSortIndicator(index, order) else: horizontal_header.setSortIndicatorShown(False) @property def depth_shading(self): return self.ui.depth_shading.isChecked() @depth_shading.setter def depth_shading(self, v): self.ui.depth_shading.setChecked(v) @property def projection(self): name_map = { 'Perspective': 'persp', 'Orthographic': 'ortho' } return name_map[self.ui.projection.currentText()] def projection_changed(self): self.ax.set_proj_type(self.projection) self.draw_idle() def cylindrical_reference_toggled(self): self.update_axes_labels() self.update_plot() def setup_connections(self): self.ui.export_button.clicked.connect(self.on_export_button_pressed) self.ui.export_stresses.clicked.connect( self.on_export_stresses_button_pressed) self.ui.projection.currentIndexChanged.connect(self.projection_changed) self.ui.plot_color_option.currentIndexChanged.connect( self.on_colorby_changed) self.ui.hide_axes.toggled.connect(self.update_axis_visibility) self.ui.depth_shading.toggled.connect(self.update_plot) self.ui.finished.connect(self.finished) self.ui.color_maps.currentIndexChanged.connect(self.update_cmap) self.ui.glyph_size_slider.valueChanged.connect(self.update_plot) self.ui.reset_glyph_size.clicked.connect(self.reset_glyph_size) self.ui.cylindrical_reference.toggled.connect( self.cylindrical_reference_toggled) for name in ('x', 'y', 'z'): action = getattr(self, f'set_view_{name}') action.triggered.connect(partial(self.reset_view, name)) for w in self.range_widgets: w.valueChanged.connect(self.update_ranges_mpl) w.valueChanged.connect(self.update_range_constraints) self.ui.reset_ranges.pressed.connect(self.reset_ranges) self.ui.convert_strain_to_stress.toggled.connect( self.convert_strain_to_stress_toggled) def setup_plot(self): # Create the figure and axes to use canvas = FigureCanvas(Figure(tight_layout=True)) # Get the canvas to take up the majority of the screen most of the time canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) fig = canvas.figure ax = fig.add_subplot(111, projection='3d', proj_type=self.projection) # Set default limits to -0.5 to 0.5 for name in ('x', 'y', 'z'): func = getattr(ax, f'set_{name}lim') func(-0.5, 0.5) self.ui.canvas_layout.addWidget(canvas) self.fig = fig self.ax = ax self.canvas = canvas self.update_axes_labels() def setup_toolbar(self): # These don't work for 3D plots # "None" removes the separators button_blacklist = [ None, 'Pan', 'Zoom', 'Subplots', 'Customize' ] self.toolbar = NavigationToolbar(self.canvas, self.ui, False, button_blacklist) self.ui.toolbar_layout.addWidget(self.toolbar) self.ui.toolbar_layout.setAlignment(self.toolbar, Qt.AlignCenter) # Make sure our ranges editor gets updated any time matplotlib # might have modified the ranges underneath. self.toolbar.after_home_callback = self.update_ranges_gui self.toolbar.after_back_callback = self.update_ranges_gui self.toolbar.after_forward_callback = self.update_ranges_gui def setup_view_direction_options(self): b = self.ui.set_view_direction m = QMenu(b) self.set_view_direction_menu = m self.set_view_z = m.addAction('XY') self.set_view_y = m.addAction('XZ') self.set_view_x = m.addAction('YZ') b.setMenu(m) def reset_view(self, direction): # The adjustment is to force the tick markers and label to # appear on one side. adjust = 1.e-5 angles_map = { 'x': (0, 0), 'y': (0, 90 - adjust), 'z': (90 - adjust, -90 - adjust) } self.ax.view_init(*angles_map[direction]) # Temporarily hide the labels of the axis perpendicular to the # screen for easier viewing. if self.axes_visible: self.hide_axis(direction) self.draw_idle() # As soon as the image is re-drawn, the perpendicular axis will # be visible again. if self.axes_visible: self.show_axis(direction) def set_axis_visible(self, name, visible): ax = getattr(self.ax, f'{name}axis') set_label_func = getattr(self.ax, f'set_{name}label') if visible: ax.set_major_locator(ticker.AutoLocator()) set_label_func(name.upper()) else: ax.set_ticks([]) set_label_func('') def hide_axis(self, name): self.set_axis_visible(name, False) def show_axis(self, name): self.set_axis_visible(name, True) @property def axes_visible(self): return not self.ui.hide_axes.isChecked() def update_axis_visibility(self): for name in ('x', 'y', 'z'): self.set_axis_visible(name, self.axes_visible) self.draw_idle() def update_axes_labels(self): axes = ('x', 'y', 'z') labels = self.axes_labels for axis, label in zip(axes, labels): func = getattr(self.ax, f'set_{axis}label') func(label) def update_selectors(self): tensor_type = self.tensor_type.capitalize() # Build combo boxes in code to assign columns in grains data items = [ ('Completeness', 1), ('Goodness of Fit', 2), (f'Equivalent {tensor_type}', EQUIVALENT_IND), (f'Hydrostatic {tensor_type}', HYDROSTATIC_IND), (f'XX {tensor_type}', 15), (f'YY {tensor_type}', 16), (f'ZZ {tensor_type}', 17), (f'YZ {tensor_type}', 18), (f'XZ {tensor_type}', 19), (f'XY {tensor_type}', 20) ] prev_ind = self.ui.plot_color_option.currentIndex() blocker = QSignalBlocker(self.ui.plot_color_option) # noqa: F841 self.ui.plot_color_option.clear() for item in items: self.ui.plot_color_option.addItem(*item) del blocker if hasattr(self, '_first_selector_update'): self.ui.plot_color_option.setCurrentIndex(prev_ind) else: self._first_selector_update = True index = self.ui.plot_color_option.findData(EQUIVALENT_IND) self.ui.plot_color_option.setCurrentIndex(index) def setup_tableview(self): view = self.ui.table_view # Subclass QSortFilterProxyModel to restrict sorting by column class GrainsTableSorter(QSortFilterProxyModel): def sort(self, column, order): if column not in SORTABLE_COLUMNS: return return super().sort(column, order) proxy_model = GrainsTableSorter(self.ui) proxy_model.setSourceModel(self.data_model) view.verticalHeader().hide() view.setModel(proxy_model) view.resizeColumnToContents(0) view.setSortingEnabled(True) view.horizontalHeader().sortIndicatorChanged.connect( self.on_sort_indicator_changed) view.sortByColumn(0, Qt.AscendingOrder) self.ui.table_view.horizontalHeader().setSortIndicatorShown(False) def show(self): self.ui.show() @property def tensor_type(self): stress = self.ui.convert_strain_to_stress.isChecked() return 'stress' if stress else 'strain' @property def range_widgets(self): widgets = [] for name in ('x', 'y', 'z'): for i in range(2): widgets.append(getattr(self.ui, f'range_{name}_{i}')) return widgets @property def ranges_gui(self): return [w.value() for w in self.range_widgets] @ranges_gui.setter def ranges_gui(self, v): self.remove_range_constraints() for x, w in zip(v, self.range_widgets): w.setValue(round(x, 5)) self.update_range_constraints() @property def ranges_mpl(self): vals = [] for name in ('x', 'y', 'z'): lims_func = getattr(self.ax, f'get_{name}lim') vals.extend(lims_func()) return vals @ranges_mpl.setter def ranges_mpl(self, v): for i, name in enumerate(('x', 'y', 'z')): lims = (v[i * 2], v[i * 2 + 1]) set_func = getattr(self.ax, f'set_{name}lim') set_func(*lims) # Update the navigation stack so the home/back/forward # buttons will know about the range change. self.toolbar.push_current() self.draw_idle() def update_ranges_mpl(self): self.ranges_mpl = self.ranges_gui def update_ranges_gui(self): blocked = [QSignalBlocker(w) for w in self.range_widgets] # noqa: F841 self.ranges_gui = self.ranges_mpl def backup_ranges(self): self._ranges_backup = self.ranges_mpl def reset_ranges(self): self.ranges_mpl = self._ranges_backup self.update_ranges_gui() def convert_strain_to_stress_toggled(self): self.update_selectors() self.update_plot() def remove_range_constraints(self): widgets = self.range_widgets for w1, w2 in zip(widgets[0::2], widgets[1::2]): w1.setMaximum(sys.float_info.max) w2.setMinimum(sys.float_info.min) def update_range_constraints(self): widgets = self.range_widgets for w1, w2 in zip(widgets[0::2], widgets[1::2]): w1.setMaximum(w2.value()) w2.setMinimum(w1.value()) def load_cmaps(self): cmaps = sorted(i[:-2] for i in dir(matplotlib.cm) if i.endswith('_r')) self.ui.color_maps.addItems(cmaps) # Set the combobox to be the default self.ui.color_maps.setCurrentText(hexrd.ui.constants.DEFAULT_CMAP) def update_cmap(self): # Get the Colormap object from the name self.cmap = matplotlib.cm.get_cmap(self.ui.color_maps.currentText()) self.update_plot() def reset_glyph_size(self, update_plot=True): default = matplotlib.rcParams['lines.markersize'] ** 3 self.ui.glyph_size_slider.setSliderPosition(default) if update_plot: self.update_plot() def draw_idle(self): self.canvas.draw_idle()
class FitGrainsResultsDialog(QObject): finished = Signal() def __init__(self, data, parent=None): super(FitGrainsResultsDialog, self).__init__() self.ax = None self.cmap = hexrd.ui.constants.DEFAULT_CMAP self.data = data self.data_model = FitGrainsResultsModel(data) self.canvas = None self.fig = None self.scatter_artist = None self.colorbar = None loader = UiLoader() self.ui = loader.load_file('fit_grains_results_dialog.ui', parent) flags = self.ui.windowFlags() self.ui.setWindowFlags(flags | Qt.Tool) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 10) self.setup_tableview() # Add column for equivalent strain ngrains = self.data.shape[0] eqv_strain = np.zeros(ngrains) for i in range(ngrains): emat = vecMVToSymm(self.data[i, 15:21], scale=False) eqv_strain[i] = 2.*np.sqrt(np.sum(emat*emat))/3. np.append(self.data, eqv_strain) self.setup_gui() def setup_gui(self): self.setup_selectors() self.setup_plot() self.setup_toolbar() self.setup_view_direction_options() self.setup_connections() self.on_colorby_changed() self.backup_ranges() self.update_ranges_gui() def clear_artists(self): # Colorbar must be removed before the scatter artist if self.colorbar is not None: self.colorbar.remove() self.colorbar = None if self.scatter_artist is not None: self.scatter_artist.remove() self.scatter_artist = None def on_colorby_changed(self): column = self.ui.plot_color_option.currentData() colors = self.data[:, column] xs = self.data[:, 6] ys = self.data[:, 7] zs = self.data[:, 8] sz = matplotlib.rcParams['lines.markersize'] ** 3 # I could not find a way to update scatter plot marker colors and # the colorbar mappable. So we must re-draw both from scratch... self.clear_artists() self.scatter_artist = self.ax.scatter3D( xs, ys, zs, c=colors, cmap=self.cmap, s=sz) self.colorbar = self.fig.colorbar(self.scatter_artist, shrink=0.8) self.draw() def on_export_button_pressed(self): selected_file, selected_filter = QFileDialog.getSaveFileName( self.ui, 'Export Fit-Grains Results', HexrdConfig().working_dir, 'Output files (*.out)|All files(*.*)') if selected_file: HexrdConfig().working_dir = os.path.dirname(selected_file) name, ext = os.path.splitext(selected_file) if not ext: selected_file += '.out' self.data_model.save(selected_file) def on_sort_indicator_changed(self, index, order): """Shows sort indicator for columns 0-2, hides for all others.""" if index < 3: self.ui.table_view.horizontalHeader().setSortIndicatorShown(True) self.ui.table_view.horizontalHeader().setSortIndicator( index, order) else: self.ui.table_view.horizontalHeader().setSortIndicatorShown(False) @property def projection(self): name_map = { 'Perspective': 'persp', 'Orthographic': 'ortho' } return name_map[self.ui.projection.currentText()] def projection_changed(self): self.ax.set_proj_type(self.projection) self.draw() def setup_connections(self): self.ui.export_button.clicked.connect(self.on_export_button_pressed) self.ui.projection.currentIndexChanged.connect(self.projection_changed) self.ui.plot_color_option.currentIndexChanged.connect( self.on_colorby_changed) self.ui.hide_axes.toggled.connect(self.update_axis_visibility) self.ui.finished.connect(self.finished) for name in ('x', 'y', 'z'): action = getattr(self, f'set_view_{name}') action.triggered.connect(partial(self.reset_view, name)) for w in self.range_widgets: w.valueChanged.connect(self.update_ranges_mpl) w.valueChanged.connect(self.update_range_constraints) self.ui.reset_ranges.pressed.connect(self.reset_ranges) def setup_plot(self): # Create the figure and axes to use canvas = FigureCanvas(Figure(tight_layout=True)) # Get the canvas to take up the majority of the screen most of the time canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) fig = canvas.figure ax = fig.add_subplot(111, projection='3d', proj_type=self.projection) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') self.ui.canvas_layout.addWidget(canvas) self.fig = fig self.ax = ax self.canvas = canvas def setup_toolbar(self): # These don't work for 3D plots # "None" removes the separators button_blacklist = [ None, 'Pan', 'Zoom', 'Subplots', 'Customize' ] self.toolbar = NavigationToolbar(self.canvas, self.ui, False, button_blacklist) self.ui.toolbar_layout.addWidget(self.toolbar) self.ui.toolbar_layout.setAlignment(self.toolbar, Qt.AlignCenter) # Make sure our ranges editor gets updated any time matplotlib # might have modified the ranges underneath. self.toolbar.after_home_callback = self.update_ranges_gui self.toolbar.after_back_callback = self.update_ranges_gui self.toolbar.after_forward_callback = self.update_ranges_gui def setup_view_direction_options(self): b = self.ui.set_view_direction m = QMenu(b) self.set_view_direction_menu = m self.set_view_z = m.addAction('XY') self.set_view_y = m.addAction('XZ') self.set_view_x = m.addAction('YZ') b.setMenu(m) def reset_view(self, direction): # The adjustment is to force the tick markers and label to # appear on one side. adjust = 1.e-5 angles_map = { 'x': (0, 0), 'y': (0, 90 - adjust), 'z': (90 - adjust, -90 - adjust) } self.ax.view_init(*angles_map[direction]) # Temporarily hide the labels of the axis perpendicular to the # screen for easier viewing. if self.axes_visible: self.hide_axis(direction) self.draw() # As soon as the image is re-drawn, the perpendicular axis will # be visible again. if self.axes_visible: self.show_axis(direction) def set_axis_visible(self, name, visible): ax = getattr(self.ax, f'{name}axis') set_label_func = getattr(self.ax, f'set_{name}label') if visible: ax.set_major_locator(ticker.AutoLocator()) set_label_func(name.upper()) else: ax.set_ticks([]) set_label_func('') def hide_axis(self, name): self.set_axis_visible(name, False) def show_axis(self, name): self.set_axis_visible(name, True) @property def axes_visible(self): return not self.ui.hide_axes.isChecked() def update_axis_visibility(self): for name in ('x', 'y', 'z'): self.set_axis_visible(name, self.axes_visible) self.draw() def setup_selectors(self): # Build combo boxes in code to assign columns in grains data blocker = QSignalBlocker(self.ui.plot_color_option) # noqa: F841 self.ui.plot_color_option.clear() self.ui.plot_color_option.addItem('Completeness', 1) self.ui.plot_color_option.addItem('Goodness of Fit', 2) self.ui.plot_color_option.addItem('Equivalent Strain', -1) self.ui.plot_color_option.addItem('XX Strain', 15) self.ui.plot_color_option.addItem('YY Strain', 16) self.ui.plot_color_option.addItem('ZZ Strain', 17) self.ui.plot_color_option.addItem('YZ Strain', 18) self.ui.plot_color_option.addItem('XZ Strain', 19) self.ui.plot_color_option.addItem('XY Strain', 20) index = self.ui.plot_color_option.findData(-1) self.ui.plot_color_option.setCurrentIndex(index) def setup_tableview(self): view = self.ui.table_view # Subclass QSortFilterProxyModel to restrict sorting by column class GrainsTableSorter(QSortFilterProxyModel): def sort(self, column, order): if column > 2: return else: super().sort(column, order) proxy_model = GrainsTableSorter(self.ui) proxy_model.setSourceModel(self.data_model) view.verticalHeader().hide() view.setModel(proxy_model) view.resizeColumnToContents(0) view.setSortingEnabled(True) view.horizontalHeader().sortIndicatorChanged.connect( self.on_sort_indicator_changed) view.sortByColumn(0, Qt.AscendingOrder) self.ui.table_view.horizontalHeader().setSortIndicatorShown(False) def show(self): self.ui.show() @property def range_widgets(self): widgets = [] for name in ('x', 'y', 'z'): for i in range(2): widgets.append(getattr(self.ui, f'range_{name}_{i}')) return widgets @property def ranges_gui(self): return [w.value() for w in self.range_widgets] @ranges_gui.setter def ranges_gui(self, v): self.remove_range_constraints() for x, w in zip(v, self.range_widgets): w.setValue(round(x, 5)) self.update_range_constraints() @property def ranges_mpl(self): vals = [] for name in ('x', 'y', 'z'): lims_func = getattr(self.ax, f'get_{name}lim') vals.extend(lims_func()) return vals @ranges_mpl.setter def ranges_mpl(self, v): for i, name in enumerate(('x', 'y', 'z')): lims = (v[i * 2], v[i * 2 + 1]) set_func = getattr(self.ax, f'set_{name}lim') set_func(*lims) # Update the navigation stack so the home/back/forward # buttons will know about the range change. self.toolbar.push_current() self.draw() def update_ranges_mpl(self): self.ranges_mpl = self.ranges_gui def update_ranges_gui(self): blocked = [QSignalBlocker(w) for w in self.range_widgets] # noqa: F841 self.ranges_gui = self.ranges_mpl def backup_ranges(self): self._ranges_backup = self.ranges_mpl def reset_ranges(self): self.ranges_mpl = self._ranges_backup self.update_ranges_gui() def remove_range_constraints(self): widgets = self.range_widgets for w1, w2 in zip(widgets[0::2], widgets[1::2]): w1.setMaximum(sys.float_info.max) w2.setMinimum(sys.float_info.min) def update_range_constraints(self): widgets = self.range_widgets for w1, w2 in zip(widgets[0::2], widgets[1::2]): w1.setMaximum(w2.value()) w2.setMinimum(w1.value()) def draw(self): self.canvas.draw()