class PlotCanvas(QtCore.QObject): """ Class handling the plotting area in the application. """ # Signals: # Request for new bitmap to display. The parameter # is a list with [xmin, xmax, ymin, ymax, zoom(optional)] update_screen_request = QtCore.pyqtSignal(list) def __init__(self, container, app): """ The constructor configures the Matplotlib figure that will contain all plots, creates the base axes and connects events to the plotting area. :param container: The parent container in which to draw plots. :rtype: PlotCanvas """ super(PlotCanvas, self).__init__() self.app = app # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels # Parent container self.container = container # Plots go onto a single matplotlib.figure self.figure = Figure(dpi=50) # TODO: dpi needed? self.figure.patch.set_visible(False) # These axes show the ticks and grid. No plotting done here. # New axes must have a label, otherwise mpl returns an existing one. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) self.axes.set_aspect(1) self.axes.grid(True) self.axes.axhline(color='Black') self.axes.axvline(color='Black') # The canvas is the top level container (FigureCanvasQTAgg) self.canvas = FigureCanvas(self.figure) # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # self.canvas.setFocus() #self.canvas.set_hexpand(1) #self.canvas.set_vexpand(1) #self.canvas.set_can_focus(True) # For key press # Attach to parent #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? self.container.addWidget(self.canvas) # Qt # Copy a bitmap of the canvas for quick animation. # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) ### Bitmap Cache self.cache = CanvasCache(self, self.app) self.cache_thread = QtCore.QThread() self.cache.moveToThread(self.cache_thread) #super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run) # self.connect() self.cache_thread.start() self.cache.new_screen.connect(self.on_new_screen) # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) self.canvas.mpl_connect('button_release_event', self.on_mouse_release) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) #self.canvas.connect('configure-event', self.auto_adjust_axes) self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) #self.canvas.connect("scroll-event", self.on_scroll) self.canvas.mpl_connect('scroll_event', self.on_scroll) self.canvas.mpl_connect('key_press_event', self.on_key_down) self.canvas.mpl_connect('key_release_event', self.on_key_up) self.canvas.mpl_connect('draw_event', self.on_draw) self.mouse = [0, 0] self.key = None self.pan_axes = [] self.panning = False def on_new_screen(self): log.debug("Cache updated the screen!") def on_key_down(self, event): """ :param event: :return: """ FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key)) self.key = event.key def on_key_up(self, event): """ :param event: :return: """ self.key = None def mpl_connect(self, event_name, callback): """ Attach an event handler to the canvas through the Matplotlib interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: func :return: Connection id :rtype: int """ return self.canvas.mpl_connect(event_name, callback) def mpl_disconnect(self, cid): """ Disconnect callback with the give id. :param cid: Callback id. :return: None """ self.canvas.mpl_disconnect(cid) def connect(self, event_name, callback): """ Attach an event handler to the canvas through the native Qt interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: function :return: Nothing """ self.canvas.connect(event_name, callback) def clear(self): """ Clears axes and figure. :return: None """ # Clear self.axes.cla() try: self.figure.clf() except KeyError: FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") # Re-build self.figure.add_axes(self.axes) self.axes.set_aspect(1) self.axes.grid(True) # Re-draw self.canvas.draw_idle() def adjust_axes(self, xmin, ymin, xmax, ymax): """ Adjusts all axes while maintaining the use of the whole canvas and an aspect ratio to 1:1 between x and y axes. The parameters are an original request that will be modified to fit these restrictions. :param xmin: Requested minimum value for the X axis. :type xmin: float :param ymin: Requested minimum value for the Y axis. :type ymin: float :param xmax: Requested maximum value for the X axis. :type xmax: float :param ymax: Requested maximum value for the Y axis. :type ymax: float :return: None """ # FlatCAMApp.App.log.debug("PC.adjust_axes()") width = xmax - xmin height = ymax - ymin try: r = width / height except ZeroDivisionError: FlatCAMApp.App.log.error("Height is %f" % height) return canvas_w, canvas_h = self.canvas.get_width_height() canvas_r = float(canvas_w) / canvas_h x_ratio = float(self.x_margin) / canvas_w y_ratio = float(self.y_margin) / canvas_h if r > canvas_r: ycenter = (ymin + ymax) / 2.0 newheight = height * r / canvas_r ymin = ycenter - newheight / 2.0 ymax = ycenter + newheight / 2.0 else: xcenter = (xmax + xmin) / 2.0 newwidth = width * canvas_r / r xmin = xcenter - newwidth / 2.0 xmax = xcenter + newwidth / 2.0 # Adjust axes for ax in self.figure.get_axes(): if ax._label != 'base': ax.set_frame_on(False) # No frame ax.set_xticks([]) # No tick ax.set_yticks([]) # No ticks ax.patch.set_visible(False) # No background ax.set_aspect(1) ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) # Sync re-draw to proper paint on form resize self.canvas.draw() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def auto_adjust_axes(self, *args): """ Calls ``adjust_axes()`` using the extents of the base axes. :rtype : None :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() self.adjust_axes(xmin, ymin, xmax, ymax) def zoom(self, factor, center=None): """ Zooms the plot by factor around a given center point. Takes care of re-drawing. :param factor: Number by which to scale the plot. :type factor: float :param center: Coordinates [x, y] of the point around which to scale the plot. :type center: list :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin if center is None or center == [None, None]: center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] # For keeping the point at the pointer location relx = (xmax - center[0]) / width rely = (ymax - center[1]) / height new_width = width / factor new_height = height / factor xmin = center[0] - new_width * (1 - relx) xmax = center[0] + new_width * relx ymin = center[1] - new_height * (1 - rely) ymax = center[1] + new_height * rely # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) # Async re-draw self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def pan(self, x, y): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin + x * width, xmax + x * width)) ax.set_ylim((ymin + y * height, ymax + y * height)) # Re-draw self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def new_axes(self, name): """ Creates and returns an Axes object attached to this object's Figure. :param name: Unique label for the axes. :return: Axes attached to the figure. :rtype: Axes """ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) def on_scroll(self, event): """ Scroll event handler. :param event: Event object containing the event information. :return: None """ # So it can receive key presses # self.canvas.grab_focus() self.canvas.setFocus() # Event info # z, direction = event.get_scroll_direction() if self.key is None: if event.button == 'up': self.zoom(1.5, self.mouse) else: self.zoom(1 / 1.5, self.mouse) return if self.key == 'shift': if event.button == 'up': self.pan(0.3, 0) else: self.pan(-0.3, 0) return if self.key == 'control': if event.button == 'up': self.pan(0, 0.3) else: self.pan(0, -0.3) return def on_mouse_press(self, event): # Check for middle mouse button press if event.button == self.app.mouse_pan_button: # Prepare axes for pan (using 'matplotlib' pan function) self.pan_axes = [] for a in self.figure.get_axes(): if (event.x is not None and event.y is not None and a.in_axes(event) and a.get_navigate() and a.can_pan()): a.start_pan(event.x, event.y, 1) self.pan_axes.append(a) # Set pan view flag if len(self.pan_axes) > 0: self.panning = True; def on_mouse_release(self, event): # Check for middle mouse button release to complete pan procedure if event.button == self.app.mouse_pan_button: for a in self.pan_axes: a.end_pan() # Clear pan flag self.panning = False def on_mouse_move(self, event): """ Mouse movement event hadler. Stores the coordinates. Updates view on pan. :param event: Contains information about the event. :return: None """ self.mouse = [event.xdata, event.ydata] # Update pan view on mouse move if self.panning is True: for a in self.pan_axes: a.drag_pan(1, event.key, event.x, event.y) # Async re-draw (redraws only on thread idle state, uses timer on backend) self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def on_draw(self, renderer): # Store background on canvas redraw self.background = self.canvas.copy_from_bbox(self.axes.bbox) def get_axes_pixelsize(self): """ Axes size in pixels. :return: Pixel width and height :rtype: tuple """ bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) width, height = bbox.width, bbox.height width *= self.figure.dpi height *= self.figure.dpi return width, height def get_density(self): """ Returns unit length per pixel on horizontal and vertical axes. :return: X and Y density :rtype: tuple """ xpx, ypx = self.get_axes_pixelsize() xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin return width / xpx, height / ypx
def update_plot(self): self.assign_parameters() ax = self.fig.gca(facecolor=self.background_color) ax.clear() ax.plot(self.plot_data[1, :], self.plot_data[0, :], color=self.linecolor, linestyle=self.linestyle, label=self.label, linewidth=self.lineswidth) # Set Legend if (len(self.label) > 0): legend = ax.legend(prop={ 'size': self.legend_font_size, 'family': self.legend_font.family(), 'style': self.legend_style, 'weight': self.legend_weight }, facecolor=self.legend_facecolor, edgecolor=self.legend_edgecolor, loc=self.legend_pos) for text in legend.get_texts(): text.set_color(self.legend_color) # Set title ax.set_title(self.title, size=self.title_font_size, family=self.title_font.family(), style=self.title_style, weight=self.title_weight, color=self.title_color, loc=self.title_horizontal_ali, y=1 + (self.title_pad / 100)) # Set x- and y-labeling ax.set_xlabel(self.xlbl, labelpad=self.xy_labelpad, size=self.xy_font_size, family=self.xy_font.family(), style=self.xy_style, weight=self.xy_weight, color=self.xy_color) ax.set_ylabel(self.ylbl, labelpad=self.xy_labelpad, size=self.xy_font_size, family=self.xy_font.family(), style=self.xy_style, weight=self.xy_weight, color=self.xy_color) scene = QtGui.QGraphicsScene() self.fig.tight_layout() canvas = FigureCanvas(self.fig) scene.addWidget(canvas) # resizing and fitting of GraphicsView and Scene self.PlotView.setFixedSize(canvas.get_width_height()[0] + 2, canvas.get_width_height()[1] + 2) self.PlotView.setSceneRect(0, 0, canvas.get_width_height()[0], canvas.get_width_height()[1]) self.PlotView.fitInView(0, 0, canvas.get_width_height()[0], canvas.get_width_height()[1]) self.PlotView.setScene(scene) # resizing the Window width = self.PlotEditor.minimumWidth() + canvas.get_width_height()[0] dialog_width = width if (width > 880) else 880 window_size = dialog_width, self.PlotEditor.geometry().height() self.PlotEditor.resize(window_size[0], window_size[1])
class CourseMapDialog(QtGui.QDialog): """Implements a dialog box to display course maps.""" time_slider_changed = QtCore.Signal(float) def __init__(self, parent): QtGui.QDialog.__init__(self, parent) self._ui = ui_course_map_dialog.Ui_Dialog() self._ui.setupUi(self) params = matplotlib.figure.SubplotParams(0, 0, 1, 1, 0, 0) self._figure = matplotlib.figure.Figure(subplotpars=params) self._canvas = FigureCanvas(self._figure) self._canvas.mpl_connect('motion_notify_event', self._handle_mouse) self._canvas.mpl_connect('scroll_event', self._handle_scroll) self._canvas.mpl_connect('button_release_event', self._handle_mouse_release) self._mouse_start = None # Make QT drawing not be super slow. See: # https://github.com/matplotlib/matplotlib/issues/2559/ def draw(): FigureCanvas.draw(self._canvas) self._canvas.repaint() self._canvas.draw = draw self._plot = self._figure.add_subplot(111) self._gdal_source = course_gdal.GdalSource() self._gdal = None layout = QtGui.QVBoxLayout(self._ui.mapFrame) layout.addWidget(self._canvas, 1) self._log_data = dict() # TODO sammy make COLORS a project wide config self._COLORS = 'rgbcmyk' self._next_color = 0 self._bounds = ((0, 0), (0, 0)) self._time_current = 0 self._total_time = 0 self._ui.timeSlider.valueChanged.connect(self._handle_time_slider) def add_log(self, log_name, log): log_data = _LogMapData(log_name, log, self._COLORS[self._next_color]) self._log_data[log_name] = log_data self._next_color = (self._next_color + 1) % len(self._COLORS) if self._gdal is None: self._gdal = self._gdal_source.get_gdal( log_data.utm_data[0][1], log_data.utm_data[0][2]) if self._gdal is not None: self._plot.imshow(self._gdal.image, extent=self._gdal.extent) self._plot.add_line(log_data.line) self._plot.add_line(log_data.marker) self._plot.legend(loc=2) self._update_scale() def update_sync(self): for log_data in self._log_data.itervalues(): log_data.update_line_data() log_data.update_marker(self._time_current) self._update_scale() def _update_scale(self): self._plot.relim() minx = 1e10 miny = 1e10 maxx = -1e10 maxy = -1e10 max_time = 0 for log_data in self._log_data.itervalues(): bounds = log_data.bounds minx = min(minx, bounds[0][0]) miny = min(miny, bounds[0][1]) maxx = max(maxx, bounds[1][0]) maxy = max(maxy, bounds[1][1]) max_time = max(max_time, log_data.utm_data[-1][0]) self._total_time = max_time x_size = maxx - minx y_size = maxy - miny xy_delta = x_size - y_size if xy_delta > 0: miny -= xy_delta / 2 maxy += xy_delta / 2 else: minx += xy_delta / 2 maxx -= xy_delta / 2 self._bounds = ((minx, miny), (maxx, maxy)) self._plot.set_xlim(left=minx, right=maxx) self._plot.set_ylim(bottom=miny, top=maxy) self._canvas.draw() def _handle_mouse(self, event): if not event.inaxes or event.button != 1: return if self._mouse_start is None: self._mouse_start = event return # Handle a pan event. What we want is for the point (data) # where the mouse was originally clicked to stay under the # pointer. (width, height) = self._canvas.get_width_height() px_x = (self._bounds[1][0] - self._bounds[0][0]) / width px_y = (self._bounds[1][1] - self._bounds[0][1]) / height x_change = (self._mouse_start.x - event.x) * px_x y_change = (self._mouse_start.y - event.y) * px_y self._plot.set_xlim(left=self._bounds[0][0] + x_change, right=self._bounds[1][0] + x_change) self._plot.set_ylim(bottom=self._bounds[0][1] + y_change, top=self._bounds[1][1] + y_change) self._canvas.draw() def _update_bounds(self): xlim = self._plot.get_xlim() ylim = self._plot.get_ylim() self._bounds = ((xlim[0], ylim[0]), (xlim[1], ylim[1])) def _handle_mouse_release(self, event): if event.button != 1: return self._mouse_start = None self._update_bounds() def _handle_scroll(self, event): # Determine the relative offset of the clicked position to the # center of the frame so that we can keep the data under the # cursor. (width, height) = self._canvas.get_width_height() x_off = float(width - event.x) / width y_off = float(height - event.y) / height x_scale = (self._bounds[1][0] - self._bounds[0][0]) * (event.step / 10.) y_scale = (self._bounds[1][1] - self._bounds[0][1]) * (event.step / 10.) # Check if we've tried to zoom to far and would invert our axes new_xlim = (self._bounds[0][0] + (x_scale * (1. - x_off)), self._bounds[1][0] - (x_scale * x_off)) new_ylim = (self._bounds[0][1] + (y_scale * (1. - y_off)), self._bounds[1][1] - (y_scale * y_off)) if (new_xlim[1] <= new_xlim[0]) or (new_ylim[1] <= new_ylim[0]): return self._plot.set_xlim(left=new_xlim[0], right=new_xlim[1]) self._plot.set_ylim(bottom=new_ylim[0], top=new_ylim[1]) self._update_bounds() self._canvas.draw() def update_time(self, new_time, update_slider=True): # Bound the time to our useful range. new_time = max(0, min(new_time, self._total_time)) self._time_current = new_time for log_data in self._log_data.itervalues(): log_data.update_marker(new_time) self._canvas.draw() self._ui.elapsedTime.setText(str(new_time)) # if update_slider: # self._ui.timeSlider.setValue(1000 * (new_time / self._total_time)) def _handle_time_slider(self): if self._total_time == 0: return current = self._ui.timeSlider.value() / 1000. self.update_time(current * self._total_time, update_slider=False) self.time_slider_changed.emit(self._time_current)
class DrawingPad_Painter(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent=parent) #prepare data and figure self.traj_pnts = [] self.curr_traj = None self.lines = [] self.curr_line = None self.dpi = 100 self.fig = Figure(figsize=(3.24, 5.0), dpi=self.dpi, facecolor="white") self.canvas = FigureCanvas(self.fig) self.ax_painter = self.fig.add_subplot(111, aspect='equal') self.ax_painter.hold(True) self.ax_painter.set_xlim([-2, 2]) self.ax_painter.set_ylim([-2, 2]) self.ax_painter.set_aspect('equal') self.ax_painter.set_xticks([]) self.ax_painter.set_yticks([]) self.ax_painter.axis('off') self.hbox_layout = QHBoxLayout() self.hbox_layout.addWidget(self.canvas, 5) self.line_width = 12.0 # self.ctrl_pnl_layout = QVBoxLayout() # #a button to clear the figure # self.clean_btn = QPushButton('Clear') # self.ctrl_pnl_layout.addWidget(self.clean_btn) # # self.hbox_layout.addLayout(self.ctrl_pnl_layout, 1) self.setLayout(self.hbox_layout) self.drawing = False self.create_event_handler() return def create_event_handler(self): self.canvas_button_clicked_cid = self.canvas.mpl_connect('button_press_event', self.on_canvas_mouse_clicked) self.canvas_button_released_cid = self.canvas.mpl_connect('button_release_event', self.on_canvas_mouse_released) self.canvas_motion_notify_cid = self.canvas.mpl_connect('motion_notify_event', self.on_canvas_mouse_move) return def on_canvas_mouse_clicked(self, event): # print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( # event.button, event.x, event.y, event.xdata, event.ydata) self.drawing = True # create a new line if we are drawing within the area if event.xdata is not None and event.ydata is not None and self.curr_line is None and self.curr_traj is None: self.curr_line, = self.ax_painter.plot([event.xdata], [event.ydata], '-k', linewidth=self.line_width) self.curr_traj = [np.array([event.xdata, event.ydata])] self.canvas.draw() return def on_canvas_mouse_released(self, event): # print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( # event.button, event.x, event.y, event.xdata, event.ydata) self.drawing = False # store finished line and trajectory # print self.curr_traj self.lines.append(self.curr_line) self.traj_pnts.append(self.curr_traj) self.curr_traj = None self.curr_line = None return def on_clean(self, event): print 'clean the canvas...' #clear everything for line in self.ax_painter.lines: self.ax_painter.lines.remove(line) self.lines = [] if self.curr_line is not None: self.ax_painter.lines.remove(self.curr_line) self.curr_line = None self.canvas.draw() self.traj_pnts = [] self.curr_traj = None self.drawing = False return def on_canvas_mouse_move(self, event): if self.drawing: # print 'In movement: x=',event.x ,', y=', event.y,', xdata=',event.xdata,', ydata=', event.ydata if event.xdata is not None and event.ydata is not None and self.curr_line is not None and self.curr_traj is not None: #append new data and update drawing self.curr_traj.append(np.array([event.xdata, event.ydata])) tmp_curr_data = np.array(self.curr_traj) self.curr_line.set_xdata(tmp_curr_data[:, 0]) self.curr_line.set_ydata(tmp_curr_data[:, 1]) self.canvas.draw() return def plot_trajs_helper(self, trajs): tmp_lines = [] for traj in trajs: tmp_line, = self.ax_painter.plot(traj[:, 0], traj[:, 1], '-.g', linewidth=self.line_width) tmp_lines.append(tmp_line) self.canvas.draw() #add these tmp_lines to lines record self.lines = self.lines + tmp_lines return def get_traj_data(self): return self.traj_pnts def get_image_data(self): """ Get the deposited image """ w,h = self.canvas.get_width_height() buf = np.fromstring ( self.canvas.tostring_argb(), dtype=np.uint8 ) buf.shape = ( w, h, 4 ) # canvas.tostring_argb give pixmap in ARGB mode. Roll the ALPHA channel to have it in RGBA mode buf = np.roll ( buf, 3, axis = 2 ) return buf
class PlotCanvas: """ Class handling the plotting area in the application. """ def __init__(self, container): """ The constructor configures the Matplotlib figure that will contain all plots, creates the base axes and connects events to the plotting area. :param container: The parent container in which to draw plots. :rtype: PlotCanvas """ # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels # Parent container self.container = container # Plots go onto a single matplotlib.figure self.figure = Figure(dpi=50) # TODO: dpi needed? self.figure.patch.set_visible(False) # These axes show the ticks and grid. No plotting done here. # New axes must have a label, otherwise mpl returns an existing one. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) self.axes.set_aspect(1) self.axes.grid(True) # The canvas is the top level container (Gtk.DrawingArea) self.canvas = FigureCanvas(self.figure) # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # self.canvas.setFocus() #self.canvas.set_hexpand(1) #self.canvas.set_vexpand(1) #self.canvas.set_can_focus(True) # For key press # Attach to parent #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? self.container.addWidget(self.canvas) # Qt # Copy a bitmap of the canvas for quick animation. # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) self.canvas.mpl_connect('button_release_event', self.on_mouse_release) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) #self.canvas.connect('configure-event', self.auto_adjust_axes) self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) #self.canvas.connect("scroll-event", self.on_scroll) self.canvas.mpl_connect('scroll_event', self.on_scroll) self.canvas.mpl_connect('key_press_event', self.on_key_down) self.canvas.mpl_connect('key_release_event', self.on_key_up) self.canvas.mpl_connect('draw_event', self.on_draw) self.mouse = [0, 0] self.key = None self.pan_axes = [] self.panning = False def on_key_down(self, event): """ :param event: :return: """ FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key)) self.key = event.key def on_key_up(self, event): """ :param event: :return: """ self.key = None def mpl_connect(self, event_name, callback): """ Attach an event handler to the canvas through the Matplotlib interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: func :return: Connection id :rtype: int """ return self.canvas.mpl_connect(event_name, callback) def mpl_disconnect(self, cid): """ Disconnect callback with the give id. :param cid: Callback id. :return: None """ self.canvas.mpl_disconnect(cid) def connect(self, event_name, callback): """ Attach an event handler to the canvas through the native GTK interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: function :return: Nothing """ self.canvas.connect(event_name, callback) def clear(self): """ Clears axes and figure. :return: None """ # Clear self.axes.cla() try: self.figure.clf() except KeyError: FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") # Re-build self.figure.add_axes(self.axes) self.axes.set_aspect(1) self.axes.grid(True) # Re-draw self.canvas.draw_idle() def adjust_axes(self, xmin, ymin, xmax, ymax): """ Adjusts all axes while maintaining the use of the whole canvas and an aspect ratio to 1:1 between x and y axes. The parameters are an original request that will be modified to fit these restrictions. :param xmin: Requested minimum value for the X axis. :type xmin: float :param ymin: Requested minimum value for the Y axis. :type ymin: float :param xmax: Requested maximum value for the X axis. :type xmax: float :param ymax: Requested maximum value for the Y axis. :type ymax: float :return: None """ # FlatCAMApp.App.log.debug("PC.adjust_axes()") width = xmax - xmin height = ymax - ymin try: r = width / height except ZeroDivisionError: FlatCAMApp.App.log.error("Height is %f" % height) return canvas_w, canvas_h = self.canvas.get_width_height() canvas_r = float(canvas_w) / canvas_h x_ratio = float(self.x_margin) / canvas_w y_ratio = float(self.y_margin) / canvas_h if r > canvas_r: ycenter = (ymin + ymax) / 2.0 newheight = height * r / canvas_r ymin = ycenter - newheight / 2.0 ymax = ycenter + newheight / 2.0 else: xcenter = (xmax + xmin) / 2.0 newwidth = width * canvas_r / r xmin = xcenter - newwidth / 2.0 xmax = xcenter + newwidth / 2.0 # Adjust axes for ax in self.figure.get_axes(): if ax._label != 'base': ax.set_frame_on(False) # No frame ax.set_xticks([]) # No tick ax.set_yticks([]) # No ticks ax.patch.set_visible(False) # No background ax.set_aspect(1) ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) # Sync re-draw to proper paint on form resize self.canvas.draw() def auto_adjust_axes(self, *args): """ Calls ``adjust_axes()`` using the extents of the base axes. :rtype : None :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() self.adjust_axes(xmin, ymin, xmax, ymax) def zoom(self, factor, center=None): """ Zooms the plot by factor around a given center point. Takes care of re-drawing. :param factor: Number by which to scale the plot. :type factor: float :param center: Coordinates [x, y] of the point around which to scale the plot. :type center: list :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin if center is None or center == [None, None]: center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] # For keeping the point at the pointer location relx = (xmax - center[0]) / width rely = (ymax - center[1]) / height new_width = width / factor new_height = height / factor xmin = center[0] - new_width * (1 - relx) xmax = center[0] + new_width * relx ymin = center[1] - new_height * (1 - rely) ymax = center[1] + new_height * rely # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) # Async re-draw self.canvas.draw_idle() def pan(self, x, y): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin + x * width, xmax + x * width)) ax.set_ylim((ymin + y * height, ymax + y * height)) # Re-draw self.canvas.draw_idle() def new_axes(self, name): """ Creates and returns an Axes object attached to this object's Figure. :param name: Unique label for the axes. :return: Axes attached to the figure. :rtype: Axes """ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) def on_scroll(self, event): """ Scroll event handler. :param event: Event object containing the event information. :return: None """ # So it can receive key presses # self.canvas.grab_focus() self.canvas.setFocus() # Event info # z, direction = event.get_scroll_direction() if self.key is None: if event.button == 'up': self.zoom(1.5, self.mouse) else: self.zoom(1 / 1.5, self.mouse) return if self.key == 'shift': if event.button == 'up': self.pan(0.3, 0) else: self.pan(-0.3, 0) return if self.key == 'control': if event.button == 'up': self.pan(0, 0.3) else: self.pan(0, -0.3) return def on_mouse_press(self, event): # Check for middle mouse button press if event.button == 2: # Prepare axes for pan (using 'matplotlib' pan function) self.pan_axes = [] for a in self.figure.get_axes(): if (event.x is not None and event.y is not None and a.in_axes(event) and a.get_navigate() and a.can_pan()): a.start_pan(event.x, event.y, 1) self.pan_axes.append(a) # Set pan view flag if len(self.pan_axes) > 0: self.panning = True; def on_mouse_release(self, event): # Check for middle mouse button release to complete pan procedure if event.button == 2: for a in self.pan_axes: a.end_pan() # Clear pan flag self.panning = False def on_mouse_move(self, event): """ Mouse movement event hadler. Stores the coordinates. Updates view on pan. :param event: Contains information about the event. :return: None """ self.mouse = [event.xdata, event.ydata] # Update pan view on mouse move if self.panning is True: for a in self.pan_axes: a.drag_pan(1, event.key, event.x, event.y) # Async re-draw (redraws only on thread idle state, uses timer on backend) self.canvas.draw_idle() def on_draw(self, renderer): # Store background on canvas redraw self.background = self.canvas.copy_from_bbox(self.axes.bbox)
class MatplotlibWithNavigationWidget(QWidget): """ MatplotlibWidget inherits PyQt4.QtGui.QHBoxLayout Options: option_name (default_value) ------- parent (None): parent widget title (''): figure title xlabel (''): X-axis label ylabel (''): Y-axis label xlim (None): X-axis limits ([min, max]) ylim (None): Y-axis limits ([min, max]) xscale ('linear'): X-axis scale yscale ('linear'): Y-axis scale width (4): width in inches height (3): height in inches dpi (100): resolution in dpi hold (False): if False, figure will be cleared each time plot is called Widget attributes: ----------------- figure: instance of matplotlib.figure.Figure axes: figure axes Example: ------- self.widget = MatplotlibWidget(self, yscale='log', hold=True) from numpy import linspace x = linspace(-10, 10) self.widget.axes.plot(x, x**2) self.wdiget.axes.plot(x, x**3) """ def __init__(self, parent=None, title='', xlabel='', ylabel='', xlim=None, ylim=None, xscale='linear', yscale='linear', width=4, height=3, dpi=100, hold=False): self.figure = Figure(figsize=(width, height), dpi=dpi) self.axes = self.figure.add_subplot(111) self.axes.set_title(title) self.axes.set_xlabel(xlabel) self.axes.set_ylabel(ylabel) if xscale is not None: self.axes.set_xscale(xscale) if yscale is not None: self.axes.set_yscale(yscale) if xlim is not None: self.axes.set_xlim(*xlim) if ylim is not None: self.axes.set_ylim(*ylim) #self.axes.hold(hold) QWidget.__init__(self, parent) self.canvas = FigureCanvas(self.figure) self.mpl_toolbar = NavigationToolbar(self.canvas, self) self.vbox = QVBoxLayout(self) self.vbox.addWidget(self.canvas, 1) self.vbox.addWidget(self.mpl_toolbar) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) def sizeHint(self): w, h = self.canvas.get_width_height() return QSize(w, h) # +6 added by Ghislain def minimumSizeHint(self): return QSize(10, 10)
def update_plot(self): self.assign_parameters() ax = self.fig.gca(facecolor=self.background_color) ax.clear() ax.plot(self.plot_data[1, :], self.plot_data[0, :], color=self.linecolor, linestyle=self.linestyle, label=self.label, linewidth=self.lineswidth) # Set Legend if (len(self.label) > 0): legend = ax.legend(prop={ 'size': self.legend_font_size, 'family': self.legend_font.family(), 'style': self.legend_style, 'weight': self.legend_weight }, facecolor=self.legend_facecolor, edgecolor=self.legend_edgecolor, loc=self.legend_pos) for text in legend.get_texts(): text.set_color(self.legend_color) # Set title ax.set_title(self.title, size=self.title_font_size, family=self.title_font.family(), style=self.title_style, weight=self.title_weight, color=self.title_color, loc=self.title_horizontal_ali, y=1 + (self.title_pad / 100)) # Set x- and y-labeling ax.set_xlabel(self.xlbl, labelpad=self.xy_labelpad, size=self.xy_font_size, family=self.xy_font.family(), style=self.xy_style, weight=self.xy_weight, color=self.xy_color) ax.set_ylabel(self.ylbl, labelpad=self.xy_labelpad, size=self.xy_font_size, family=self.xy_font.family(), style=self.xy_style, weight=self.xy_weight, color=self.xy_color) width_pixels, height_pixels, xfak, yfak = self.compute_widht_height_pixels( ) scene = QtGui.QGraphicsScene() self.fig.tight_layout() canvas = FigureCanvas(self.fig) scene.addWidget(canvas) self.PlotView.setFixedSize(canvas.get_width_height()[0] + 2, canvas.get_width_height()[1] + 2) self.PlotView.setSceneRect(0, 0, canvas.get_width_height()[0], canvas.get_width_height()[1]) self.PlotView.fitInView(0, 0, canvas.get_width_height()[0], canvas.get_width_height()[1]) self.PlotView.setScene(scene)
import sys app = QtGui.QApplication(sys.argv) imageViewer = ImageViewer() imageViewer.show() figure = plt.figure() figure.set_size_inches(11, 8.5) figure.patch.set_facecolor('white') plt.plot([1, 2, 3, 4], [1, 2, 3, 4], '-b') figure_canvas = FigureCanvasQTAgg(figure) figure_canvas.draw() size = figure_canvas.size() width, height = size.width(), size.height() print(width, height) print(figure_canvas.get_width_height()) imgbuffer = figure_canvas.buffer_rgba() image = QtGui.QImage(imgbuffer, width, height, QtGui.QImage.Format_ARGB32) # Reference for the RGB to BGR swap: # http://sourceforge.net/p/matplotlib/mailman/message/5194542/ image = QtGui.QImage.rgbSwapped(image) imageViewer.load_image(image, 0) sys.exit(app.exec_())