def _config_interface(self): "Called by __init__ to configure the interface elements" self._info_dialog = None self._drag_start = None self._pan_id = None self._pan_crop = None self._zoom_id = None self._zoom_rect = None # Create a figure in a tab for the file self.figure = Figure(figsize=(5.0, 5.0), dpi=FIGURE_DPI, facecolor='w', edgecolor='w') self.canvas = FigureCanvas(self.figure) self.image_axes = self.figure.add_axes((0.1, 0.1, 0.8, 0.8)) self.histogram_axes = None self.colorbar_axes = None self.title_axes = None self.ui.splitter.addWidget(self.canvas) # Set up the redraw timer self.redraw_timer = QtCore.QTimer() self.redraw_timer.setInterval(REDRAW_TIMEOUT_DEFAULT) self.redraw_timer.timeout.connect(self.redraw_timeout) # Set up the limits of the crop spinners self.ui.crop_left_spinbox.setRange(0, self._file.x_size - 1) self.ui.crop_right_spinbox.setRange(0, self._file.x_size - 1) self.ui.crop_top_spinbox.setRange(0, self._file.y_size - 1) self.ui.crop_bottom_spinbox.setRange(0, self._file.y_size - 1) # Configure the common combos default = -1 for interpolation in sorted(matplotlib.image.AxesImage._interpd): if interpolation == DEFAULT_INTERPOLATION: default = self.ui.interpolation_combo.count() self.ui.interpolation_combo.addItem(interpolation) self.ui.interpolation_combo.setCurrentIndex(default) if hasattr(self.ui, 'colorbar_check'): default = -1 for color in sorted(matplotlib.cm.datad): if not color.endswith('_r'): if color == DEFAULT_COLORMAP: default = self.ui.colormap_combo.count() self.ui.colormap_combo.addItem(color) self.ui.colormap_combo.setCurrentIndex(default)
class SubWindow(QtGui.QWidget): "Base class for rasviewer document windows" cropChanged = QtCore.pyqtSignal() def __init__(self, ui_file, data_file, channel_file=None): super(SubWindow, self).__init__(None) self._load_interface(ui_file) try: self._load_data(data_file, channel_file) except (ValueError, IOError) as exc: QtGui.QMessageBox.critical(self, self.tr('Error'), str(exc)) self.close() return self._config_interface() self._config_handlers() self.channel_changed() def _load_interface(self, ui_file): "Called by __init__ to load the Qt interface file" self.ui = None self.ui = uic.loadUi(get_ui_file(ui_file), self) def _load_data(self, data_file, channel_file=None): "Called by __init__ to load the data file" self._file = None self._progress = 0 self._progress_update = None self._progress_dialog = None QtGui.QApplication.instance().setOverrideCursor(QtCore.Qt.WaitCursor) try: from rastools.data_parsers import DATA_PARSERS finally: QtGui.QApplication.instance().restoreOverrideCursor() # Open the selected file ext = os.path.splitext(data_file)[-1] parsers = dict( (ext, cls) for (cls, exts, _) in DATA_PARSERS for ext in exts) try: parser = parsers[ext] except KeyError: raise ValueError( self.tr('Unrecognized file extension "{0}"').format(ext)) self._file = parser(data_file, channel_file, delay_load=False, progress=( self.progress_start, self.progress_update, self.progress_finish, )) self.setWindowTitle(os.path.basename(data_file)) def _config_interface(self): "Called by __init__ to configure the interface elements" self._info_dialog = None self._drag_start = None self._pan_id = None self._pan_crop = None self._zoom_id = None self._zoom_rect = None # Create a figure in a tab for the file self.figure = Figure(figsize=(5.0, 5.0), dpi=FIGURE_DPI, facecolor='w', edgecolor='w') self.canvas = FigureCanvas(self.figure) self.image_axes = self.figure.add_axes((0.1, 0.1, 0.8, 0.8)) self.histogram_axes = None self.colorbar_axes = None self.title_axes = None self.ui.splitter.addWidget(self.canvas) # Set up the redraw timer self.redraw_timer = QtCore.QTimer() self.redraw_timer.setInterval(REDRAW_TIMEOUT_DEFAULT) self.redraw_timer.timeout.connect(self.redraw_timeout) # Set up the limits of the crop spinners self.ui.crop_left_spinbox.setRange(0, self._file.x_size - 1) self.ui.crop_right_spinbox.setRange(0, self._file.x_size - 1) self.ui.crop_top_spinbox.setRange(0, self._file.y_size - 1) self.ui.crop_bottom_spinbox.setRange(0, self._file.y_size - 1) # Configure the common combos default = -1 for interpolation in sorted(matplotlib.image.AxesImage._interpd): if interpolation == DEFAULT_INTERPOLATION: default = self.ui.interpolation_combo.count() self.ui.interpolation_combo.addItem(interpolation) self.ui.interpolation_combo.setCurrentIndex(default) if hasattr(self.ui, 'colorbar_check'): default = -1 for color in sorted(matplotlib.cm.datad): if not color.endswith('_r'): if color == DEFAULT_COLORMAP: default = self.ui.colormap_combo.count() self.ui.colormap_combo.addItem(color) self.ui.colormap_combo.setCurrentIndex(default) def _config_handlers(self): "Called by __init__ to connect events to handlers" # Set up common event connections self.ui.interpolation_combo.currentIndexChanged.connect( self.invalidate_image) self.ui.crop_top_spinbox.valueChanged.connect(self.crop_changed) self.ui.crop_left_spinbox.valueChanged.connect(self.crop_changed) self.ui.crop_right_spinbox.valueChanged.connect(self.crop_changed) self.ui.crop_bottom_spinbox.valueChanged.connect(self.crop_changed) self.ui.axes_check.toggled.connect(self.invalidate_image) self.ui.x_label_edit.textChanged.connect(self.invalidate_image) self.ui.y_label_edit.textChanged.connect(self.invalidate_image) self.ui.x_scale_spinbox.valueChanged.connect(self.x_scale_changed) self.ui.y_scale_spinbox.valueChanged.connect(self.y_scale_changed) self.ui.x_offset_spinbox.valueChanged.connect(self.x_offset_changed) self.ui.y_offset_spinbox.valueChanged.connect(self.y_offset_changed) self.ui.grid_check.toggled.connect(self.invalidate_image) self.ui.histogram_check.toggled.connect(self.invalidate_image) self.ui.histogram_bins_spinbox.valueChanged.connect( self.invalidate_image) self.ui.title_edit.textChanged.connect(self.invalidate_image) self.ui.default_title_button.clicked.connect( self.default_title_clicked) self.ui.clear_title_button.clicked.connect(self.clear_title_clicked) self.ui.title_info_button.clicked.connect(self.title_info_clicked) self.ui.splitter.splitterMoved.connect(self.splitter_moved) QtGui.QApplication.instance().focusChanged.connect(self.focus_changed) self.canvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.canvas.customContextMenuRequested.connect(self.canvas_popup) if hasattr(self.ui, 'colorbar_check'): self.ui.colorbar_check.toggled.connect(self.invalidate_image) self.ui.colormap_combo.currentIndexChanged.connect( self.invalidate_image) self.ui.reverse_check.toggled.connect(self.invalidate_image) self.press_id = self.canvas.mpl_connect('button_press_event', self.canvas_press) self.release_id = self.canvas.mpl_connect('button_release_event', self.canvas_release) self.motion_id = self.canvas.mpl_connect('motion_notify_event', self.canvas_motion) def splitter_moved(self, pos, index): self.invalidate_image() def progress_start(self): "Handler for loading progress start event" self._progress = 0 self._progress_dialog = ProgressDialog(self.window()) self._progress_dialog.show() self._progress_dialog.task = self.tr('Opening file') QtGui.QApplication.instance().setOverrideCursor(QtCore.Qt.WaitCursor) def progress_update(self, progress): "Handler for loading progress update event" now = time.time() if ((self._progress_update is None) or (now - self._progress_update) > 0.2): if self._progress_dialog.cancelled: raise KeyboardInterrupt self._progress_update = now if progress != self._progress: self._progress_dialog.progress = progress self._progress = progress def progress_finish(self): "Handler for loading progress finished event" QtGui.QApplication.instance().restoreOverrideCursor() if self._progress_dialog is not None: self._progress_dialog.close() self._progress_dialog = None def canvas_popup(self, pos): "Handler for canvas context menu event" menu = QtGui.QMenu(self) menu.addAction(self.window().ui.zoom_mode_action) menu.addAction(self.window().ui.pan_mode_action) menu.addSeparator() menu.addAction(self.window().ui.zoom_in_action) menu.addAction(self.window().ui.zoom_out_action) menu.addAction(self.window().ui.reset_zoom_action) menu.addSeparator() menu.addAction(self.window().ui.home_axes_action) menu.addAction(self.window().ui.reset_axes_action) menu.popup(self.canvas.mapToGlobal(pos)) def canvas_motion(self, event): "Handler for mouse movement over graph canvas" raise NotImplementedError def canvas_press(self, event): "Handler for mouse press on graph canvas" if event.button != 1: return if event.inaxes != self.image_axes: return self._drag_start = Coord(event.x, event.y) if self.window().ui.zoom_mode_action.isChecked(): self._zoom_id = self.canvas.mpl_connect('motion_notify_event', self.canvas_zoom_motion) elif self.window().ui.pan_mode_action.isChecked(): self._pan_id = self.canvas.mpl_connect('motion_notify_event', self.canvas_pan_motion) self._pan_crop = Crop(top=self.ui.crop_top_spinbox.value(), left=self.ui.crop_left_spinbox.value(), bottom=self.ui.crop_bottom_spinbox.value(), right=self.ui.crop_right_spinbox.value()) self.redraw_timer.setInterval(REDRAW_TIMEOUT_PAN) def canvas_pan_motion(self, event): "Handler for mouse movement in pan mode" inverse = self.image_axes.transData.inverted() start_x, start_y = inverse.transform_point(self._drag_start) end_x, end_y = inverse.transform_point((event.x, event.y)) delta = Coord(int(start_x - end_x), int(start_y - end_y)) if (self._pan_crop.left + delta.x >= 0) and (self._pan_crop.right - delta.x >= 0): self.ui.crop_left_spinbox.setValue(self._pan_crop.left + delta.x) self.ui.crop_right_spinbox.setValue(self._pan_crop.right - delta.x) if (self._pan_crop.top + delta.y >= 0) and (self._pan_crop.bottom - delta.y >= 0): self.ui.crop_top_spinbox.setValue(self._pan_crop.top + delta.y) self.ui.crop_bottom_spinbox.setValue(self._pan_crop.bottom - delta.y) def canvas_zoom_motion(self, event): "Handler for mouse movement in zoom mode" # Calculate the display coordinates of the selection box_left, box_top, box_right, box_bottom = self.image_axes.bbox.extents height = self.figure.bbox.height band_left = max(min(self._drag_start.x, event.x), box_left) band_right = min(max(self._drag_start.x, event.x), box_right) band_top = max(min(self._drag_start.y, event.y), box_top) band_bottom = min(max(self._drag_start.y, event.y), box_bottom) rectangle = (band_left, height - band_top, band_right - band_left, band_top - band_bottom) # Calculate the data coordinates of the selection. Note that top and # bottom are reversed by this conversion inverse = self.image_axes.transData.inverted() data_left, data_bottom = inverse.transform_point((band_left, band_top)) data_right, data_top = inverse.transform_point( (band_right, band_bottom)) # Ignore the drag operation until the total number of data-points in # the selection exceeds the threshold if (abs(data_right - data_left) * abs(data_bottom - data_top)) > ZOOM_THRESHOLD: self._zoom_rect = (data_left, data_top, data_right, data_bottom) self.window().statusBar().showMessage( self.tr('Crop from ({left:.0f}, {top:.0f}) to ' '({right:.0f}, {bottom:.0f})').format( left=data_left, top=data_top, right=data_right, bottom=data_bottom)) self.canvas.drawRectangle(rectangle) else: self._zoom_rect = None self.window().statusBar().clearMessage() self.canvas.draw() def canvas_release(self, event): "Handler for mouse release on graph canvas" if self._pan_id: self.window().statusBar().clearMessage() self.canvas.mpl_disconnect(self._pan_id) self._pan_id = None self.redraw_timer.setInterval(REDRAW_TIMEOUT_DEFAULT) if self._zoom_id: self.window().statusBar().clearMessage() self.canvas.mpl_disconnect(self._zoom_id) self._zoom_id = None if self._zoom_rect: ( data_left, data_top, data_right, data_bottom, ) = self._zoom_rect data_left = ((data_left / self.ui.x_scale_spinbox.value()) - self.ui.x_offset_spinbox.value()) data_right = ((data_right / self.ui.x_scale_spinbox.value()) - self.ui.x_offset_spinbox.value()) data_top = ((data_top / self.ui.y_scale_spinbox.value()) - self.ui.y_offset_spinbox.value()) data_bottom = ( (data_bottom / self.ui.y_scale_spinbox.value()) - self.ui.y_offset_spinbox.value()) self.ui.crop_left_spinbox.setValue(data_left) self.ui.crop_top_spinbox.setValue(data_top) self.ui.crop_right_spinbox.setValue(self._file.x_size - data_right) self.ui.crop_bottom_spinbox.setValue(self._file.y_size - data_bottom) self.canvas.draw() def channel_changed(self): "Handler for data channel change event" self.invalidate_data() self.crop_changed() def crop_changed(self, value=None): "Handler for crop_*_spinbox change event" self.cropChanged.emit() @property def zoom_factor(self): "Returns the percentage by which zoom in/out will operate" factor = 0.2 height, width = self.data_cropped.shape[:2] return (max(1.0, width * factor), max(1.0, height * factor)) @property def can_zoom_in(self): "Returns True if the image can be zoomed" height, width = self.data_cropped.shape[:2] x_factor, y_factor = self.zoom_factor return (width - x_factor * 2) * (height - y_factor * 2) > ZOOM_THRESHOLD @property def can_zoom_out(self): "Returns True if the image is zoomed" return (self.ui.crop_left_spinbox.value() > 0 or self.ui.crop_right_spinbox.value() > 0 or self.ui.crop_top_spinbox.value() > 0 or self.ui.crop_bottom_spinbox.value()) def zoom_in(self): "Zooms the image in by a fixed amount" x_factor, y_factor = self.zoom_factor self.ui.crop_left_spinbox.setValue(self.ui.crop_left_spinbox.value() + x_factor) self.ui.crop_right_spinbox.setValue( self.ui.crop_right_spinbox.value() + x_factor) self.ui.crop_top_spinbox.setValue(self.ui.crop_top_spinbox.value() + y_factor) self.ui.crop_bottom_spinbox.setValue( self.ui.crop_bottom_spinbox.value() + y_factor) def zoom_out(self): "Zooms the image out by a fixed amount" x_factor, y_factor = self.zoom_factor self.ui.crop_left_spinbox.setValue( max(0.0, self.ui.crop_left_spinbox.value() - x_factor)) self.ui.crop_right_spinbox.setValue( max(0.0, self.ui.crop_right_spinbox.value() - x_factor)) self.ui.crop_top_spinbox.setValue( max(0.0, self.ui.crop_top_spinbox.value() - y_factor)) self.ui.crop_bottom_spinbox.setValue( max(0.0, self.ui.crop_bottom_spinbox.value() - y_factor)) def reset_zoom(self): "Handler for reset_zoom_action triggered event" self.ui.crop_left_spinbox.setValue(0) self.ui.crop_right_spinbox.setValue(0) self.ui.crop_top_spinbox.setValue(0) self.ui.crop_bottom_spinbox.setValue(0) def reset_axes(self): "Handler for the reset_axes_action triggered event" self.ui.scale_locked_check.setChecked(True) self.ui.x_scale_spinbox.setValue(1.0) self.ui.y_scale_spinbox.setValue(1.0) self.ui.offset_locked_check.setChecked(True) self.ui.x_offset_spinbox.setValue(0.0) self.ui.y_offset_spinbox.setValue(0.0) def home_axes(self): "Handler for home_axes_action triggered event" self.ui.scale_locked_check.setChecked(True) self.ui.x_scale_spinbox.setValue(1.0) self.ui.y_scale_spinbox.setValue(1.0) self.ui.offset_locked_check.setChecked(False) self.ui.x_offset_spinbox.setValue(-self.ui.crop_left_spinbox.value()) self.ui.y_offset_spinbox.setValue(-self.ui.crop_top_spinbox.value()) def x_scale_changed(self, value): "Handler for x_scale_spinbox change event" if self.ui.scale_locked_check.isChecked(): self.ui.y_scale_spinbox.setValue(value) self.invalidate_image() def y_scale_changed(self, value): "Handler for y_scale_spinbox change event" if self.ui.scale_locked_check.isChecked(): self.ui.x_scale_spinbox.setValue(value) self.invalidate_image() def x_offset_changed(self, value): "Handler for x_offset_spinbox change event" if self.ui.offset_locked_check.isChecked(): self.ui.y_offset_spinbox.setValue(value) self.invalidate_image() def y_offset_changed(self, value): "Handler for x_offset_spinbox change event" if self.ui.offset_locked_check.isChecked(): self.ui.x_offset_spinbox.setValue(value) self.invalidate_image() def default_title_clicked(self): "Handler for default_title_button click event" raise NotImplementedError def clear_title_clicked(self): "Handler for clear_title_button click event" self.ui.title_edit.clear() def title_info_clicked(self, items): "Handler for title_info_button click event" from rastools.rasviewer.title_info_dialog import TitleInfoDialog if not self._info_dialog: self._info_dialog = TitleInfoDialog(self) self._info_dialog.ui.template_list.clear() for key, value in sorted(self.format_dict().items()): if isinstance(value, type('')): if '\n' in value: value = value.splitlines()[0].rstrip() self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem(['{{{0}}}'.format(key), value])) elif isinstance(value, int): self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem( ['{{{0}}}'.format(key), '{0}'.format(value)])) if 0 < value < 10: self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem([ '{{{0}:02d}}'.format(key), '{0:02d}'.format(value) ])) elif isinstance(value, float): self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem( ['{{{0}}}'.format(key), '{0}'.format(value)])) self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem( ['{{{0}:.2f}}'.format(key), '{0:.2f}'.format(value)])) elif isinstance(value, dt.datetime): self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem( ['{{{0}}}'.format(key), '{0}'.format(value)])) self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem([ '{{{0}:%Y-%m-%d}}'.format(key), '{0:%Y-%m-%d}'.format(value) ])) self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem([ '{{{0}:%H:%M:%S}}'.format(key), '{0:%H:%M:%S}'.format(value) ])) self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem([ '{{{0}:%A, %d %b %Y, %H:%M:%S}}'.format(key), '{0:%A, %d %b %Y, %H:%M:%S}'.format(value) ])) else: self._info_dialog.ui.template_list.addTopLevelItem( QtGui.QTreeWidgetItem( ['{{{0}}}'.format(key), '{0}'.format(value)])) self._info_dialog.show() @property def data(self): "Returns the original data array" raise NotImplementedError @property def data_cropped(self): "Returns the data after cropping" raise NotImplementedError @property def x_limits(self): "Returns a tuple of the X-axis limits after scaling and offset" if self.data_cropped is not None: return Range( (self.ui.x_scale_spinbox.value() or 1.0) * (self.ui.x_offset_spinbox.value() + self.ui.crop_left_spinbox.value()), (self.ui.x_scale_spinbox.value() or 1.0) * (self.ui.x_offset_spinbox.value() + self._file.x_size - self.ui.crop_right_spinbox.value())) @property def y_limits(self): "Returns a tuple of the Y-axis limits after scaling and offset" if self.data_cropped is not None: return Range( (self.ui.y_scale_spinbox.value() or 1.0) * (self.ui.y_offset_spinbox.value() + self._file.y_size - self.ui.crop_bottom_spinbox.value()), (self.ui.y_scale_spinbox.value() or 1.0) * (self.ui.y_offset_spinbox.value() + self.ui.crop_top_spinbox.value())) @property def axes_visible(self): "Returns True if the axes should be shown" return hasattr(self.ui, 'axes_check') and self.ui.axes_check.isChecked() @property def colorbar_visible(self): "Returns True if the colorbar should be shown" return hasattr( self.ui, 'colorbar_check') and self.ui.colorbar_check.isChecked() @property def histogram_visible(self): "Returns True if the histogram should be shown" return hasattr( self.ui, 'histogram_check') and self.ui.histogram_check.isChecked() @property def margin_visible(self): "Returns True if the image margins should be shown" return (self.axes_visible or self.histogram_visible or self.colorbar_visible or bool(self.image_title)) @property def x_margin(self): "Returns the size of the left and right margins when drawing" return 0.75 if self.margin_visible else 0.0 @property def y_margin(self): "Returns the size of the top and bottom margins when drawing" return 0.25 if self.margin_visible else 0.0 @property def sep_margin(self): "Returns the size of the separator between image elements" return 0.3 @property def image_title(self): "Returns the text of the image title after substitution" result = '' try: if self.ui.title_edit.toPlainText(): result = str(self.ui.title_edit.toPlainText()).format( **self.format_dict()) except KeyError as exc: self.ui.title_error_label.setText( 'Unknown template "{}"'.format(exc)) self.ui.title_error_label.show() except ValueError as exc: self.ui.title_error_label.setText(str(exc)) self.ui.title_error_label.show() else: self.ui.title_error_label.hide() return result @property def figure_box(self): "Returns the overall bounding box" return BoundingBox(0.0, 0.0, self.figure.get_figwidth(), self.figure.get_figheight()) @property def colorbar_box(self): "Returns the colorbar bounding box" return BoundingBox(self.x_margin, self.y_margin, self.figure_box.width - (self.x_margin * 2), 0.5 if self.colorbar_visible else 0.0) @property def title_box(self): "Returns the title bounding box" return BoundingBox( self.x_margin, self.figure_box.height - (self.y_margin + 1.0 if bool(self.image_title) else 0.0), self.figure_box.width - (self.x_margin * 2), 1.0 if bool(self.image_title) else 0.0) @property def histogram_box(self): "Returns the histogram bounding box" return BoundingBox( self.x_margin, self.colorbar_box.top + (self.sep_margin if self.colorbar_visible else 0.0), self.figure_box.width - (self.x_margin * 2), (self.figure_box.height - (self.y_margin * 2) - self.colorbar_box.height - self.title_box.height - (self.sep_margin if self.colorbar_visible else 0.0) - (self.sep_margin if bool(self.image_title) else 0.0)) / 2.0 if self.histogram_visible else 0.0) @property def image_box(self): "Returns the image bounding box" return BoundingBox( self.x_margin, self.histogram_box.top + (self.sep_margin if self.colorbar_visible or self.histogram_visible else 0.0), self.figure_box.width - (self.x_margin * 2), (self.figure_box.height - (self.y_margin * 2) - self.colorbar_box.height - self.title_box.height - self.histogram_box.height - (self.sep_margin if self.colorbar_visible else 0.0) - (self.sep_margin if self.histogram_visible else 0.0) - (self.sep_margin if bool(self.image_title) else 0.0))) def invalidate_image(self): "Invalidate the image" # Actually, this method doesn't immediately invalidate the image (as # this results in a horribly sluggish UI), but starts a timer which # causes a redraw after no invalidations have occurred for a period # (see __init__ for the duration) if self.redraw_timer.isActive(): self.redraw_timer.stop() self.redraw_timer.start() def redraw_timeout(self): "Handler for the redraw_timer's timeout event" self.redraw_timer.stop() self.redraw_figure() def redraw_figure(self): "Called to redraw the channel image" # The following tests ensure we don't try and draw anything while we're # still loading the file if self._file and self.data is not None: # Draw the various image elements within bounding boxes calculated # from the metrics above image = self.draw_image() self.draw_histogram() self.draw_colorbar(image) self.draw_title() self.canvas.draw() def draw_image(self): "Draws the image of the data within the specified figure" raise NotImplementedError def draw_histogram(self): "Draws the data's historgram within the figure" raise NotImplementedError def draw_colorbar(self, image): "Draws a range color-bar within the figure" raise NotImplementedError def draw_title(self): "Draws a title within the specified figure" box = self.title_box.relative_to(self.figure_box) if bool(self.image_title): if self.title_axes is None: self.title_axes = self.figure.add_axes(box) else: self.title_axes.clear() self.title_axes.set_position(box) self.title_axes.set_axis_off() # Render the title self.title_axes.text(0.5, 0, self.image_title, horizontalalignment='center', verticalalignment='baseline', multialignment='center', size='medium', family='sans-serif', transform=self.title_axes.transAxes) elif self.title_axes: self.figure.delaxes(self.title_axes) self.title_axes = None def format_dict(self): "Returns UI settings in a dict for use in format substitutions" raise NotImplementedError