diff options
Diffstat (limited to 'silx/gui/plot/backends')
-rw-r--r-- | silx/gui/plot/backends/BackendBase.py | 11 | ||||
-rw-r--r-- | silx/gui/plot/backends/BackendMatplotlib.py | 154 | ||||
-rw-r--r-- | silx/gui/plot/backends/BackendOpenGL.py | 177 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotCurve.py | 2 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/PlotImageFile.py | 2 |
5 files changed, 206 insertions, 140 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py index 12561b2..45bf785 100644 --- a/silx/gui/plot/backends/BackendBase.py +++ b/silx/gui/plot/backends/BackendBase.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# Copyright (c) 2004-2018 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 @@ -189,7 +189,7 @@ class BackendBase(object): def addMarker(self, x, y, legend, text, color, selectable, draggable, - symbol, constraint, overlay): + symbol, constraint): """Add a point, vertical line or horizontal line marker to the plot. :param float x: Horizontal position of the marker in graph coordinates. @@ -221,9 +221,6 @@ class BackendBase(object): :type constraint: None or a callable that takes the coordinates of the current cursor position in the plot as input and that returns the filtered coordinates. - :param bool overlay: True if marker is an overlay (Default: False). - This allows for rendering optimization if this - marker is changed often. :return: Handle used by the backend to univocally access the marker """ return legend @@ -270,11 +267,13 @@ class BackendBase(object): """ pass - def pickItems(self, x, y): + def pickItems(self, x, y, kinds): """Get a list of items at a pixel position. :param float x: The x pixel coord where to pick. :param float y: The y pixel coord where to pick. + :param List[str] kind: List of item kinds to pick. + Supported kinds: 'marker', 'curve', 'image'. :return: All picked items from back to front. One dict per item, with 'kind' key in 'curve', 'marker', 'image'; diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py index b41f20e..f9a1fe5 100644 --- a/silx/gui/plot/backends/BackendMatplotlib.py +++ b/silx/gui/plot/backends/BackendMatplotlib.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# Copyright (c) 2004-2018 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 @@ -58,6 +58,59 @@ from . import BackendBase from .._utils import FLOAT32_MINPOS +class _MarkerContainer(Container): + """Marker artists container supporting draw/remove and text position update + + :param artists: + Iterable with either one Line2D or a Line2D and a Text. + The use of an iterable if enforced by Container being + a subclass of tuple that defines a specific __new__. + :param x: X coordinate of the marker (None for horizontal lines) + :param y: Y coordinate of the marker (None for vertical lines) + """ + + def __init__(self, artists, x, y): + self.line = artists[0] + self.text = artists[1] if len(artists) > 1 else None + self.x = x + self.y = y + + Container.__init__(self, artists) + + def draw(self, *args, **kwargs): + """artist-like draw to broadcast draw to line and text""" + self.line.draw(*args, **kwargs) + if self.text is not None: + self.text.draw(*args, **kwargs) + + def updateMarkerText(self, xmin, xmax, ymin, ymax): + """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 + """ + 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 + delta = abs(xmax - xmin) + if xmin > xmax: + xmax = xmin + xmax -= 0.005 * delta + self.text.set_x(xmax) + + class BackendMatplotlib(BackendBase.BackendBase): """Base class for Matplotlib backend without a FigureCanvas. @@ -356,10 +409,13 @@ class BackendMatplotlib(BackendBase.BackendBase): self.ax.add_patch(item) elif shape in ('polygon', 'polylines'): - xView = xView.reshape(1, -1) - yView = yView.reshape(1, -1) - item = Polygon(numpy.vstack((xView, yView)).T, - closed=(shape == 'polygon'), + points = numpy.array((xView, yView)).T + if shape == 'polygon': + closed = True + else: # shape == 'polylines' + closed = numpy.all(numpy.equal(points[0], points[-1])) + item = Polygon(points, + closed=closed, fill=False, label=legend, color=color) @@ -381,9 +437,14 @@ class BackendMatplotlib(BackendBase.BackendBase): def addMarker(self, x, y, legend, text, color, selectable, draggable, - symbol, constraint, overlay): + symbol, constraint): legend = "__MARKER__" + legend + textArtist = None + + xmin, xmax = self.getGraphXLimits() + ymin, ymax = self.getGraphYLimits(axis='left') + if x is not None and y is not None: line = self.ax.plot(x, y, label=legend, linestyle=" ", @@ -392,49 +453,35 @@ class BackendMatplotlib(BackendBase.BackendBase): markersize=10.)[-1] if text is not None: - xtmp, ytmp = self.ax.transData.transform_point((x, y)) - inv = self.ax.transData.inverted() - xtmp, ytmp = inv.transform_point((xtmp, ytmp)) - if symbol is None: valign = 'baseline' else: valign = 'top' text = " " + text - line._infoText = self.ax.text(x, ytmp, text, - color=color, - horizontalalignment='left', - verticalalignment=valign) + textArtist = self.ax.text(x, y, text, + color=color, + horizontalalignment='left', + verticalalignment=valign) elif x is not None: line = self.ax.axvline(x, label=legend, color=color) if text is not None: - text = " " + text - ymin, ymax = self.getGraphYLimits(axis='left') - delta = abs(ymax - ymin) - if ymin > ymax: - ymax = ymin - ymax -= 0.005 * delta - line._infoText = self.ax.text(x, ymax, text, - color=color, - horizontalalignment='left', - verticalalignment='top') + # Y position will be updated in updateMarkerText call + textArtist = self.ax.text(x, 1., " " + text, + color=color, + horizontalalignment='left', + verticalalignment='top') elif y is not None: line = self.ax.axhline(y, label=legend, color=color) if text is not None: - text = " " + text - xmin, xmax = self.getGraphXLimits() - delta = abs(xmax - xmin) - if xmin > xmax: - xmax = xmin - xmax -= 0.005 * delta - line._infoText = self.ax.text(xmax, y, text, - color=color, - horizontalalignment='right', - verticalalignment='top') + # X position will be updated in updateMarkerText call + textArtist = self.ax.text(1., y, " " + text, + color=color, + horizontalalignment='right', + verticalalignment='top') else: raise RuntimeError('A marker must at least have one coordinate') @@ -442,19 +489,29 @@ class BackendMatplotlib(BackendBase.BackendBase): if selectable or draggable: line.set_picker(5) - if overlay: - line.set_animated(True) - self._overlays.add(line) + # All markers are overlays + line.set_animated(True) + if textArtist is not None: + textArtist.set_animated(True) + + artists = [line] if textArtist is None else [line, textArtist] + container = _MarkerContainer(artists, x, y) + container.updateMarkerText(xmin, xmax, ymin, ymax) + self._overlays.add(container) - return line + return container + + def _updateMarkers(self): + xmin, xmax = self.ax.get_xbound() + ymin, ymax = self.ax.get_ybound() + for item in self._overlays: + if isinstance(item, _MarkerContainer): + item.updateMarkerText(xmin, xmax, ymin, ymax) # Remove methods def remove(self, item): # Warning: It also needs to remove extra stuff if added as for markers - if hasattr(item, "_infoText"): # For markers text - item._infoText.remove() - item._infoText = None self._overlays.discard(item) try: item.remove() @@ -562,6 +619,8 @@ class BackendMatplotlib(BackendBase.BackendBase): else: self.ax.set_ylim(max(ymin, ymax), min(ymin, ymax)) + self._updateMarkers() + def getGraphXLimits(self): if self._dirtyLimits and self.isKeepDataAspectRatio(): self.replot() # makes sure we get the right limits @@ -570,6 +629,7 @@ class BackendMatplotlib(BackendBase.BackendBase): def setGraphXLimits(self, xmin, xmax): self._dirtyLimits = True self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax)) + self._updateMarkers() def getGraphYLimits(self, axis): assert axis in ('left', 'right') @@ -607,6 +667,8 @@ class BackendMatplotlib(BackendBase.BackendBase): else: ax.set_ylim(ymax, ymin) + self._updateMarkers() + # Graph axes def setXAxisLogarithmic(self, flag): @@ -814,7 +876,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): self._picked.append({'kind': 'curve', 'legend': label, 'indices': event.ind}) - def pickItems(self, x, y): + def pickItems(self, x, y, kinds): self._picked = [] # Weird way to do an explicit picking: Simulate a button press event @@ -822,7 +884,8 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): cid = self.mpl_connect('pick_event', self._onPick) self.fig.pick(mouseEvent) self.mpl_disconnect(cid) - picked = self._picked + + picked = [p for p in self._picked if p['kind'] in kinds] self._picked = None return picked @@ -882,6 +945,10 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): xLimits, yLimits, yRightLimits = self._limitsBeforeResize self._limitsBeforeResize = None + if (xLimits != self.ax.get_xbound() or + yLimits != self.ax.get_ybound()): + self._updateMarkers() + if xLimits != self.ax.get_xbound(): self._plot.getXAxis()._emitLimitsChanged() if yLimits != self.ax.get_ybound(): @@ -889,6 +956,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): if yRightLimits != self.ax2.get_ybound(): self._plot.getYAxis(axis='right')._emitLimitsChanged() + self._drawOverlays() def replot(self): diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py index c70b03a..3c18f4f 100644 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ b/silx/gui/plot/backends/BackendOpenGL.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility +# Copyright (c) 2014-2018 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 @@ -892,11 +892,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): for item in self._items.values(): shape2D = item.get('_shape2D') if shape2D is None: + closed = item['shape'] != 'polylines' shape2D = Shape2D(tuple(zip(item['x'], item['y'])), fill=item['fill'], fillColor=item['color'], stroke=True, - strokeColor=item['color']) + strokeColor=item['color'], + strokeClosed=closed) item['_shape2D'] = shape2D if ((isXLog and shape2D.xMin < FLOAT32_MINPOS) or @@ -1032,17 +1034,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): data = numpy.array(data, dtype=numpy.float32, order='C') colormapIsLog = colormap.getNormalization() == 'log' - cmapRange = colormap.getColormapRange(data=data) - - # Retrieve colormap LUT from name and color array - colormapDisp = Colormap(name=colormap.getName(), - normalization=Colormap.LINEAR, - vmin=0, - vmax=255, - colors=colormap.getColormapLUT()) - colormapLut = colormapDisp.applyToData( - numpy.arange(256, dtype=numpy.uint8)) + colormapLut = colormap.getNColors(nbColors=256) image = GLPlotColormap(data, origin, @@ -1087,7 +1080,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def addItem(self, x, y, legend, shape, color, fill, overlay, z): # TODO handle overlay - if shape not in ('polygon', 'rectangle', 'line', 'vline', 'hline'): + if shape not in ('polygon', 'rectangle', 'line', + 'vline', 'hline', 'polylines'): raise NotImplementedError("Unsupported shape {0}".format(shape)) x = numpy.array(x, copy=False) @@ -1107,6 +1101,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): raise RuntimeError( 'Cannot add item with Y <= 0 with Y axis log scale') + # Ignore fill for polylines to mimic matplotlib + fill = fill if shape != 'polylines' else False + self._items[legend] = { 'shape': shape, 'color': Colors.rgba(color), @@ -1119,8 +1116,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def addMarker(self, x, y, legend, text, color, selectable, draggable, - symbol, constraint, overlay): - # TODO handle overlay + symbol, constraint): if symbol is None: symbol = '+' @@ -1227,90 +1223,93 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): self._plotFrame.size[1] - self._plotFrame.margins.bottom - 1) return xPlot, yPlot - def pickItems(self, x, y): + def pickItems(self, x, y, kinds): picked = [] dataPos = self.pixelToData(x, y, axis='left', check=True) if dataPos is not None: # Pick markers - for marker in reversed(list(self._markers.values())): - pixelPos = self.dataToPixel( - marker['x'], marker['y'], axis='left', check=False) - if pixelPos is None: # negative coord on a log axis - continue - - if marker['x'] is None: # Horizontal line - pt1 = self.pixelToData( - x, y - self._PICK_OFFSET, axis='left', check=False) - pt2 = self.pixelToData( - x, y + self._PICK_OFFSET, axis='left', check=False) - isPicked = (min(pt1[1], pt2[1]) <= marker['y'] <= - max(pt1[1], pt2[1])) - - elif marker['y'] is None: # Vertical line - pt1 = self.pixelToData( - x - self._PICK_OFFSET, y, axis='left', check=False) - pt2 = self.pixelToData( - x + self._PICK_OFFSET, y, axis='left', check=False) - isPicked = (min(pt1[0], pt2[0]) <= marker['x'] <= - max(pt1[0], pt2[0])) - - else: - isPicked = ( - numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and - numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET) - - if isPicked: - picked.append(dict(kind='marker', - legend=marker['legend'])) - - # Pick image and curves - for item in self._plotContent.zOrderedPrimitives(reverse=True): - if isinstance(item, (GLPlotColormap, GLPlotRGBAImage)): - pickedPos = item.pick(*dataPos) - if pickedPos is not None: - picked.append(dict(kind='image', - legend=item.info['legend'])) - - elif isinstance(item, GLPlotCurve2D): - offset = self._PICK_OFFSET - if item.marker is not None: - offset = max(item.markerSize / 2., offset) - if item.lineStyle is not None: - offset = max(item.lineWidth / 2., offset) - - yAxis = item.info['yAxis'] - - inAreaPos = self._mouseInPlotArea(x - offset, y - offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) - if dataPos is None: + if 'marker' in kinds: + for marker in reversed(list(self._markers.values())): + pixelPos = self.dataToPixel( + marker['x'], marker['y'], axis='left', check=False) + if pixelPos is None: # negative coord on a log axis continue - xPick0, yPick0 = dataPos - inAreaPos = self._mouseInPlotArea(x + offset, y + offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) - if dataPos is None: - continue - xPick1, yPick1 = dataPos + if marker['x'] is None: # Horizontal line + pt1 = self.pixelToData( + x, y - self._PICK_OFFSET, axis='left', check=False) + pt2 = self.pixelToData( + x, y + self._PICK_OFFSET, axis='left', check=False) + isPicked = (min(pt1[1], pt2[1]) <= marker['y'] <= + max(pt1[1], pt2[1])) + + elif marker['y'] is None: # Vertical line + pt1 = self.pixelToData( + x - self._PICK_OFFSET, y, axis='left', check=False) + pt2 = self.pixelToData( + x + self._PICK_OFFSET, y, axis='left', check=False) + isPicked = (min(pt1[0], pt2[0]) <= marker['x'] <= + max(pt1[0], pt2[0])) - if xPick0 < xPick1: - xPickMin, xPickMax = xPick0, xPick1 else: - xPickMin, xPickMax = xPick1, xPick0 + isPicked = ( + numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and + numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET) - if yPick0 < yPick1: - yPickMin, yPickMax = yPick0, yPick1 - else: - yPickMin, yPickMax = yPick1, yPick0 - - pickedIndices = item.pick(xPickMin, yPickMin, - xPickMax, yPickMax) - if pickedIndices: - picked.append(dict(kind='curve', - legend=item.info['legend'], - indices=pickedIndices)) + if isPicked: + picked.append(dict(kind='marker', + legend=marker['legend'])) + + # Pick image and curves + if 'image' in kinds or 'curve' in kinds: + for item in self._plotContent.zOrderedPrimitives(reverse=True): + if ('image' in kinds and + isinstance(item, (GLPlotColormap, GLPlotRGBAImage))): + pickedPos = item.pick(*dataPos) + if pickedPos is not None: + picked.append(dict(kind='image', + legend=item.info['legend'])) + + elif 'curve' in kinds and isinstance(item, GLPlotCurve2D): + offset = self._PICK_OFFSET + if item.marker is not None: + offset = max(item.markerSize / 2., offset) + if item.lineStyle is not None: + offset = max(item.lineWidth / 2., offset) + + yAxis = item.info['yAxis'] + + inAreaPos = self._mouseInPlotArea(x - offset, y - offset) + dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], + axis=yAxis, check=True) + if dataPos is None: + continue + xPick0, yPick0 = dataPos + + inAreaPos = self._mouseInPlotArea(x + offset, y + offset) + dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], + axis=yAxis, check=True) + if dataPos is None: + continue + xPick1, yPick1 = dataPos + + if xPick0 < xPick1: + xPickMin, xPickMax = xPick0, xPick1 + else: + xPickMin, xPickMax = xPick1, xPick0 + + if yPick0 < yPick1: + yPickMin, yPickMax = yPick0, yPick1 + else: + yPickMin, yPickMax = yPick1, yPick0 + + pickedIndices = item.pick(xPickMin, yPickMin, + xPickMax, yPickMax) + if pickedIndices: + picked.append(dict(kind='curve', + legend=item.info['legend'], + indices=pickedIndices)) return picked diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py index 4433613..124a3da 100644 --- a/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py @@ -606,7 +606,7 @@ class _Points2D(object): """, ASTERISK: """ float alphaSymbol(vec2 coord, float size) { - /* Combining +, x and cirle */ + /* Combining +, x and circle */ vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5))); vec2 pos = floor(size * coord) + 0.5; vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size)); diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py index f028ee8..83c7ae0 100644 --- a/silx/gui/plot/backends/glutils/PlotImageFile.py +++ b/silx/gui/plot/backends/glutils/PlotImageFile.py @@ -93,7 +93,7 @@ def saveImageToFile(data, fileNameOrObj, fileFormat): assert fileFormat in ('png', 'ppm', 'svg', 'tiff') if not hasattr(fileNameOrObj, 'write'): - if sys.version < "3.0": + if sys.version_info < (3, ): fileObj = open(fileNameOrObj, "wb") else: if fileFormat in ('png', 'ppm', 'tiff'): |