def curvelinear_test1(fig): """ grid for custom transform. """ def tr(x, y): x, y = np.asarray(x), np.asarray(y) return x, y - x def inv_tr(x, y): x, y = np.asarray(x), np.asarray(y) return x, y + x grid_helper = GridHelperCurveLinear((tr, inv_tr)) ax1 = Subplot(fig, 1, 2, 1, grid_helper=grid_helper) # ax1 will have a ticks and gridlines defined by the given # transform (+ transData of the Axes). Note that the transform of # the Axes itself (i.e., transData) is not affected by the given # transform. fig.add_subplot(ax1) xx, yy = tr([3, 6], [5.0, 10.]) ax1.plot(xx, yy, linewidth=2.0) ax1.set_aspect(1.) ax1.set_xlim(0, 10.) ax1.set_ylim(0, 10.) ax1.axis["t"] = ax1.new_floating_axis(0, 3.) ax1.axis["t2"] = ax1.new_floating_axis(1, 7.) ax1.grid(True, zorder=0)
def plotCorrelation(tauArray,kappaMatrix,kappaLower=None,kappaUpper=None,CI=None,amplify=1): """Plots Pearson Correlation Coefficient K(t,tau) with rotated axis to indicate absolute t, and relative time shift tau, between two signals x(t),y(t). Specified matrix has to be square with values -1 < p < +1 with corresponding time array giving the absolute time, t of the centers of each correlated window.""" # defining tranformation for relative time shifts def R(x, y): x, y = asarray(x), asarray(y) #return x,y return (2*x - y)/2, (y + 2*x)/2 def Rt(x, y): x, y = asarray(x), asarray(y) #return x,y return x + y, x - y # create figure with rotated axes fig = figure(figsize=(10, 10),frameon=False) grid_locator = angle_helper.LocatorDMS(20) grid_helper = GridHelperCurveLinear((R, Rt), grid_locator1=grid_locator, grid_locator2=grid_locator) ax = Subplot(fig, 1, 1, 1, grid_helper=grid_helper) fig.add_subplot(ax);ax.axis('off'); # copying over matrix K = array(kappaMatrix) # zero out correlations if confidence intervals overlap zero if all(kappaLower != None) and all(kappaUpper != None) : K[ (kappaLower<0) * (0<kappaUpper) ] = 0 # zero out statistically insignificant correlations if all(CI != None) : K[ abs(kappaMatrix) < CI ] = 0 # display pearson correlation matrix with +ive in red and -ive in blue ax.imshow(K,cmap="RdBu_r",interpolation="none",origin="bottom", extent = (tauArray[0],tauArray[-1],tauArray[0],tauArray[-1]),vmin=-1.0/amplify,vmax=1.0/amplify) # display rotated axes time,t and time delay,tau ax.axis["tau"] = tau = ax.new_floating_axis(0,0) ax.axis["t"] = t = ax.new_floating_axis(1,0) # setting axes options ax.set_xlim(tauArray[0],tauArray[-1]) ax.set_ylim(tauArray[0],tauArray[-1]) ax.grid(which="both") ax.set_aspect(1) return fig
def setup_axes1(self, fig, T_ticks, subplotshape=None): """ A simple one. """ deg = -45. self.tr = Affine2D().rotate_deg(deg) theta_ticks = [] #np.arange(theta_min, theta_max, d_T) grid_helper = GridHelperCurveLinear( self.tr, grid_locator1=FixedLocator(T_ticks), grid_locator2=FixedLocator(theta_ticks)) if subplotshape is None: subplotshape = (1, 1, 1) ax1 = Subplot(fig, *subplotshape, grid_helper=grid_helper) # ax1 will have a ticks and gridlines defined by the given # transform (+ transData of the Axes). Note that the transform of # the Axes itself (i.e., transData) is not affected by the given # transform. fig.add_subplot(ax1) # SW, SE, NE, NW corners = np.array([[-25., -20.], [30., 40.], [-40., 120.], [-105., 60.]]) corners_t = self._tf(corners[:, 0], corners[:, 1]) # ax1.set_aspect(1.) x_min, x_max = self.x_range ax1.set_xlim(x_min, x_max) ax1.set_ylim(*self.y_range) ax1.set_xlabel('Temperature [C]') ax1.set_aspect(1) #ax1.axis["t"]=ax1.new_floating_axis(0, 0.) #T_axis = ax1.axis['t'] #theta_axis = ax1.axis["t2"]=ax1.new_floating_axis(1, 0.) # plot.draw() # plot.show() self.ax1 = ax1
def setup_axes1(self, fig, T_ticks, subplotshape=None): """ A simple one. """ deg = -45. self.tr = Affine2D().rotate_deg(deg) theta_ticks = [] #np.arange(theta_min, theta_max, d_T) grid_helper = GridHelperCurveLinear(self.tr, grid_locator1=FixedLocator(T_ticks), grid_locator2=FixedLocator(theta_ticks)) if subplotshape is None: subplotshape = (1,1,1) ax1 = Subplot(fig, *subplotshape, grid_helper=grid_helper) # ax1 will have a ticks and gridlines defined by the given # transform (+ transData of the Axes). Note that the transform of # the Axes itself (i.e., transData) is not affected by the given # transform. fig.add_subplot(ax1) # SW, SE, NE, NW corners = np.array([[-25., -20.], [30., 40.], [-40., 120.], [-105., 60.]]) corners_t = self._tf(corners[:,0], corners[:,1]) # ax1.set_aspect(1.) x_min, x_max = self.x_range ax1.set_xlim(x_min, x_max) ax1.set_ylim(*self.y_range) ax1.set_xlabel('Temperature [C]') ax1.set_aspect(1) #ax1.axis["t"]=ax1.new_floating_axis(0, 0.) #T_axis = ax1.axis['t'] #theta_axis = ax1.axis["t2"]=ax1.new_floating_axis(1, 0.) # plot.draw() # plot.show() self.ax1 = ax1
def curvelinear_test1(fig): """ grid for custom transform. """ def tr(x, y): x, y = np.asarray(x), np.asarray(y) return x, y-x def inv_tr(x,y): x, y = np.asarray(x), np.asarray(y) return x, y+x grid_helper = GridHelperCurveLinear((tr, inv_tr)) ax1 = Subplot(fig, 1, 2, 1, grid_helper=grid_helper) fig.add_subplot(ax1) xx, yy = tr([3, 6], [5.0, 10.]) ax1.plot(xx, yy) ax1.set_aspect(1.) ax1.set_xlim(0, 10.) ax1.set_ylim(0, 10.) ax1.axis["t"]=ax1.new_floating_axis(0, 3.) ax1.axis["t2"]=ax1.new_floating_axis(1, 7.) ax1.grid(True)
def curvelinear_test1(fig): """ Grid for custom transform. """ def tr(x, y): x, y = numpy.asarray(x), numpy.asarray(y) return x, y - (2 * x) # return x + (5 * y), (7 * y) + (3 * x) def inv_tr(x, y): x, y = numpy.asarray(x), numpy.asarray(y) return x, y + (2 * x) grid_helper = GridHelperCurveLinear((tr, inv_tr)) ax1 = Subplot(fig, 1, 1, 1, grid_helper=grid_helper) # ax1 will have a ticks and gridlines defined by the given # transform (+ transData of the Axes). Note that the transform of # the Axes itself (i.e., transData) is not affected by the given # transform. fig.add_subplot(ax1) xx, yy = tr([0, 1], [0, 2]) ax1.plot(xx, yy, linewidth=2.0) ax1.set_aspect(1) ax1.set_xlim(-3, 3) ax1.set_ylim(-3, 3) ax1.axis["t"] = ax1.new_floating_axis( 0, 0 ) # first argument appears to be slope, second argument appears to be starting point on vertical ax1.axis["t2"] = ax1.new_floating_axis(1, 0) ax1.axhline(y=0, color='r') ax1.axvline(x=0, color='r') ax1.grid(True, zorder=0)
from mpl_toolkits.axisartist import Subplot from mpl_toolkits.axisartist.grid_helper_curvelinear import \ GridHelperCurveLinear def tr(x, y): # source (data) to target (rectilinear plot) coordinates x, y = numpy.asarray(x), numpy.asarray(y) return x + 0.2 * y, y - x def inv_tr(x, y): x, y = numpy.asarray(x), numpy.asarray(y) return x - 0.2 * y, y + x grid_helper = GridHelperCurveLinear((tr, inv_tr)) ax6 = Subplot(fig, nrow, ncol, 6, grid_helper=grid_helper) fig.add_subplot(ax6) ax6.set_title('non-ortho axes') xx, yy = tr([3, 6], [5.0, 10.]) ax6.plot(xx, yy) ax6.set_aspect(1.) ax6.set_xlim(0, 10.) ax6.set_ylim(0, 10.) ax6.axis["t"] = ax6.new_floating_axis(0, 3.) ax6.axis["t2"] = ax6.new_floating_axis(1, 7.) ax6.grid(True) plt.show()
class SliceViewerDataView(QWidget): """The view for the data portion of the sliceviewer""" def __init__(self, presenter: IDataViewSubscriber, dims_info, can_normalise, parent=None, conf=None): super().__init__(parent) self.presenter = presenter self.image = None self.line_plots_active = False self.can_normalise = can_normalise self.nonortho_transform = None self.conf = conf self._line_plots = None self._image_info_tracker = None self._region_selection_on = False self._orig_lims = None # Dimension widget self.dimensions_layout = QGridLayout() self.dimensions = DimensionWidget(dims_info, parent=self) self.dimensions.dimensionsChanged.connect(self.presenter.dimensions_changed) self.dimensions.valueChanged.connect(self.presenter.slicepoint_changed) self.dimensions_layout.addWidget(self.dimensions, 1, 0, 1, 1) self.colorbar_layout = QVBoxLayout() self.colorbar_layout.setContentsMargins(0, 0, 0, 0) self.colorbar_layout.setSpacing(0) self.image_info_widget = ImageInfoWidget(self) self.image_info_widget.setToolTip("Information about the selected pixel") self.track_cursor = QCheckBox("Track Cursor", self) self.track_cursor.setToolTip( "Update the image readout table when the cursor is over the plot. " "If unticked the table will update only when the plot is clicked") self.dimensions_layout.setHorizontalSpacing(10) self.dimensions_layout.addWidget(self.track_cursor, 0, 1, Qt.AlignRight) self.dimensions_layout.addWidget(self.image_info_widget, 1, 1) self.track_cursor.setChecked(True) self.track_cursor.stateChanged.connect(self.on_track_cursor_state_change) # normalization options if can_normalise: self.norm_label = QLabel("Normalization") self.colorbar_layout.addWidget(self.norm_label) self.norm_opts = QComboBox() self.norm_opts.addItems(["None", "By bin width"]) self.norm_opts.setToolTip("Normalization options") self.colorbar_layout.addWidget(self.norm_opts) # MPL figure + colorbar self.fig = Figure() self.ax = None self.image = None self._grid_on = False self.fig.set_facecolor(self.palette().window().color().getRgbF()) self.canvas = SliceViewerCanvas(self.fig) self.canvas.mpl_connect('button_release_event', self.mouse_release) self.canvas.mpl_connect('button_press_event', self.presenter.canvas_clicked) self.colorbar_label = QLabel("Colormap") self.colorbar_layout.addWidget(self.colorbar_label) norm_scale = self.get_default_scale_norm() self.colorbar = ColorbarWidget(self, norm_scale) self.colorbar.cmap.setToolTip("Colormap options") self.colorbar.crev.setToolTip("Reverse colormap") self.colorbar.norm.setToolTip("Colormap normalisation options") self.colorbar.powerscale.setToolTip("Power colormap scale") self.colorbar.cmax.setToolTip("Colormap maximum limit") self.colorbar.cmin.setToolTip("Colormap minimum limit") self.colorbar.autoscale.setToolTip("Automatically changes colormap limits when zooming on the plot") self.colorbar_layout.addWidget(self.colorbar) self.colorbar.colorbarChanged.connect(self.update_data_clim) self.colorbar.scaleNormChanged.connect(self.scale_norm_changed) # make width larger to fit image readout table self.colorbar.setMaximumWidth(200) # MPL toolbar self.toolbar_layout = QHBoxLayout() self.mpl_toolbar = SliceViewerNavigationToolbar(self.canvas, self, False) self.mpl_toolbar.gridClicked.connect(self.toggle_grid) self.mpl_toolbar.linePlotsClicked.connect(self.on_line_plots_toggle) self.mpl_toolbar.regionSelectionClicked.connect(self.on_region_selection_toggle) self.mpl_toolbar.homeClicked.connect(self.on_home_clicked) self.mpl_toolbar.nonOrthogonalClicked.connect(self.on_non_orthogonal_axes_toggle) self.mpl_toolbar.zoomPanClicked.connect(self.presenter.zoom_pan_clicked) self.mpl_toolbar.zoomPanFinished.connect(self.on_data_limits_changed) self.toolbar_layout.addWidget(self.mpl_toolbar) # Status bar self.status_bar = QStatusBar(parent=self) self.status_bar.setStyleSheet('QStatusBar::item {border: None;}') # Hide spacers between button and label self.status_bar_label = QLabel() self.help_button = QToolButton() self.help_button.setText("?") self.status_bar.addWidget(self.help_button) self.status_bar.addWidget(self.status_bar_label) # layout layout = QGridLayout(self) layout.setSpacing(1) layout.addLayout(self.dimensions_layout, 0, 0, 1, 2) layout.addLayout(self.toolbar_layout, 1, 0, 1, 1) layout.addLayout(self.colorbar_layout, 1, 1, 3, 1) layout.addWidget(self.canvas, 2, 0, 1, 1) layout.addWidget(self.status_bar, 3, 0, 1, 1) layout.setRowStretch(2, 1) @property def grid_on(self): return self._grid_on @property def line_plotter(self): return self._line_plots @property def nonorthogonal_mode(self): return self.nonortho_transform is not None def create_axes_orthogonal(self, redraw_on_zoom=False): """Create a standard set of orthogonal axes :param redraw_on_zoom: If True then when scroll zooming the canvas is redrawn immediately """ self.clear_figure() self.nonortho_transform = None self.ax = self.fig.add_subplot(111, projection='mantid') self.enable_zoom_on_mouse_scroll(redraw_on_zoom) if self.grid_on: self.ax.grid(self.grid_on) if self.line_plots_active: self.add_line_plots() self.plot_MDH = self.plot_MDH_orthogonal self.canvas.draw_idle() def create_axes_nonorthogonal(self, transform): self.clear_figure() self.set_nonorthogonal_transform(transform) self.ax = CurveLinearSubPlot(self.fig, 1, 1, 1, grid_helper=GridHelperCurveLinear( (transform.tr, transform.inv_tr))) # don't redraw on zoom as the data is rebinned and has to be redrawn again anyway self.enable_zoom_on_mouse_scroll(redraw=False) self.set_grid_on() self.fig.add_subplot(self.ax) self.plot_MDH = self.plot_MDH_nonorthogonal self.canvas.draw_idle() def enable_zoom_on_mouse_scroll(self, redraw): """Enable zoom on scroll the mouse wheel for the created axes :param redraw: Pass through to redraw option in enable_zoom_on_scroll """ self.canvas.enable_zoom_on_scroll(self.ax, redraw=redraw, toolbar=self.mpl_toolbar, callback=self.on_data_limits_changed) def add_line_plots(self, toolcls, exporter): """Assuming line plots are currently disabled, enable them on the current figure The image axes must have been created first. :param toolcls: Use this class to handle creating the plots :param exporter: Object defining methods to export cuts/roi """ if self.line_plots_active: return self.line_plots_active = True self._line_plots = toolcls(LinePlots(self.ax, self.colorbar), exporter) self.status_bar_label.setText(self._line_plots.status_message()) self.canvas.setFocus() self.mpl_toolbar.set_action_checked(ToolItemText.LINEPLOTS, True, trigger=False) def switch_line_plots_tool(self, toolcls, exporter): """Assuming line plots are currently enabled then switch the tool used to generate the plot curves. :param toolcls: Use this class to handle creating the plots """ if not self.line_plots_active: return # Keep the same set of line plots axes but swap the selection tool plotter = self._line_plots.plotter plotter.delete_line_plot_lines() self._line_plots.disconnect() self._line_plots = toolcls(plotter, exporter) self.status_bar_label.setText(self._line_plots.status_message()) self.canvas.setFocus() self.canvas.draw_idle() def remove_line_plots(self): """Assuming line plots are currently enabled, remove them from the current figure """ if not self.line_plots_active: return self._line_plots.plotter.close() self.status_bar_label.clear() self._line_plots = None self.line_plots_active = False def plot_MDH_orthogonal(self, ws, **kwargs): """ clears the plot and creates a new one using a MDHistoWorkspace """ self.clear_image() self.image = self.ax.imshow(ws, origin='lower', aspect='auto', transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), **kwargs) # ensure the axes data limits are updated to match the # image. For example if the axes were zoomed and the # swap dimensions was clicked we need to restore the # appropriate extents to see the image in the correct place extent = self.image.get_extent() self.ax.set_xlim(extent[0], extent[1]) self.ax.set_ylim(extent[2], extent[3]) # Set the original data limits which get passed to the ImageInfoWidget so that # the mouse projection to data space is correct for MDH workspaces when zoomed/changing slices self._orig_lims = self.get_axes_limits() self.on_track_cursor_state_change(self.track_cursor_checked()) self.draw_plot() def plot_MDH_nonorthogonal(self, ws, **kwargs): self.clear_image() self.image = pcolormesh_nonorthogonal(self.ax, ws, self.nonortho_transform.tr, transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), **kwargs) self.on_track_cursor_state_change(self.track_cursor_checked()) # swapping dimensions in nonorthogonal mode currently resets back to the # full data limits as the whole axes has been recreated so we don't have # access to the original limits # pcolormesh clears any grid that was previously visible if self.grid_on: self.ax.grid(self.grid_on) self.draw_plot() def plot_matrix(self, ws, **kwargs): """ clears the plot and creates a new one using a MatrixWorkspace keeping the axes limits that have already been set """ # ensure view is correct if zoomed in while swapping dimensions # compute required extent and just have resampling imshow deal with it old_extent = None if self.image is not None: old_extent = self.image.get_extent() if self.image.transpose != self.dimensions.transpose: e1, e2, e3, e4 = old_extent old_extent = e3, e4, e1, e2 self.clear_image() self.image = self.ax.imshow(ws, origin='lower', aspect='auto', interpolation='none', transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), extent=old_extent, **kwargs) self.on_track_cursor_state_change(self.track_cursor_checked()) self.draw_plot() def clear_image(self): """Removes any image from the axes""" if self.image is not None: if self.line_plots_active: self._line_plots.plotter.delete_line_plot_lines() self.image_info_widget.cursorAt(DBLMAX, DBLMAX, DBLMAX) if hasattr(self.ax, "remove_artists_if"): self.ax.remove_artists_if(lambda art: art == self.image) else: self.image.remove() self.image = None def clear_figure(self): """Removes everything from the figure""" if self.line_plots_active: self._line_plots.plotter.close() self.line_plots_active = False self.image = None self.canvas.disable_zoom_on_scroll() self.fig.clf() self.ax = None def draw_plot(self): self.ax.set_title('') self.canvas.draw() if self.image: self.colorbar.set_mappable(self.image) self.colorbar.update_clim() self.mpl_toolbar.update() # clear nav stack if self.line_plots_active: self._line_plots.plotter.delete_line_plot_lines() self._line_plots.plotter.update_line_plot_labels() def export_region(self, limits, cut): """ React to a region selection that should be exported :param limits: 2-tuple of ((left, right), (bottom, top)) :param cut: A str denoting which cuts to export. """ self.presenter.export_region(limits, cut) def update_plot_data(self, data): """ This just updates the plot data without creating a new plot. The extents can change if the data has been rebinned. """ if self.nonortho_transform: self.image.set_array(data.T.ravel()) else: self.image.set_data(data.T) self.colorbar.update_clim() def track_cursor_checked(self): return self.track_cursor.isChecked() if self.track_cursor else False def on_track_cursor_state_change(self, state): """ Called to notify the current state of the track cursor box """ if self._image_info_tracker is not None: self._image_info_tracker.disconnect() if self._line_plots is not None and not self._region_selection_on: self._line_plots.disconnect() self._image_info_tracker = ImageInfoTracker(image=self.image, transform=self.nonortho_transform, do_transform=self.nonorthogonal_mode, widget=self.image_info_widget, cursor_transform=self._orig_lims) if state: self._image_info_tracker.connect() if self._line_plots and not self._region_selection_on: self._line_plots.connect() else: self._image_info_tracker.disconnect() if self._line_plots and not self._region_selection_on: self._line_plots.disconnect() def on_home_clicked(self): """Reset the view to encompass all of the data""" self.presenter.show_all_data_clicked() def on_line_plots_toggle(self, state): """Switch state of the line plots""" self.presenter.line_plots(state) def on_region_selection_toggle(self, state): """Switch state of the region selection""" self.presenter.region_selection(state) self._region_selection_on = state # If state is off and track cursor is on, make sure line plots are re-connected to move cursor if not state and self.track_cursor_checked(): if self._line_plots: self._line_plots.connect() def on_non_orthogonal_axes_toggle(self, state): """ Switch state of the non-orthognal axes on/off """ self.presenter.nonorthogonal_axes(state) def on_data_limits_changed(self): """ React to when the data limits have changed """ self.presenter.data_limits_changed() def deactivate_and_disable_tool(self, tool_text): """Deactivate a tool as if the control had been pressed and disable the functionality""" self.deactivate_tool(tool_text) self.disable_tool_button(tool_text) def activate_tool(self, tool_text): """Activate a given tool as if the control had been pressed""" self.mpl_toolbar.set_action_checked(tool_text, True) def deactivate_tool(self, tool_text): """Deactivate a given tool as if the tool button had been pressed""" self.mpl_toolbar.set_action_checked(tool_text, False) def enable_tool_button(self, tool_text): """Set a given tool button enabled so it can be interacted with""" self.mpl_toolbar.set_action_enabled(tool_text, True) def disable_tool_button(self, tool_text): """Set a given tool button disabled so it cannot be interacted with""" self.mpl_toolbar.set_action_enabled(tool_text, False) def get_axes_limits(self): """ Return the limits on the image axes transformed into the nonorthogonal frame if appropriate """ if self.image is None: return None else: xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() if self.nonorthogonal_mode: inv_tr = self.nonortho_transform.inv_tr # viewing axis y not aligned with plot axis xmin_p, ymax_p = inv_tr(xlim[0], ylim[1]) xmax_p, ymin_p = inv_tr(xlim[1], ylim[0]) xlim, ylim = (xmin_p, xmax_p), (ymin_p, ymax_p) return xlim, ylim def get_full_extent(self): """ Return the full extent of image - only applicable for plots of matrix workspaces """ if self.image and isinstance(self.image, samplingimage.SamplingImage): return self.image.get_full_extent() else: return None def set_axes_limits(self, xlim, ylim): """ Set the view limits on the image axes to the given extents. Assume the limits are in the orthogonal frame. :param xlim: 2-tuple of (xmin, xmax) :param ylim: 2-tuple of (ymin, ymax) """ self.ax.set_xlim(xlim) self.ax.set_ylim(ylim) def set_grid_on(self): """ If not visible sets the grid visibility """ if not self._grid_on: self._grid_on = True self.mpl_toolbar.set_action_checked(ToolItemText.GRID, state=self._grid_on) def set_nonorthogonal_transform(self, transform): """ Set the transform for nonorthogonal axes mode :param transform: An object with a tr method to transform from nonorthognal coordinates to display coordinates """ self.nonortho_transform = transform def show_temporary_status_message(self, msg, timeout_ms): """ Show a message in the status bar that disappears after a set period :param msg: A str message to display :param timeout_ms: Timeout in milliseconds to display the message for """ self.status_bar.showMessage(msg, timeout_ms) def toggle_grid(self, state): """ Toggle the visibility of the grid on the axes """ self._grid_on = state self.ax.grid(self._grid_on) self.canvas.draw_idle() def mouse_release(self, event): if event.inaxes != self.ax: return self.canvas.setFocus() if event.button == 1: self._image_info_tracker.on_cursor_at(event.xdata, event.ydata) if self.line_plots_active and not self._region_selection_on: self._line_plots.on_cursor_at(event.xdata, event.ydata) if event.button == 3: self.on_home_clicked() def deactivate_zoom_pan(self): self.deactivate_tool(ToolItemText.PAN) self.deactivate_tool(ToolItemText.ZOOM) def update_data_clim(self): self.image.set_clim(self.colorbar.colorbar.mappable.get_clim()) if self.line_plots_active: self._line_plots.plotter.update_line_plot_limits() self.canvas.draw_idle() def set_normalization(self, ws, **kwargs): normalize_by_bin_width, _ = get_normalize_by_bin_width(ws, self.ax, **kwargs) is_normalized = normalize_by_bin_width or ws.isDistribution() self.presenter.normalization = is_normalized if is_normalized: self.norm_opts.setCurrentIndex(1) else: self.norm_opts.setCurrentIndex(0) def get_default_scale_norm(self): scale = 'Linear' if self.conf is None: return scale if self.conf.has(SCALENORM): scale = self.conf.get(SCALENORM) if scale == 'Power' and self.conf.has(POWERSCALE): exponent = self.conf.get(POWERSCALE) scale = (scale, exponent) scale = "SymmetricLog10" if scale == 'Log' else scale return scale def scale_norm_changed(self): if self.conf is None: return scale = self.colorbar.norm.currentText() self.conf.set(SCALENORM, scale) if scale == 'Power': exponent = self.colorbar.powerscale_value self.conf.set(POWERSCALE, exponent)
class Tephigram: """ Generate a tephigram of one or more pressure and temperature data sets. """ def __init__( self, figure=None, isotherm_locator=None, dry_adiabat_locator=None, anchor=None, ): """ Initialise the tephigram transformation and plot axes. Kwargs: * figure: An existing :class:`matplotlib.figure.Figure` instance for the tephigram plot. If a figure is not provided, a new figure will be created by default. * isotherm_locator: A :class:`tephi.Locator` instance or a numeric step size for the isotherm lines. * dry_adiabat_locator: A :class:`tephi.Locator` instance or a numeric step size for the dry adiabat lines. * anchor: A sequence of two pressure, temperature pairs specifying the extent of the tephigram plot in terms of the bottom left hand corner and the top right hand corner. Pressure data points must be in units of mb or hPa, and temperature data points must be in units of degC. For example: .. plot:: :include-source: import matplotlib.pyplot as plt from numpy import column_stack import os.path import tephi from tephi import Tephigram dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dry_bulb = os.path.join(tephi.DATA_DIR, 'temps.txt') dew_data, temp_data = tephi.loadtxt(dew_point, dry_bulb) dews = column_stack((dew_data.pressure, dew_data.temperature)) temps = column_stack((temp_data.pressure, temp_data.temperature)) tpg = Tephigram() tpg.plot(dews, label='Dew-point', color='blue', linewidth=2) tpg.plot(temps, label='Dry-bulb', color='red', linewidth=2) plt.show() """ if not figure: # Create a default figure. self.figure = plt.figure(0, figsize=(9, 9)) else: self.figure = figure # Configure the locators. if isotherm_locator and not isinstance(isotherm_locator, Locator): if not isinstance(isotherm_locator, numbers.Number): raise ValueError("Invalid isotherm locator") locator_isotherm = Locator(isotherm_locator) else: locator_isotherm = isotherm_locator if dry_adiabat_locator and not isinstance(dry_adiabat_locator, Locator): if not isinstance(dry_adiabat_locator, numbers.Number): raise ValueError("Invalid dry adiabat locator") locator_theta = Locator(dry_adiabat_locator) else: locator_theta = dry_adiabat_locator # Define the tephigram coordinate-system transformation. self.tephi_transform = transforms.TephiTransform() ghelper = GridHelperCurveLinear( self.tephi_transform, tick_formatter1=_FormatterIsotherm(), grid_locator1=locator_isotherm, tick_formatter2=_FormatterTheta(), grid_locator2=locator_theta, ) self.axes = Subplot(self.figure, 1, 1, 1, grid_helper=ghelper) self.transform = self.tephi_transform + self.axes.transData self.axes.axis["isotherm"] = self.axes.new_floating_axis(1, 0) self.axes.axis["theta"] = self.axes.new_floating_axis(0, 0) self.axes.axis["left"].get_helper().nth_coord_ticks = 0 self.axes.axis["left"].toggle(all=True) self.axes.axis["bottom"].get_helper().nth_coord_ticks = 1 self.axes.axis["bottom"].toggle(all=True) self.axes.axis["top"].get_helper().nth_coord_ticks = 0 self.axes.axis["top"].toggle(all=False) self.axes.axis["right"].get_helper().nth_coord_ticks = 1 self.axes.axis["right"].toggle(all=True) self.axes.gridlines.set_linestyle("solid") self.figure.add_subplot(self.axes) # Configure default axes. axis = self.axes.axis["left"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("baseline") axis.major_ticklabels.set_rotation(135) axis = self.axes.axis["right"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("baseline") axis.major_ticklabels.set_rotation(-135) self.axes.axis["top"].major_ticklabels.set_fontsize(10) axis = self.axes.axis["bottom"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_ha("left") axis.major_ticklabels.set_va("top") axis.major_ticklabels.set_rotation(-45) # Isotherms: lines of constant temperature (degC). axis = self.axes.axis["isotherm"] axis.set_axis_direction("right") axis.set_axislabel_direction("-") axis.major_ticklabels.set_rotation(90) axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("bottom") axis.major_ticklabels.set_color("grey") axis.major_ticklabels.set_visible(False) # turned-off # Dry adiabats: lines of constant potential temperature (degC). axis = self.axes.axis["theta"] axis.set_axis_direction("right") axis.set_axislabel_direction("+") axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("bottom") axis.major_ticklabels.set_color("grey") axis.major_ticklabels.set_visible(False) # turned-off axis.line.set_linewidth(3) axis.line.set_linestyle("--") # Lock down the aspect ratio. self.axes.set_aspect(1.0) self.axes.grid(True) # Initialise the text formatter for the navigation status bar. self.axes.format_coord = self._status_bar # Factor in the tephigram transform. ISOBAR_TEXT["transform"] = self.transform WET_ADIABAT_TEXT["transform"] = self.transform MIXING_RATIO_TEXT["transform"] = self.transform # Create plot collections for the tephigram isopleths. func = partial( isopleths.isobar, MIN_THETA, MAX_THETA, self.axes, self.transform, ISOBAR_LINE, ) self._isobars = _PlotCollection( self.axes, ISOBAR_SPEC, MAX_PRESSURE, func, ISOBAR_TEXT, fixed=ISOBAR_FIXED, minimum=MIN_PRESSURE, ) func = partial( isopleths.wet_adiabat, MAX_PRESSURE, MIN_TEMPERATURE, self.axes, self.transform, WET_ADIABAT_LINE, ) self._wet_adiabats = _PlotCollection( self.axes, WET_ADIABAT_SPEC, MAX_WET_ADIABAT, func, WET_ADIABAT_TEXT, fixed=WET_ADIABAT_FIXED, minimum=MIN_WET_ADIABAT, xfocus=True, ) func = partial( isopleths.mixing_ratio, MIN_PRESSURE, MAX_PRESSURE, self.axes, self.transform, MIXING_RATIO_LINE, ) self._mixing_ratios = _PlotCollection( self.axes, MIXING_RATIO_SPEC, MIXING_RATIOS, func, MIXING_RATIO_TEXT, fixed=MIXING_RATIO_FIXED, ) # Initialise for the tephigram plot event handler. plt.connect("motion_notify_event", _handler) self.axes.tephigram = True self.axes.tephigram_original_delta_xlim = DEFAULT_WIDTH self.original_delta_xlim = DEFAULT_WIDTH self.axes.tephigram_transform = self.tephi_transform self.axes.tephigram_inverse = self.tephi_transform.inverted() self.axes.tephigram_isopleths = [ self._isobars, self._wet_adiabats, self._mixing_ratios, ] # The tephigram profiles. self._profiles = [] self.axes.tephigram_profiles = self._profiles # Center the plot around the anchor extent. self._anchor = anchor if self._anchor is not None: self._anchor = np.asarray(anchor) if (self._anchor.ndim != 2 or self._anchor.shape[-1] != 2 or len(self._anchor) != 2): msg = ("Invalid anchor, expecting [(bottom-left-pressure, " "bottom-left-temperature), (top-right-pressure, " "top-right-temperature)]") raise ValueError(msg) ( (bottom_pressure, bottom_temp), (top_pressure, top_temp), ) = self._anchor if (bottom_pressure - top_pressure) < 0: raise ValueError("Invalid anchor pressure range") if (bottom_temp - top_temp) < 0: raise ValueError("Invalid anchor temperature range") self._anchor = isopleths.Profile(anchor, self.axes) self._anchor.plot(visible=False) xlim, ylim = self._calculate_extents() self.axes.set_xlim(xlim) self.axes.set_ylim(ylim) def plot(self, data, **kwargs): """ Plot the environmental lapse rate profile of the pressure and temperature data points. The pressure and temperature data points are transformed into potential temperature and temperature data points before plotting. By default, the tephigram will automatically center the plot around all profiles. .. warning:: Pressure data points must be in units of mb or hPa, and temperature data points must be in units of degC. Args: * data: pressure and temperature pair data points. .. note:: All keyword arguments are passed through to :func:`matplotlib.pyplot.plot`. For example: .. plot:: :include-source: import matplotlib.pyplot as plt from tephi import Tephigram tpg = Tephigram() data = [[1006, 26.4], [924, 20.3], [900, 19.8], [850, 14.5], [800, 12.9], [755, 8.3]] profile = tpg.plot(data, color='red', linestyle='--', linewidth=2, marker='o') barbs = [(10, 45, 900), (20, 60, 850), (25, 90, 800)] profile.barbs(barbs) plt.show() For associating wind barbs with an environmental lapse rate profile, see :meth:`~tephi.isopleths.Profile.barbs`. """ profile = isopleths.Profile(data, self.axes) profile.plot(**kwargs) self._profiles.append(profile) # Center the tephigram plot around all the profiles. if self._anchor is None: xlim, ylim = self._calculate_extents(xfactor=0.25, yfactor=0.05) self.axes.set_xlim(xlim) self.axes.set_ylim(ylim) # Refresh the tephigram plot isopleths. _refresh_isopleths(self.axes) # Show the plot legend. if "label" in kwargs: font_properties = FontProperties(size="x-small") plt.legend( loc="upper left", fancybox=True, shadow=True, prop=font_properties, ) return profile def _status_bar(self, x_point, y_point): """Generate text for the interactive backend navigation status bar.""" temperature, theta = transforms.convert_xy2Tt(x_point, y_point) pressure, _ = transforms.convert_Tt2pT(temperature, theta) xlim = self.axes.get_xlim() zoom = (xlim[1] - xlim[0]) / self.original_delta_xlim msg = "T:{:.2f}, theta:{:.2f}, phi:{:.2f} (zoom:{:.3f})" text = msg.format(float(temperature), float(theta), float(pressure), zoom) return text def _calculate_extents(self, xfactor=None, yfactor=None): min_x = min_y = 1e10 max_x = max_y = -1e-10 profiles = self._profiles transform = self.tephi_transform.transform if self._anchor is not None: profiles = [self._anchor] for profile in profiles: temperature = profile.temperature.reshape(-1, 1) theta = profile.theta.reshape(-1, 1) xy_points = transform(np.concatenate((temperature, theta), axis=1)) x_points = xy_points[:, 0] y_points = xy_points[:, 1] min_x = np.min([min_x, np.min(x_points)]) min_y = np.min([min_y, np.min(y_points)]) max_x = np.max([max_x, np.max(x_points)]) max_y = np.max([max_y, np.max(y_points)]) if xfactor is not None: delta_x = max_x - min_x min_x, max_x = min_x - xfactor * delta_x, max_x + xfactor * delta_x if yfactor is not None: delta_y = max_y - min_y min_y, max_y = min_y - yfactor * delta_y, max_y + yfactor * delta_y return ([min_x, max_x], [min_y, max_y])
class Tephigram(object): """ Generate a tephigram of one or more pressure and temperature data sets. """ def __init__(self, figure=None, isotherm_locator=None, dry_adiabat_locator=None, anchor=None): """ Initialise the tephigram transformation and plot axes. Kwargs: * figure: An existing :class:`matplotlib.figure.Figure` instance for the tephigram plot. If a figure is not provided, a new figure will be created by default. * isotherm_locator: A :class:`edson.Locator` instance or a numeric step size for the isotherm lines. * dry_adiabat_locator: A :class:`edson.Locator` instance or a numeric step size for the dry adiabat lines. * anchor: A sequence of two pressure, temperature pairs specifying the extent of the tephigram plot in terms of the bottom left hand corner and the top right hand corner. Pressure data points must be in units of mb or hPa, and temperature data points must be in units of degC. For example: .. plot:: :include-source: import matplotlib.pyplot as plt import os.path import edson from edson import Tephigram dew_point = os.path.join(edson.RESOURCES_DIR, 'tephigram', 'dews.txt') dry_bulb = os.path.join(edson.RESOURCES_DIR, 'tephigram', 'temps.txt') dew_data, temp_data = edson.loadtxt(dew_point, dry_bulb) dews = zip(dew_data.pressure, dew_data.temperature) temps = zip(temp_data.pressure, temp_data.temperature) tephi = Tephigram() tephi.plot(dews, label='Dew-point', color='blue', linewidth=2, marker='s') tephi.plot(temps, label='Dry-bulb', color='red', linewidth=2, marker='o') plt.show() """ if not figure: # Create a default figure. self.figure = plt.figure(0, figsize=(9, 9)) else: self.figure = figure # Configure the locators. if isotherm_locator and not isinstance(isotherm_locator, Locator): if not isinstance(isotherm_locator, numbers.Number): raise ValueError('Invalid isotherm locator') locator_isotherm = Locator(isotherm_locator) else: locator_isotherm = isotherm_locator if dry_adiabat_locator and not isinstance(dry_adiabat_locator, Locator): if not isinstance(dry_adiabat_locator, numbers.Number): raise ValueError('Invalid dry adiabat locator') locator_theta = Locator(dry_adiabat_locator) else: locator_theta = dry_adiabat_locator # Define the tephigram coordinate-system transformation. self.tephi_transform = transforms.TephiTransform() grid_helper1 = GridHelperCurveLinear(self.tephi_transform, tick_formatter1=_FormatterIsotherm(), grid_locator1=locator_isotherm, tick_formatter2=_FormatterTheta(), grid_locator2=locator_theta) self.axes = Subplot(self.figure, 1, 1, 1, grid_helper=grid_helper1) self.transform = self.tephi_transform + self.axes.transData self.axes.axis['isotherm'] = self.axes.new_floating_axis(1, 0) self.axes.axis['theta'] = self.axes.new_floating_axis(0, 0) self.axes.axis['left'].get_helper().nth_coord_ticks = 0 self.axes.axis['left'].toggle(all=True) self.axes.axis['bottom'].get_helper().nth_coord_ticks = 1 self.axes.axis['bottom'].toggle(all=True) self.axes.axis['top'].get_helper().nth_coord_ticks = 0 self.axes.axis['top'].toggle(all=False) self.axes.axis['right'].get_helper().nth_coord_ticks = 1 self.axes.axis['right'].toggle(all=True) self.axes.gridlines.set_linestyle('solid') self.figure.add_subplot(self.axes) # Configure default axes. axis = self.axes.axis['left'] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va('baseline') axis.major_ticklabels.set_rotation(135) axis = self.axes.axis['right'] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va('baseline') axis.major_ticklabels.set_rotation(-135) self.axes.axis['top'].major_ticklabels.set_fontsize(10) axis = self.axes.axis['bottom'] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_ha('left') axis.major_ticklabels.set_va('top') axis.major_ticklabels.set_rotation(-45) # Isotherms: lines of constant temperature (degC). axis = self.axes.axis['isotherm'] axis.set_axis_direction('right') axis.set_axislabel_direction('-') axis.major_ticklabels.set_rotation(90) axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va('bottom') axis.major_ticklabels.set_color('grey') axis.major_ticklabels.set_visible(False) # turned-off # Dry adiabats: lines of constant potential temperature (degC). axis = self.axes.axis['theta'] axis.set_axis_direction('right') axis.set_axislabel_direction('+') axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va('bottom') axis.major_ticklabels.set_color('grey') axis.major_ticklabels.set_visible(False) # turned-off axis.line.set_linewidth(3) axis.line.set_linestyle('--') # Lock down the aspect ratio. self.axes.set_aspect(1.) self.axes.grid(True) # Initialise the text formatter for the navigation status bar. self.axes.format_coord = self._status_bar # Factor in the tephigram transform. ISOBAR_TEXT['transform'] = self.transform WET_ADIABAT_TEXT['transform'] = self.transform MIXING_RATIO_TEXT['transform'] = self.transform # Create plot collections for the tephigram isopleths. func = partial(isopleths.isobar, MIN_THETA, MAX_THETA, self.axes, self.transform, ISOBAR_LINE) self._isobars = _PlotCollection(self.axes, ISOBAR_SPEC, MAX_PRESSURE, func, ISOBAR_TEXT, fixed=ISOBAR_FIXED, minimum=MIN_PRESSURE) func = partial(isopleths.wet_adiabat, MAX_PRESSURE, MIN_TEMPERATURE, self.axes, self.transform, WET_ADIABAT_LINE) self._wet_adiabats = _PlotCollection(self.axes, WET_ADIABAT_SPEC, MAX_WET_ADIABAT, func, WET_ADIABAT_TEXT, fixed=WET_ADIABAT_FIXED, minimum=MIN_WET_ADIABAT, xfocus=True) func = partial(isopleths.mixing_ratio, MIN_PRESSURE, MAX_PRESSURE, self.axes, self.transform, MIXING_RATIO_LINE) self._mixing_ratios = _PlotCollection(self.axes, MIXING_RATIO_SPEC, MIXING_RATIOS, func, MIXING_RATIO_TEXT, fixed=MIXING_RATIO_FIXED) # Initialise for the tephigram plot event handler. plt.connect('motion_notify_event', _handler) self.axes.tephigram = True self.axes.tephigram_original_delta_xlim = self.original_delta_xlim = DEFAULT_WIDTH self.axes.tephigram_transform = self.tephi_transform self.axes.tephigram_inverse = self.tephi_transform.inverted() self.axes.tephigram_isopleths = [self._isobars, self._wet_adiabats, self._mixing_ratios] # The tephigram profiles. self._profiles = [] self.axes.tephigram_profiles = self._profiles # Center the plot around the anchor extent. self._anchor = anchor if self._anchor is not None: self._anchor = np.asarray(anchor) if self._anchor.ndim != 2 or self._anchor.shape[-1] != 2 or \ len(self._anchor) != 2: msg = 'Invalid anchor, expecting [(bottom-left-pressure, ' \ 'bottom-left-temperature), (top-right-pressure, ' \ 'top-right-temperature)]' raise ValueError(msg) (bottom_pressure, bottom_temp), \ (top_pressure, top_temp) = self._anchor if (bottom_pressure - top_pressure) < 0: raise ValueError('Invalid anchor pressure range') if (bottom_temp - top_temp) < 0: raise ValueError('Invalid anchor temperature range') self._anchor = isopleths.Profile(anchor, self.axes) self._anchor.plot(visible=False) xlim, ylim = self._calculate_extents() self.axes.set_xlim(xlim) self.axes.set_ylim(ylim) def plot(self, data, **kwargs): """ Plot the environmental lapse rate profile of the pressure and temperature data points. The pressure and temperature data points are transformed into potential temperature and temperature data points before plotting. By default, the tephigram will automatically center the plot around all profiles. .. warning:: Pressure data points must be in units of mb or hPa, and temperature data points must be in units of degC. Args: * data: pressure and temperature pair data points. .. note:: All keyword arguments are passed through to :func:`matplotlib.pyplot.plot`. For example: .. plot:: :include-source: import matplotlib.pyplot as plt from edson import Tephigram tephi = Tephigram() data = [[1006, 26.4], [924, 20.3], [900, 19.8], [850, 14.5], [800, 12.9], [755, 8.3]] profile = tephi.plot(data, color='red', linestyle='--', linewidth=2, marker='o') barbs = [(10, 45, 900), (20, 60, 850), (25, 90, 800)] profile.barbs(barbs) plt.show() For associating wind barbs with an environmental lapse rate profile, see :meth:`~edson.isopleths.Profile.barbs`. """ profile = isopleths.Profile(data, self.axes) profile.plot(**kwargs) self._profiles.append(profile) # Center the tephigram plot around all the profiles. if self._anchor is None: xlim, ylim = self._calculate_extents(xfactor=.25, yfactor=.05) self.axes.set_xlim(xlim) self.axes.set_ylim(ylim) # Refresh the tephigram plot isopleths. _refresh_isopleths(self.axes) # Show the plot legend. if 'label' in kwargs: font_properties = FontProperties(size='x-small') plt.legend(loc='upper left', fancybox=True, shadow=True, prop=font_properties) return profile def _status_bar(self, x_point, y_point): """Generate text for the interactive backend navigation status bar.""" temperature, theta = transforms.xy_to_temperature_theta(x_point, y_point) pressure, _ = transforms.temperature_theta_to_pressure_temperature(temperature, theta) xlim = self.axes.get_xlim() zoom = (xlim[1] - xlim[0]) / self.original_delta_xlim text = "T:%.2f, theta:%.2f, phi:%.2f (zoom:%.3f)" % (float(temperature), float(theta), float(pressure), zoom) return text def _calculate_extents(self, xfactor=None, yfactor=None): min_x = min_y = 1e10 max_x = max_y = -1e-10 profiles = self._profiles if self._anchor is not None: profiles = [self._anchor] for profile in profiles: xy_points = self.tephi_transform.transform(np.concatenate((profile.temperature.reshape(-1, 1), profile.theta.reshape(-1, 1)), axis=1)) x_points = xy_points[:, 0] y_points = xy_points[:, 1] min_x, min_y = np.min([min_x, np.min(x_points)]), np.min([min_y, np.min(y_points)]) max_x, max_y = np.max([max_x, np.max(x_points)]), np.max([max_y, np.max(y_points)]) if xfactor is not None: delta_x = max_x - min_x min_x, max_x = min_x - xfactor * delta_x, max_x + xfactor * delta_x if yfactor is not None: delta_y = max_y - min_y min_y, max_y = min_y - yfactor * delta_y, max_y + yfactor * delta_y return ([min_x, max_x], [min_y, max_y])
class SliceViewerDataView(QWidget): """The view for the data portion of the sliceviewer""" def __init__(self, presenter, dims_info, can_normalise, parent=None): super().__init__(parent) self.presenter = presenter self.image = None self.line_plots = False self.can_normalise = can_normalise self.nonortho_tr = None self.ws_type = dims_info[0]['type'] # Dimension widget self.dimensions_layout = QGridLayout() self.dimensions = DimensionWidget(dims_info, parent=self) self.dimensions.dimensionsChanged.connect( self.presenter.dimensions_changed) self.dimensions.valueChanged.connect(self.presenter.slicepoint_changed) self.dimensions_layout.addWidget(self.dimensions, 1, 0, 1, 1) self.colorbar_layout = QVBoxLayout() self.colorbar_layout.setContentsMargins(0, 0, 0, 0) self.colorbar_layout.setSpacing(0) self.image_info_widget = ImageInfoWidget(self.ws_type, self) self.track_cursor = QCheckBox("Track Cursor", self) self.track_cursor.setToolTip( "Update the image readout table when the cursor is over the plot. " "If unticked the table will update only when the plot is clicked") if self.ws_type == 'MDE': self.colorbar_layout.addWidget(self.image_info_widget, alignment=Qt.AlignCenter) self.colorbar_layout.addWidget(self.track_cursor) else: self.dimensions_layout.setHorizontalSpacing(10) self.dimensions_layout.addWidget(self.track_cursor, 0, 1, Qt.AlignRight) self.dimensions_layout.addWidget(self.image_info_widget, 1, 1) self.track_cursor.setChecked(True) # normalization options if can_normalise: self.norm_label = QLabel("Normalization") self.colorbar_layout.addWidget(self.norm_label) self.norm_opts = QComboBox() self.norm_opts.addItems(["None", "By bin width"]) self.norm_opts.setToolTip("Normalization options") self.colorbar_layout.addWidget(self.norm_opts) # MPL figure + colorbar self.fig = Figure() self.ax = None self.axx, self.axy = None, None self.image = None self._grid_on = False self.fig.set_facecolor(self.palette().window().color().getRgbF()) self.canvas = SliceViewerCanvas(self.fig) self.canvas.mpl_connect('motion_notify_event', self.mouse_move) self.canvas.mpl_connect('axes_leave_event', self.mouse_outside_image) self.canvas.mpl_connect('button_press_event', self.mouse_click) self.canvas.mpl_connect('button_release_event', self.mouse_release) self.colorbar_label = QLabel("Colormap") self.colorbar_layout.addWidget(self.colorbar_label) self.colorbar = ColorbarWidget(self) self.colorbar_layout.addWidget(self.colorbar) self.colorbar.colorbarChanged.connect(self.update_data_clim) self.colorbar.colorbarChanged.connect(self.update_line_plot_limits) # make width larger to fit image readout table if self.ws_type == 'MDE': self.colorbar.setMaximumWidth(155) # MPL toolbar self.toolbar_layout = QHBoxLayout() self.mpl_toolbar = SliceViewerNavigationToolbar( self.canvas, self, False) self.mpl_toolbar.gridClicked.connect(self.toggle_grid) self.mpl_toolbar.linePlotsClicked.connect(self.on_line_plots_toggle) self.mpl_toolbar.homeClicked.connect(self.on_home_clicked) self.mpl_toolbar.plotOptionsChanged.connect( self.colorbar.mappable_changed) self.mpl_toolbar.nonOrthogonalClicked.connect( self.on_non_orthogonal_axes_toggle) self.mpl_toolbar.zoomPanFinished.connect(self.on_data_limits_changed) self.toolbar_layout.addWidget(self.mpl_toolbar) # layout layout = QGridLayout(self) layout.setSpacing(1) layout.addLayout(self.dimensions_layout, 0, 0, 1, 2) layout.addLayout(self.toolbar_layout, 1, 0, 1, 2) layout.addWidget(self.canvas, 2, 0, 1, 1) layout.addLayout(self.colorbar_layout, 1, 1, 2, 1) layout.setRowStretch(2, 1) @property def grid_on(self): return self._grid_on @property def nonorthogonal_mode(self): return self.nonortho_tr is not None def create_axes_orthogonal(self, redraw_on_zoom=False): """Create a standard set of orthogonal axes :param redraw_on_zoom: If True then when scroll zooming the canvas is redrawn immediately """ self.clear_figure() self.nonortho_tr = None self.ax = self.fig.add_subplot(111, projection='mantid') self.enable_zoom_on_mouse_scroll(redraw_on_zoom) if self.grid_on: self.ax.grid(self.grid_on) if self.line_plots: self.add_line_plots() self.plot_MDH = self.plot_MDH_orthogonal self.canvas.draw_idle() def create_axes_nonorthogonal(self, transform): self.clear_figure() self.set_nonorthogonal_transform(transform) self.ax = CurveLinearSubPlot(self.fig, 1, 1, 1, grid_helper=GridHelperCurveLinear( (self.nonortho_tr, transform.inv_tr))) # don't redraw on zoom as the data is rebinned and has to be redrawn again anyway self.enable_zoom_on_mouse_scroll(redraw=False) self.set_grid_on() self.fig.add_subplot(self.ax) self.plot_MDH = self.plot_MDH_nonorthogonal self.canvas.draw_idle() def enable_zoom_on_mouse_scroll(self, redraw): """Enable zoom on scroll the mouse wheel for the created axes :param redraw: Pass through to redraw option in enable_zoom_on_scroll """ self.canvas.enable_zoom_on_scroll(self.ax, redraw=redraw, toolbar=self.mpl_toolbar, callback=self.on_data_limits_changed) def add_line_plots(self): """Assuming line plots are currently disabled, enable them on the current figure The image axes must have been created first. """ if self.line_plots: return self.line_plots = True image_axes = self.ax if image_axes is None: return # Create a new GridSpec and reposition the existing image Axes gs = gridspec.GridSpec(2, 2, width_ratios=[1, 4], height_ratios=[4, 1], wspace=0.0, hspace=0.0) image_axes.set_position(gs[1].get_position(self.fig)) set_artist_property(image_axes.get_xticklabels(), visible=False) set_artist_property(image_axes.get_yticklabels(), visible=False) self.axx = self.fig.add_subplot(gs[3], sharex=image_axes) self.axx.yaxis.tick_right() self.axy = self.fig.add_subplot(gs[0], sharey=image_axes) self.axy.xaxis.tick_top() self.update_line_plot_labels() self.mpl_toolbar.update() # sync list of axes in navstack self.canvas.draw_idle() def remove_line_plots(self): """Assuming line plots are currently enabled, remove them from the current figure """ if not self.line_plots: return self.line_plots = False image_axes = self.ax if image_axes is None: return self.delete_line_plot_lines() all_axes = self.fig.axes # The order is defined by the order of the add_subplot calls so we always want to remove # the last two Axes. Do it backwards to cope with the container size change all_axes[2].remove() all_axes[1].remove() gs = gridspec.GridSpec(1, 1) image_axes.set_position(gs[0].get_position(self.fig)) image_axes.xaxis.tick_bottom() image_axes.yaxis.tick_left() self.axx, self.axy = None, None self.mpl_toolbar.update() # sync list of axes in navstack self.canvas.draw_idle() def plot_MDH_orthogonal(self, ws, **kwargs): """ clears the plot and creates a new one using a MDHistoWorkspace """ self.clear_image() self.image = self.ax.imshow(ws, origin='lower', aspect='auto', transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), **kwargs) # ensure the axes data limits are updated to match the # image. For example if the axes were zoomed and the # swap dimensions was clicked we need to restore the # appropriate extents to see the image in the correct place extent = self.image.get_extent() self.ax.set_xlim(extent[0], extent[1]) self.ax.set_ylim(extent[2], extent[3]) self.draw_plot() def plot_MDH_nonorthogonal(self, ws, **kwargs): self.clear_image() self.image = pcolormesh_nonorthogonal( self.ax, ws, self.nonortho_tr, transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), **kwargs) # swapping dimensions in nonorthogonal mode currently resets back to the # full data limits as the whole axes has been recreated so we don't have # access to the original limits # pcolormesh clears any grid that was previously visible if self.grid_on: self.ax.grid(self.grid_on) self.draw_plot() def plot_matrix(self, ws, **kwargs): """ clears the plot and creates a new one using a MatrixWorkspace keeping the axes limits that have already been set """ # ensure view is correct if zoomed in while swapping dimensions # compute required extent and just have resampling imshow deal with it old_extent = None if self.image is not None: old_extent = self.image.get_extent() if self.image.transpose != self.dimensions.transpose: e1, e2, e3, e4 = old_extent old_extent = e3, e4, e1, e2 self.clear_image() self.image = imshow_sampling(self.ax, ws, origin='lower', aspect='auto', interpolation='none', transpose=self.dimensions.transpose, norm=self.colorbar.get_norm(), extent=old_extent, **kwargs) self.draw_plot() def clear_image(self): """Removes any image from the axes""" if self.image is not None: if self.line_plots: self.delete_line_plot_lines() self.image_info_widget.updateTable(DBLMAX, DBLMAX, DBLMAX) self.image.remove() self.image = None def clear_figure(self): """Removes everything from the figure""" if self.line_plots: self.delete_line_plot_lines() self.axx, self.axy = None, None self.image = None self.canvas.disable_zoom_on_scroll() self.fig.clf() self.ax = None def draw_plot(self): self.ax.set_title('') self.colorbar.set_mappable(self.image) self.colorbar.update_clim() self.mpl_toolbar.update() # clear nav stack self.delete_line_plot_lines() self.update_line_plot_labels() self.canvas.draw_idle() def select_zoom(self): """Select the zoom control on the toolbar""" self.mpl_toolbar.zoom() def update_plot_data(self, data): """ This just updates the plot data without creating a new plot. The extents can change if the data has been rebinned """ if self.nonortho_tr: self.image.set_array(data.T.ravel()) else: self.image.set_data(data.T) self.colorbar.update_clim() def on_home_clicked(self): """Reset the view to encompass all of the data""" self.presenter.show_all_data_requested() def on_line_plots_toggle(self, state): self.presenter.line_plots(state) def on_non_orthogonal_axes_toggle(self, state): """ Switch state of the non-orthognal axes on/off """ self.presenter.nonorthogonal_axes(state) def on_data_limits_changed(self): """ React to when the data limits have changed """ self.presenter.data_limits_changed() def enable_lineplots_button(self): """ Enables line plots functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.LINEPLOTS, True) def disable_lineplots_button(self): """ Disabled line plots functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.LINEPLOTS, False) def enable_peaks_button(self): """ Enables line plots functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.OVERLAYPEAKS, True) def disable_peaks_button(self): """ Disables line plots functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.OVERLAYPEAKS, False) def enable_nonorthogonal_axes_button(self): """ Enables access to non-orthogonal axes functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.NONORTHOGONAL_AXES, True) def disable_nonorthogonal_axes_button(self): """ Disables non-orthorognal axes functionality """ self.mpl_toolbar.set_action_enabled(ToolItemText.NONORTHOGONAL_AXES, state=False) def delete_line_plot_lines(self): try: # clear old plots try: self.xfig.remove() self.yfig.remove() except ValueError: pass del self.xfig del self.yfig except AttributeError: pass def get_axes_limits(self): """ Return the limits of the image axes or None if no image yet exists """ if self.image is None: return None else: return self.ax.get_xlim(), self.ax.get_ylim() def set_axes_limits(self, xlim, ylim): """ Set the view limits on the image axes to the given extents :param xlim: 2-tuple of (xmin, xmax) :param ylim: 2-tuple of (ymin, ymax) """ self.ax.set_xlim(xlim) self.ax.set_ylim(ylim) def set_grid_on(self): """ If not visible sets the grid visibility """ if not self._grid_on: self._grid_on = True self.mpl_toolbar.set_action_checked(ToolItemText.GRID, state=self._grid_on) def set_nonorthogonal_transform(self, transform): """ Set the transform for nonorthogonal axes mode :param transform: An object with a tr method to transform from nonorthognal coordinates to display coordinates """ self.nonortho_tr = transform.tr def toggle_grid(self, state): """ Toggle the visibility of the grid on the axes """ self._grid_on = state self.ax.grid(self._grid_on) self.canvas.draw_idle() def mouse_move(self, event): if event.inaxes == self.ax: signal = self.update_image_data(event.xdata, event.ydata, self.line_plots) if self.track_cursor.checkState() == Qt.Checked: self.update_image_table_widget(event.xdata, event.ydata, signal) def mouse_outside_image(self, _): """ Indicates that the mouse have moved outside of an axes. We clear the line plots so that it is not confusing what they mean. """ if self.line_plots: self.delete_line_plot_lines() self.canvas.draw_idle() def mouse_click(self, event): if self.track_cursor.checkState() == Qt.Unchecked \ and event.inaxes == self.ax and event.button == 1: signal = self.update_image_data(event.xdata, event.ydata) self.update_image_table_widget(event.xdata, event.ydata, signal) def mouse_release(self, event): if event.button == 3 and event.inaxes == self.ax: self.on_home_clicked() def update_image_table_widget(self, xdata, ydata, signal): if signal is not None: if self.dimensions.transpose and self.ws_type == "MATRIX": self.image_info_widget.updateTable(ydata, xdata, signal) else: self.image_info_widget.updateTable(xdata, ydata, signal) def plot_x_line(self, x, y): try: self.xfig.set_data(x, y) except (AttributeError, IndexError): self.axx.clear() self.xfig = self.axx.plot(x, y, scalex=False)[0] self.update_line_plot_labels() self.update_line_plot_limits() self.canvas.draw_idle() def plot_y_line(self, x, y): try: self.yfig.set_data(y, x) except (AttributeError, IndexError): self.axy.clear() self.yfig = self.axy.plot(y, x, scaley=False)[0] self.update_line_plot_labels() self.update_line_plot_limits() self.canvas.draw_idle() def update_data_clim(self): self.image.set_clim(self.colorbar.colorbar.mappable.get_clim()) self.canvas.draw_idle() def update_line_plot_limits(self): try: # set line plot intensity axes to match colorbar limits self.axx.set_ylim(self.colorbar.cmin_value, self.colorbar.cmax_value) self.axy.set_xlim(self.colorbar.cmin_value, self.colorbar.cmax_value) except AttributeError: pass def update_line_plot_labels(self): try: # ensure plot labels are in sync with main axes self.axx.set_xlabel(self.ax.get_xlabel()) self.axy.set_ylabel(self.ax.get_ylabel()) except AttributeError: pass def update_image_data(self, x, y, update_line_plot=False): xmin, xmax, ymin, ymax = self.image.get_extent() arr = self.image.get_array() data_extent = Bbox([[ymin, xmin], [ymax, xmax]]) array_extent = Bbox([[0, 0], arr.shape[:2]]) trans = BboxTransform(boxin=data_extent, boxout=array_extent) point = trans.transform_point([y, x]) if any(np.isnan(point)): return i, j = point.astype(int) if update_line_plot: if 0 <= i < arr.shape[0]: self.plot_x_line(np.linspace(xmin, xmax, arr.shape[1]), arr[i, :]) if 0 <= j < arr.shape[1]: self.plot_y_line(np.linspace(ymin, ymax, arr.shape[0]), arr[:, j]) # Clip the coordinates at array bounds if not (0 <= i < arr.shape[0]) or not (0 <= j < arr.shape[1]): return None else: return arr[i, j] def set_normalization(self, ws, **kwargs): normalize_by_bin_width, _ = get_normalize_by_bin_width( ws, self.ax, **kwargs) is_normalized = normalize_by_bin_width or ws.isDistribution() if is_normalized: self.presenter.normalization = mantid.api.MDNormalization.VolumeNormalization self.norm_opts.setCurrentIndex(1) else: self.presenter.normalization = mantid.api.MDNormalization.NoNormalization self.norm_opts.setCurrentIndex(0)