diff options
Diffstat (limited to 'silx/gui/plot/backends/BackendMatplotlib.py')
-rwxr-xr-x | silx/gui/plot/backends/BackendMatplotlib.py | 252 |
1 files changed, 181 insertions, 71 deletions
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py index 2336494..036e630 100755 --- a/silx/gui/plot/backends/BackendMatplotlib.py +++ b/silx/gui/plot/backends/BackendMatplotlib.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2019 European Synchrotron Radiation Facility +# Copyright (c) 2004-2020 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -52,6 +52,7 @@ from matplotlib.patches import Rectangle, Polygon from matplotlib.image import AxesImage from matplotlib.backend_bases import MouseEvent from matplotlib.lines import Line2D +from matplotlib.text import Text from matplotlib.collections import PathCollection, LineCollection from matplotlib.ticker import Formatter, ScalarFormatter, Locator from matplotlib.tri import Triangulation @@ -252,6 +253,60 @@ class _PickableContainer(Container): return False, {} +class _TextWithOffset(Text): + """Text object which can be displayed at a specific position + of the plot, but with a pixel offset""" + + def __init__(self, *args, **kwargs): + Text.__init__(self, *args, **kwargs) + self.pixel_offset = (0, 0) + self.__cache = None + + def draw(self, renderer): + self.__cache = None + return Text.draw(self, renderer) + + def __get_xy(self): + if self.__cache is not None: + return self.__cache + + align = self.get_horizontalalignment() + if align == "left": + xoffset = self.pixel_offset[0] + elif align == "right": + xoffset = -self.pixel_offset[0] + else: + xoffset = 0 + + align = self.get_verticalalignment() + if align == "top": + yoffset = -self.pixel_offset[1] + elif align == "bottom": + yoffset = self.pixel_offset[1] + else: + yoffset = 0 + + trans = self.get_transform() + invtrans = self.get_transform().inverted() + + x = super(_TextWithOffset, self).convert_xunits(self._x) + y = super(_TextWithOffset, self).convert_xunits(self._y) + pos = x, y + proj = trans.transform_point(pos) + proj = proj + numpy.array((xoffset, yoffset)) + pos = invtrans.transform_point(proj) + self.__cache = pos + return pos + + def convert_xunits(self, x): + """Return the pixel position of the annotated point.""" + return self.__get_xy()[0] + + def convert_yunits(self, y): + """Return the pixel position of the annotated point.""" + return self.__get_xy()[1] + + class _MarkerContainer(_PickableContainer): """Marker artists container supporting draw/remove and text position update @@ -263,9 +318,10 @@ class _MarkerContainer(_PickableContainer): :param y: Y coordinate of the marker (None for vertical lines) """ - def __init__(self, artists, x, y, yAxis): + def __init__(self, artists, symbol, x, y, yAxis): self.line = artists[0] self.text = artists[1] if len(artists) > 1 else None + self.symbol = symbol self.x = x self.y = y self.yAxis = yAxis @@ -278,27 +334,39 @@ class _MarkerContainer(_PickableContainer): if self.text is not None: self.text.draw(*args, **kwargs) - def updateMarkerText(self, xmin, xmax, ymin, ymax): + def updateMarkerText(self, xmin, xmax, ymin, ymax, yinverted): """Update marker text position and visibility according to plot limits :param xmin: X axis lower limit :param xmax: X axis upper limit :param ymin: Y axis lower limit - :param ymax: Y axis upprt limit + :param ymax: Y axis upper limit + :param yinverted: True if the y axis is inverted """ if self.text is not None: visible = ((self.x is None or xmin <= self.x <= xmax) and (self.y is None or ymin <= self.y <= ymax)) self.text.set_visible(visible) - if self.x is not None and self.y is None: # vertical line - delta = abs(ymax - ymin) - if ymin > ymax: - ymax = ymin - ymax -= 0.005 * delta - self.text.set_y(ymax) - - if self.x is None and self.y is not None: # Horizontal line + if self.x is not None and self.y is not None: + if self.symbol is None: + valign = 'baseline' + else: + if yinverted: + valign = 'bottom' + else: + valign = 'top' + self.text.set_verticalalignment(valign) + + elif self.y is None: # vertical line + # Always display it on top + center = (ymax + ymin) * 0.5 + pos = (ymax - ymin) * 0.5 * 0.99 + if yinverted: + pos = -pos + self.text.set_y(center + pos) + + elif self.x is None: # Horizontal line delta = abs(xmax - xmin) if xmin > xmax: xmax = xmin @@ -354,9 +422,35 @@ class _DoubleColoredLinePatch(matplotlib.patches.Patch): class Image(AxesImage): - """An AxesImage with a fast path for uint8 RGBA images""" + """An AxesImage with a fast path for uint8 RGBA images. + + :param List[float] silx_origin: (ox, oy) Offset of the image. + :param List[float] silx_scale: (sx, sy) Scale of the image. + """ + + def __init__(self, *args, + silx_origin=(0., 0.), + silx_scale=(1., 1.), + **kwargs): + super().__init__(*args, **kwargs) + self.__silx_origin = silx_origin + self.__silx_scale = silx_scale + + def contains(self, mouseevent): + """Overridden to fill 'ind' with row and column""" + inside, info = super().contains(mouseevent) + if inside: + x, y = mouseevent.xdata, mouseevent.ydata + ox, oy = self.__silx_origin + sx, sy = self.__silx_scale + height, width = self.get_size() + column = numpy.clip(int((x - ox) / sx), 0, width - 1) + row = numpy.clip(int((y - oy) / sy), 0, height - 1) + info['ind'] = (row,), (column,) + return inside, info def set_data(self, A): + """Overridden to add a fast path for RGBA unit8 images""" A = numpy.array(A, copy=False) if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8: super(Image, self).set_data(A) @@ -402,10 +496,15 @@ class BackendMatplotlib(BackendBase.BackendBase): # disable the use of offsets try: - self.ax.get_yaxis().get_major_formatter().set_useOffset(False) - self.ax.get_xaxis().get_major_formatter().set_useOffset(False) - self.ax2.get_yaxis().get_major_formatter().set_useOffset(False) - self.ax2.get_xaxis().get_major_formatter().set_useOffset(False) + axes = [ + self.ax.get_yaxis().get_major_formatter(), + self.ax.get_xaxis().get_major_formatter(), + self.ax2.get_yaxis().get_major_formatter(), + self.ax2.get_xaxis().get_major_formatter(), + ] + for axis in axes: + axis.set_useOffset(False) + axis.set_scientific(False) except: _logger.warning('Cannot disabled axes offsets in %s ' % matplotlib.__version__) @@ -485,10 +584,10 @@ class BackendMatplotlib(BackendBase.BackendBase): def addCurve(self, x, y, color, symbol, linewidth, linestyle, yaxis, - xerror, yerror, z, + xerror, yerror, fill, alpha, symbolsize, baseline): for parameter in (x, y, color, symbol, linewidth, linestyle, - yaxis, z, fill, alpha, symbolsize): + yaxis, fill, alpha, symbolsize): assert parameter is not None assert yaxis in ('left', 'right') @@ -584,12 +683,12 @@ class BackendMatplotlib(BackendBase.BackendBase): return _PickableContainer(artists) - def addImage(self, data, origin, scale, z, colormap, alpha): + def addImage(self, data, origin, scale, colormap, alpha): # Non-uniform image # http://wiki.scipy.org/Cookbook/Histograms # Non-linear axes # http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib - for parameter in (data, origin, scale, z): + for parameter in (data, origin, scale): assert parameter is not None origin = float(origin[0]), float(origin[1]) @@ -600,7 +699,9 @@ class BackendMatplotlib(BackendBase.BackendBase): image = Image(self.ax, interpolation='nearest', picker=True, - origin='lower') + origin='lower', + silx_origin=origin, + silx_scale=scale) if alpha < 1: image.set_alpha(alpha) @@ -627,13 +728,17 @@ class BackendMatplotlib(BackendBase.BackendBase): if data.ndim == 2: # Data image, convert to RGBA image data = colormap.applyToData(data) - + elif data.dtype == numpy.uint16: + # Normalize uint16 data to have a similar behavior as opengl backend + data = data.astype(numpy.float32) + data /= 65535 + image.set_data(data) self.ax.add_artist(image) return image - def addTriangles(self, x, y, triangles, color, z, alpha): - for parameter in (x, y, triangles, color, z, alpha): + def addTriangles(self, x, y, triangles, color, alpha): + for parameter in (x, y, triangles, color, alpha): assert parameter is not None color = numpy.array(color, copy=False) @@ -651,8 +756,8 @@ class BackendMatplotlib(BackendBase.BackendBase): return collection - def addItem(self, x, y, shape, color, fill, overlay, z, - linestyle, linewidth, linebgcolor): + def addShape(self, x, y, shape, color, fill, overlay, + linestyle, linewidth, linebgcolor): if (linebgcolor is not None and shape not in ('rectangle', 'polygon', 'polylines')): _logger.warning( @@ -755,17 +860,11 @@ class BackendMatplotlib(BackendBase.BackendBase): markersize=10.)[-1] if text is not None: - if symbol is None: - valign = 'baseline' - else: - valign = 'top' - text = " " + text - - textArtist = ax.text(x, y, text, - color=color, - horizontalalignment='left', - verticalalignment=valign) - + textArtist = _TextWithOffset(x, y, text, + color=color, + horizontalalignment='left') + if symbol is not None: + textArtist.pixel_offset = 10, 3 elif x is not None: line = ax.axvline(x, color=color, @@ -773,11 +872,11 @@ class BackendMatplotlib(BackendBase.BackendBase): linestyle=linestyle) if text is not None: # Y position will be updated in updateMarkerText call - textArtist = ax.text(x, 1., " " + text, - color=color, - horizontalalignment='left', - verticalalignment='top') - + textArtist = _TextWithOffset(x, 1., text, + color=color, + horizontalalignment='left', + verticalalignment='top') + textArtist.pixel_offset = 5, 3 elif y is not None: line = ax.axhline(y, color=color, @@ -786,11 +885,11 @@ class BackendMatplotlib(BackendBase.BackendBase): if text is not None: # X position will be updated in updateMarkerText call - textArtist = ax.text(1., y, " " + text, - color=color, - horizontalalignment='right', - verticalalignment='top') - + textArtist = _TextWithOffset(1., y, text, + color=color, + horizontalalignment='right', + verticalalignment='top') + textArtist.pixel_offset = 5, 3 else: raise RuntimeError('A marker must at least have one coordinate') @@ -799,11 +898,12 @@ class BackendMatplotlib(BackendBase.BackendBase): # All markers are overlays line.set_animated(True) if textArtist is not None: + ax.add_artist(textArtist) textArtist.set_animated(True) artists = [line] if textArtist is None else [line, textArtist] - container = _MarkerContainer(artists, x, y, yaxis) - container.updateMarkerText(xmin, xmax, ymin, ymax) + container = _MarkerContainer(artists, symbol, x, y, yaxis) + container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted()) return container @@ -811,12 +911,13 @@ class BackendMatplotlib(BackendBase.BackendBase): xmin, xmax = self.ax.get_xbound() ymin1, ymax1 = self.ax.get_ybound() ymin2, ymax2 = self.ax2.get_ybound() + yinverted = self.isYAxisInverted() for item in self._overlayItems(): if isinstance(item, _MarkerContainer): if item.yAxis == 'left': - item.updateMarkerText(xmin, xmax, ymin1, ymax1) + item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted) else: - item.updateMarkerText(xmin, xmax, ymin2, ymax2) + item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted) # Remove methods @@ -1076,6 +1177,7 @@ class BackendMatplotlib(BackendBase.BackendBase): def setYAxisInverted(self, flag): if self.ax.yaxis_inverted() != bool(flag): self.ax.invert_yaxis() + self._updateMarkers() def isYAxisInverted(self): return self.ax.yaxis_inverted() @@ -1143,15 +1245,15 @@ class BackendMatplotlib(BackendBase.BackendBase): BackendBase.BackendBase.setAxesDisplayed(self, displayed) if displayed: # show axes and viewbox rect - self.ax.set_axis_on() - self.ax2.set_axis_on() + self.ax.set_frame_on(True) + self.ax2.set_frame_on(True) # set the default margins self.ax.set_position([.15, .15, .75, .75]) self.ax2.set_position([.15, .15, .75, .75]) else: # hide axes and viewbox rect - self.ax.set_axis_off() - self.ax2.set_axis_off() + self.ax.set_frame_on(False) + self.ax2.set_frame_on(False) # remove external margins self.ax.set_position([0, 0, 1, 1]) self.ax2.set_position([0, 0, 1, 1]) @@ -1168,7 +1270,7 @@ class BackendMatplotlib(BackendBase.BackendBase): else: dataBackgroundColor = backgroundColor - if self.ax.axison: + if self.ax.get_frame_on(): self.fig.patch.set_facecolor(backgroundColor) if self._matplotlibVersion < _parse_version('2'): self.ax.set_axis_bgcolor(dataBackgroundColor) @@ -1187,7 +1289,7 @@ class BackendMatplotlib(BackendBase.BackendBase): gridColor = foregroundColor for axes in (self.ax, self.ax2): - if axes.axison: + if axes.get_frame_on(): axes.spines['bottom'].set_color(foregroundColor) axes.spines['top'].set_color(foregroundColor) axes.spines['right'].set_color(foregroundColor) @@ -1244,11 +1346,6 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): self.mpl_connect('motion_notify_event', self._onMouseMove) self.mpl_connect('scroll_event', self._onMouseWheel) - def contextMenuEvent(self, event): - """Override QWidget.contextMenuEvent to implement the context menu""" - # Makes sure it is overridden (issue with PySide) - BackendBase.BackendBase.contextMenuEvent(self, event) - def postRedisplay(self): self._sigPostRedisplay.emit() @@ -1265,17 +1362,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): def _onMouseMove(self, event): if self._graphCursor: + position = self._plot.pixelToData( + event.x, + self._mplQtYAxisCoordConversion(event.y), + axis='left', + check=True) lineh, linev = self._graphCursor - if event.inaxes not in (self.ax, self.ax2) and lineh.get_visible(): - lineh.set_visible(False) - linev.set_visible(False) - self._plot._setDirtyPlot(overlayOnly=True) - else: + if position is not None: linev.set_visible(True) - linev.set_xdata((event.xdata, event.xdata)) + linev.set_xdata((position[0], position[0])) lineh.set_visible(True) - lineh.set_ydata((event.ydata, event.ydata)) + lineh.set_ydata((position[1], position[1])) self._plot._setDirtyPlot(overlayOnly=True) + elif lineh.get_visible(): + lineh.set_visible(False) + linev.set_visible(False) + self._plot._setDirtyPlot(overlayOnly=True) # onMouseMove must trigger replot if dirty flag is raised self._plot.onMouseMove( @@ -1294,14 +1396,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): def leaveEvent(self, event): """QWidget event handler""" - self._plot.onMouseLeaveWidget() + try: + plot = self._plot + except RuntimeError: + pass + else: + plot.onMouseLeaveWidget() # picking def pickItem(self, x, y, item): mouseEvent = MouseEvent( 'button_press_event', self, x, self._mplQtYAxisCoordConversion(y)) + # Override axes and data position with the axes mouseEvent.inaxes = item.axes + mouseEvent.xdata, mouseEvent.ydata = self.pixelToData( + x, y, axis='left' if item.axes is self.ax else 'right') picked, info = item.contains(mouseEvent) if not picked: |