diff options
Diffstat (limited to 'silx/gui/plot/backends/BackendOpenGL.py')
-rwxr-xr-x[-rw-r--r--] | silx/gui/plot/backends/BackendOpenGL.py | 990 |
1 files changed, 363 insertions, 627 deletions
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py index 0420aa9..27f3894 100644..100755 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ b/silx/gui/plot/backends/BackendOpenGL.py @@ -30,13 +30,13 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "21/12/2018" -from collections import OrderedDict, namedtuple import logging import warnings import weakref import numpy +from .. import items from .._utils import FLOAT32_MINPOS from . import BackendBase from ... import colors @@ -59,186 +59,66 @@ _logger = logging.getLogger(__name__) # TODO check if OpenGL is available # TODO make an off-screen mesa backend -# Bounds ###################################################################### - -class Range(namedtuple('Range', ('min_', 'max_'))): - """Describes a 1D range""" - - @property - def range_(self): - return self.max_ - self.min_ - - @property - def center(self): - return 0.5 * (self.min_ + self.max_) - - -class Bounds(object): - """Describes plot bounds with 2 y axis""" - - def __init__(self, xMin, xMax, yMin, yMax, y2Min, y2Max): - self._xAxis = Range(xMin, xMax) - self._yAxis = Range(yMin, yMax) - self._y2Axis = Range(y2Min, y2Max) - - def __repr__(self): - return "x: %s, y: %s, y2: %s" % (repr(self._xAxis), - repr(self._yAxis), - repr(self._y2Axis)) - - @property - def xAxis(self): - return self._xAxis - - @property - def yAxis(self): - return self._yAxis - - @property - def y2Axis(self): - return self._y2Axis - - # Content ##################################################################### -class PlotDataContent(object): - """Manage plot data content: images and curves. - - This class is only meant to work with _OpenGLPlotCanvas. - """ - - _PRIMITIVE_TYPES = 'curve', 'image', 'triangles' - - def __init__(self): - self._primitives = OrderedDict() # For images and curves - - def add(self, primitive): - """Add a curve or image to the content dictionary. - - This function generates the key in the dict from the primitive. - - :param primitive: The primitive to add. - :type primitive: Instance of GLPlotCurve2D, GLPlotColormap, - GLPlotRGBAImage. - """ - if isinstance(primitive, GLPlotCurve2D): - primitiveType = 'curve' - elif isinstance(primitive, (GLPlotColormap, GLPlotRGBAImage)): - primitiveType = 'image' - elif isinstance(primitive, GLPlotTriangles): - primitiveType = 'triangles' - else: - raise RuntimeError('Unsupported object type: %s', primitive) - - key = primitiveType, primitive.info['legend'] - self._primitives[key] = primitive +class _ShapeItem(dict): + def __init__(self, x, y, shape, color, fill, overlay, z, + linestyle, linewidth, linebgcolor): + super(_ShapeItem, self).__init__() - def get(self, primitiveType, legend): - """Get the corresponding primitive of given type with given legend. + if shape not in ('polygon', 'rectangle', 'line', + 'vline', 'hline', 'polylines'): + raise NotImplementedError("Unsupported shape {0}".format(shape)) - :param str primitiveType: Type of primitive ('curve' or 'image'). - :param str legend: The legend of the primitive to retrieve. - :return: The corresponding curve or None if no such curve. - """ - assert primitiveType in self._PRIMITIVE_TYPES - return self._primitives.get((primitiveType, legend)) + x = numpy.array(x, copy=False) + y = numpy.array(y, copy=False) - def pop(self, primitiveType, key): - """Pop the corresponding curve or return None if no such curve. + if shape == 'rectangle': + xMin, xMax = x + x = numpy.array((xMin, xMin, xMax, xMax)) + yMin, yMax = y + y = numpy.array((yMin, yMax, yMax, yMin)) - :param str primitiveType: - :param str key: - :return: - """ - assert primitiveType in self._PRIMITIVE_TYPES - return self._primitives.pop((primitiveType, key), None) + # Ignore fill for polylines to mimic matplotlib + fill = fill if shape != 'polylines' else False - def zOrderedPrimitives(self, reverse=False): - """List of primitives sorted according to their z order. + self.update({ + 'shape': shape, + 'color': colors.rgba(color), + 'fill': 'hatch' if fill else None, + 'x': x, + 'y': y, + 'linestyle': linestyle, + 'linewidth': linewidth, + 'linebgcolor': linebgcolor, + }) - It is a stable sort (as sorted): - Original order is preserved when key is the same. - :param bool reverse: Ascending (True, default) or descending (False). - """ - return sorted(self._primitives.values(), - key=lambda primitive: primitive.info['zOrder'], - reverse=reverse) - - def primitives(self): - """Iterator over all primitives.""" - return self._primitives.values() - - def primitiveKeys(self, primitiveType): - """Iterator over primitives of a specific type.""" - assert primitiveType in self._PRIMITIVE_TYPES - for type_, key in self._primitives.keys(): - if type_ == primitiveType: - yield key - - def getBounds(self, xPositive=False, yPositive=False): - """Bounds of the data. - - Can return strictly positive bounds (for log scale). - In this case, curves are clipped to their smaller positive value - and images with negative min are ignored. - - :param bool xPositive: True to get strictly positive range. - :param bool yPositive: True to get strictly positive range. - :return: The range of data for x, y and y2, or default (1., 100.) - if no range found for one dimension. - :rtype: Bounds - """ - xMin, yMin, y2Min = float('inf'), float('inf'), float('inf') - xMax = 0. if xPositive else -float('inf') - if yPositive: - yMax, y2Max = 0., 0. - else: - yMax, y2Max = -float('inf'), -float('inf') - - for item in self._primitives.values(): - # To support curve <= 0. and log and bypass images: - # If positive only, uses x|yMinPos if available - # and bypass other data with negative min bounds - if xPositive: - itemXMin = getattr(item, 'xMinPos', item.xMin) - if itemXMin is None or itemXMin < FLOAT32_MINPOS: - continue - else: - itemXMin = item.xMin +class _MarkerItem(dict): + def __init__(self, x, y, text, color, + symbol, linestyle, linewidth, constraint, yaxis): + super(_MarkerItem, self).__init__() - if yPositive: - itemYMin = getattr(item, 'yMinPos', item.yMin) - if itemYMin is None or itemYMin < FLOAT32_MINPOS: - continue - else: - itemYMin = item.yMin - - if itemXMin < xMin: - xMin = itemXMin - if item.xMax > xMax: - xMax = item.xMax - - if item.info.get('yAxis') == 'right': - if itemYMin < y2Min: - y2Min = itemYMin - if item.yMax > y2Max: - y2Max = item.yMax - else: - if itemYMin < yMin: - yMin = itemYMin - if item.yMax > yMax: - yMax = item.yMax + if symbol is None: + symbol = '+' - # One of the limit has not been updated, return default range - if xMin >= xMax: - xMin, xMax = 1., 100. - if yMin >= yMax: - yMin, yMax = 1., 100. - if y2Min >= y2Max: - y2Min, y2Max = 1., 100. + # Apply constraint to provided position + isConstraint = (constraint is not None and + x is not None and y is not None) + if isConstraint: + x, y = constraint(x, y) - return Bounds(xMin, xMax, yMin, yMax, y2Min, y2Max) + self.update({ + 'x': x, + 'y': y, + 'text': text, + 'color': colors.rgba(color), + 'constraint': constraint if isConstraint else None, + 'symbol': symbol, + 'linestyle': linestyle, + 'linewidth': linewidth, + 'yaxis': yaxis, + }) # shaders ##################################################################### @@ -350,9 +230,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): self._crosshairCursor = None self._mousePosInPixels = None - self._markers = OrderedDict() - self._items = OrderedDict() - self._plotContent = PlotDataContent() # For images and curves self._glGarbageCollector = [] self._plotFrame = GLPlotFrame2D( @@ -457,7 +334,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def _paintDirectGL(self): self._renderPlotAreaGL() self._plotFrame.render() - self._renderMarkersGL() self._renderOverlayGL() def _paintFBOGL(self): @@ -522,7 +398,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): with plotFBOTex.texture: gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0])) - self._renderMarkersGL() self._renderOverlayGL() def paintGL(self): @@ -543,120 +418,203 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # self._paintDirectGL() self._paintFBOGL() - def _renderMarkersGL(self): - if len(self._markers) == 0: - return + def _renderItems(self, overlay=False): + """Render items according to :class:`PlotWidget` order - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] + Note: Scissor test should already be set. - # Render in plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) + :param bool overlay: + False (the default) to render item that are not overlays. + True to render items that are overlays. + """ + # Values that are often used + plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] + isXLog = self._plotFrame.xAxis.isLog + isYLog = self._plotFrame.yAxis.isLog + # Used by marker rendering labels = [] pixelOffset = 3 - for marker in self._markers.values(): - xCoord, yCoord = marker['x'], marker['y'] - - if ((self._plotFrame.xAxis.isLog and - xCoord is not None and - xCoord <= 0) or - (self._plotFrame.yAxis.isLog and - yCoord is not None and - yCoord <= 0)): - # Do not render markers with negative coords on log axis + for plotItem in self.getItemsFromBackToFront( + condition=lambda i: i.isVisible() and i.isOverlay() == overlay): + if plotItem._backendRenderer is None: continue - if xCoord is None or yCoord is None: - pixelPos = self.dataToPixel( - xCoord, yCoord, axis='left', check=False) - - if xCoord is None: # Horizontal line in data space - if marker['text'] is not None: - x = self._plotFrame.size[0] - \ - self._plotFrame.margins.right - pixelOffset - y = pixelPos[1] - pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=RIGHT, valign=BOTTOM) - labels.append(label) + item = plotItem._backendRenderer + + if isinstance(item, (GLPlotCurve2D, + GLPlotColormap, + GLPlotRGBAImage, + GLPlotTriangles)): # Render data items + gl.glViewport(self._plotFrame.margins.left, + self._plotFrame.margins.bottom, + plotWidth, plotHeight) + if isinstance(item, GLPlotCurve2D) and item.info.get('yAxis') == 'right': + item.render(self._plotFrame.transformedDataY2ProjMat, + isXLog, isYLog) + else: + item.render(self._plotFrame.transformedDataProjMat, + isXLog, isYLog) + + elif isinstance(item, _ShapeItem): # Render shape items + gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) + + if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or + (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)): + # Ignore items <= 0. on log axes + continue + + if item['shape'] == 'hline': width = self._plotFrame.size[0] - lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]), - style=marker['linestyle'], - color=marker['color'], - width=marker['linewidth']) + _, yPixel = self._plot.dataToPixel( + None, item['y'], axis='left', check=False) + points = numpy.array(((0., yPixel), (width, yPixel)), + dtype=numpy.float32) + + elif item['shape'] == 'vline': + xPixel, _ = self._plot.dataToPixel( + item['x'], None, axis='left', check=False) + height = self._plotFrame.size[1] + points = numpy.array(((xPixel, 0), (xPixel, height)), + dtype=numpy.float32) + + else: + points = numpy.array([ + self._plot.dataToPixel(x, y, axis='left', check=False) + for (x, y) in zip(item['x'], item['y'])]) + + # Draw the fill + if (item['fill'] is not None and + item['shape'] not in ('hline', 'vline')): + self._progBase.use() + gl.glUniformMatrix4fv( + self._progBase.uniforms['matrix'], 1, gl.GL_TRUE, + self.matScreenProj.astype(numpy.float32)) + gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) + gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) + + shape2D = FilledShape2D( + points, style=item['fill'], color=item['color']) + shape2D.render( + posAttrib=self._progBase.attributes['position'], + colorUnif=self._progBase.uniforms['color'], + hatchStepUnif=self._progBase.uniforms['hatchStep']) + + # Draw the stroke + if item['linestyle'] not in ('', ' ', None): + if item['shape'] != 'polylines': + # close the polyline + points = numpy.append(points, + numpy.atleast_2d(points[0]), axis=0) + + lines = GLLines2D(points[:, 0], points[:, 1], + style=item['linestyle'], + color=item['color'], + dash2ndColor=item['linebgcolor'], + width=item['linewidth']) lines.render(self.matScreenProj) - else: # yCoord is None: vertical line in data space - if marker['text'] is not None: + elif isinstance(item, _MarkerItem): + gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) + + xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis'] + + if ((isXLog and xCoord is not None and xCoord <= 0) or + (isYLog and yCoord is not None and yCoord <= 0)): + # Do not render markers with negative coords on log axis + continue + + if xCoord is None or yCoord is None: + pixelPos = self._plot.dataToPixel( + xCoord, yCoord, axis=yAxis, check=False) + + if xCoord is None: # Horizontal line in data space + if item['text'] is not None: + x = self._plotFrame.size[0] - \ + self._plotFrame.margins.right - pixelOffset + y = pixelPos[1] - pixelOffset + label = Text2D(item['text'], x, y, + color=item['color'], + bgColor=(1., 1., 1., 0.5), + align=RIGHT, valign=BOTTOM) + labels.append(label) + + width = self._plotFrame.size[0] + lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]), + style=item['linestyle'], + color=item['color'], + width=item['linewidth']) + lines.render(self.matScreenProj) + + else: # yCoord is None: vertical line in data space + if item['text'] is not None: + x = pixelPos[0] + pixelOffset + y = self._plotFrame.margins.top + pixelOffset + label = Text2D(item['text'], x, y, + color=item['color'], + bgColor=(1., 1., 1., 0.5), + align=LEFT, valign=TOP) + labels.append(label) + + height = self._plotFrame.size[1] + lines = GLLines2D((pixelPos[0], pixelPos[0]), (0, height), + style=item['linestyle'], + color=item['color'], + width=item['linewidth']) + lines.render(self.matScreenProj) + + else: + pixelPos = self._plot.dataToPixel( + xCoord, yCoord, axis=yAxis, check=True) + if pixelPos is None: + # Do not render markers outside visible plot area + continue + + if item['text'] is not None: x = pixelPos[0] + pixelOffset - y = self._plotFrame.margins.top + pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], + y = pixelPos[1] + pixelOffset + label = Text2D(item['text'], x, y, + color=item['color'], bgColor=(1., 1., 1., 0.5), align=LEFT, valign=TOP) labels.append(label) - height = self._plotFrame.size[1] - lines = GLLines2D((pixelPos[0], pixelPos[0]), (0, height), - style=marker['linestyle'], - color=marker['color'], - width=marker['linewidth']) - lines.render(self.matScreenProj) + # For now simple implementation: using a curve for each marker + # Should pack all markers to a single set of points + markerCurve = GLPlotCurve2D( + numpy.array((pixelPos[0],), dtype=numpy.float64), + numpy.array((pixelPos[1],), dtype=numpy.float64), + marker=item['symbol'], + markerColor=item['color'], + markerSize=11) + markerCurve.render(self.matScreenProj, False, False) + markerCurve.discard() else: - pixelPos = self.dataToPixel( - xCoord, yCoord, axis='left', check=True) - if pixelPos is None: - # Do not render markers outside visible plot area - continue - - if marker['text'] is not None: - x = pixelPos[0] + pixelOffset - y = pixelPos[1] + pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=LEFT, valign=TOP) - labels.append(label) - - # For now simple implementation: using a curve for each marker - # Should pack all markers to a single set of points - markerCurve = GLPlotCurve2D( - numpy.array((pixelPos[0],), dtype=numpy.float64), - numpy.array((pixelPos[1],), dtype=numpy.float64), - marker=marker['symbol'], - markerColor=marker['color'], - markerSize=11) - markerCurve.render(self.matScreenProj, False, False) - markerCurve.discard() - - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) + _logger.error('Unsupported item: %s', str(item)) + continue # Render marker labels + gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) for label in labels: label.render(self.matScreenProj) - gl.glDisable(gl.GL_SCISSOR_TEST) - def _renderOverlayGL(self): - # Render crosshair cursor - if self._crosshairCursor is not None: - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] + """Render overlay layer: overlay items and crosshair.""" + plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] + + # Scissor to plot area + gl.glScissor(self._plotFrame.margins.left, + self._plotFrame.margins.bottom, + plotWidth, plotHeight) + gl.glEnable(gl.GL_SCISSOR_TEST) - # Scissor to plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) + self._renderItems(overlay=True) + # Render crosshair cursor + if self._crosshairCursor is not None and self._mousePosInPixels is not None: self._progBase.use() gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) @@ -665,39 +623,39 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): colorUnif = self._progBase.uniforms['color'] hatchStepUnif = self._progBase.uniforms['hatchStep'] - # Render crosshair cursor in screen frame but with scissor - if (self._crosshairCursor is not None and - self._mousePosInPixels is not None): - gl.glViewport( - 0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - - color, lineWidth = self._crosshairCursor - gl.glUniform4f(colorUnif, *color) - gl.glUniform1i(hatchStepUnif, 0) - - xPixel, yPixel = self._mousePosInPixels - xPixel, yPixel = xPixel + 0.5, yPixel + 0.5 - vertices = numpy.array(((0., yPixel), - (self._plotFrame.size[0], yPixel), - (xPixel, 0.), - (xPixel, self._plotFrame.size[1])), - dtype=numpy.float32) - - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - gl.glLineWidth(lineWidth) - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - gl.glDisable(gl.GL_SCISSOR_TEST) + gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) + + gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE, + self.matScreenProj.astype(numpy.float32)) + + color, lineWidth = self._crosshairCursor + gl.glUniform4f(colorUnif, *color) + gl.glUniform1i(hatchStepUnif, 0) + + xPixel, yPixel = self._mousePosInPixels + xPixel, yPixel = xPixel + 0.5, yPixel + 0.5 + vertices = numpy.array(((0., yPixel), + (self._plotFrame.size[0], yPixel), + (xPixel, 0.), + (xPixel, self._plotFrame.size[1])), + dtype=numpy.float32) + + gl.glEnableVertexAttribArray(posAttrib) + gl.glVertexAttribPointer(posAttrib, + 2, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, vertices) + gl.glLineWidth(lineWidth) + gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) + + gl.glDisable(gl.GL_SCISSOR_TEST) def _renderPlotAreaGL(self): + """Render base layer of plot area. + + It renders the background, grid and items except overlays + """ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] gl.glScissor(self._plotFrame.margins.left, @@ -713,85 +671,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # Matrix trBounds = self._plotFrame.transformedDataRanges - if trBounds.x[0] == trBounds.x[1] or \ - trBounds.y[0] == trBounds.y[1]: - return - - isXLog = self._plotFrame.xAxis.isLog - isYLog = self._plotFrame.yAxis.isLog - - gl.glViewport(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - - # Render images and curves - # sorted is stable: original order is preserved when key is the same - for item in self._plotContent.zOrderedPrimitives(): - if item.info.get('yAxis') == 'right': - item.render(self._plotFrame.transformedDataY2ProjMat, - isXLog, isYLog) - else: - item.render(self._plotFrame.transformedDataProjMat, - isXLog, isYLog) - - # Render Items - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - for item in self._items.values(): - if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or - (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)): - # Ignore items <= 0. on log axes - continue - - if item['shape'] == 'hline': - width = self._plotFrame.size[0] - _, yPixel = self.dataToPixel( - None, item['y'], axis='left', check=False) - points = numpy.array(((0., yPixel), (width, yPixel)), - dtype=numpy.float32) - - elif item['shape'] == 'vline': - xPixel, _ = self.dataToPixel( - item['x'], None, axis='left', check=False) - height = self._plotFrame.size[1] - points = numpy.array(((xPixel, 0), (xPixel, height)), - dtype=numpy.float32) - - else: - points = numpy.array([ - self.dataToPixel(x, y, axis='left', check=False) - for (x, y) in zip(item['x'], item['y'])]) - - # Draw the fill - if (item['fill'] is not None and - item['shape'] not in ('hline', 'vline')): - self._progBase.use() - gl.glUniformMatrix4fv( - self._progBase.uniforms['matrix'], 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - - shape2D = FilledShape2D( - points, style=item['fill'], color=item['color']) - shape2D.render( - posAttrib=self._progBase.attributes['position'], - colorUnif=self._progBase.uniforms['color'], - hatchStepUnif=self._progBase.uniforms['hatchStep']) - - # Draw the stroke - if item['linestyle'] not in ('', ' ', None): - if item['shape'] != 'polylines': - # close the polyline - points = numpy.append(points, - numpy.atleast_2d(points[0]), axis=0) - - lines = GLLines2D(points[:, 0], points[:, 1], - style=item['linestyle'], - color=item['color'], - dash2ndColor=item['linebgcolor'], - width=item['linewidth']) - lines.render(self.matScreenProj) + if trBounds.x[0] != trBounds.x[1] and trBounds.y[0] != trBounds.y[1]: + # Do rendering of items + self._renderItems(overlay=False) gl.glDisable(gl.GL_SCISSOR_TEST) @@ -841,13 +723,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): else: raise ValueError('Unsupported data type') - def addCurve(self, x, y, legend, + def addCurve(self, x, y, color, symbol, linewidth, linestyle, yaxis, - xerror, yerror, z, selectable, - fill, alpha, symbolsize): - for parameter in (x, y, legend, color, symbol, linewidth, linestyle, - yaxis, z, selectable, fill, symbolsize): + xerror, yerror, z, + fill, alpha, symbolsize, baseline): + for parameter in (x, y, color, symbol, linewidth, linestyle, + yaxis, z, fill, symbolsize): assert parameter is not None assert yaxis in ('left', 'right') @@ -939,10 +821,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): if color is not None: color = color[0], color[1], color[2], color[3] * alpha - behaviors = set() - if selectable: - behaviors.add('selectable') - + fillColor = None + if fill is True: + fillColor = color curve = GLPlotCurve2D(x, y, colorArray, xError=xerror, yError=yerror, @@ -952,36 +833,24 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): marker=symbol, markerColor=color, markerSize=symbolsize, - fillColor=color if fill else None, + fillColor=fillColor, + baseline=baseline, isYLog=isYLog) curve.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors, 'yAxis': 'left' if yaxis is None else yaxis, } if yaxis == "right": self._plotFrame.isY2Axis = True - self._plotContent.add(curve) + return curve - return legend, 'curve' - - def addImage(self, data, legend, + def addImage(self, data, origin, scale, z, - selectable, draggable, colormap, alpha): - for parameter in (data, legend, origin, scale, z, - selectable, draggable): + for parameter in (data, origin, scale, z): assert parameter is not None - behaviors = set() - if selectable: - behaviors.add('selectable') - if draggable: - behaviors.add('draggable') - if data.ndim == 2: # Ensure array is contiguous and eventually convert its type if data.dtype in (numpy.float32, numpy.uint8, numpy.uint16): @@ -1002,12 +871,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): colormapIsLog, cmapRange, alpha) - image.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors - } - self._plotContent.add(image) elif len(data.shape) == 3: # For RGB, RGBA data @@ -1022,29 +885,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): image = GLPlotRGBAImage(data, origin, scale, alpha) - image.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors - } - - if self._plotFrame.xAxis.isLog and image.xMin <= 0.: - raise RuntimeError( - 'Cannot add image with X <= 0 with X axis log scale') - if self._plotFrame.yAxis.isLog and image.yMin <= 0.: - raise RuntimeError( - 'Cannot add image with Y <= 0 with Y axis log scale') - - self._plotContent.add(image) - else: raise RuntimeError("Unsupported data shape {0}".format(data.shape)) - return legend, 'image' + # TODO is this needed? + if self._plotFrame.xAxis.isLog and image.xMin <= 0.: + raise RuntimeError( + 'Cannot add image with X <= 0 with X axis log scale') + if self._plotFrame.yAxis.isLog and image.yMin <= 0.: + raise RuntimeError( + 'Cannot add image with Y <= 0 with Y axis log scale') - def addTriangles(self, x, y, triangles, legend, - color, z, selectable, alpha): + return image + def addTriangles(self, x, y, triangles, + color, z, alpha): # Handle axes log scale: convert data if self._plotFrame.xAxis.isLog: x = numpy.log10(x) @@ -1052,31 +907,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): y = numpy.log10(y) triangles = GLPlotTriangles(x, y, color, triangles, alpha) - triangles.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': set(['selectable']) if selectable else set(), - } - self._plotContent.add(triangles) - return legend, 'triangles' + return triangles - def addItem(self, x, y, legend, shape, color, fill, overlay, z, + def addItem(self, x, y, shape, color, fill, overlay, z, linestyle, linewidth, linebgcolor): - # TODO handle overlay - if shape not in ('polygon', 'rectangle', 'line', - 'vline', 'hline', 'polylines'): - raise NotImplementedError("Unsupported shape {0}".format(shape)) - x = numpy.array(x, copy=False) y = numpy.array(y, copy=False) - if shape == 'rectangle': - xMin, xMax = x - x = numpy.array((xMin, xMin, xMax, xMax)) - yMin, yMax = y - y = numpy.array((yMin, yMax, yMax, yMin)) - # TODO is this needed? if self._plotFrame.xAxis.isLog and x.min() <= 0.: raise RuntimeError( @@ -1085,84 +923,35 @@ 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), - 'fill': 'hatch' if fill else None, - 'x': x, - 'y': y, - 'linestyle': linestyle, - 'linewidth': linewidth, - 'linebgcolor': linebgcolor, - } - - return legend, 'item' - - def addMarker(self, x, y, legend, text, color, - selectable, draggable, - symbol, linestyle, linewidth, constraint): - - if symbol is None: - symbol = '+' - - behaviors = set() - if selectable: - behaviors.add('selectable') - if draggable: - behaviors.add('draggable') - - # Apply constraint to provided position - isConstraint = (draggable and constraint is not None and - x is not None and y is not None) - if isConstraint: - x, y = constraint(x, y) - - self._markers[legend] = { - 'x': x, - 'y': y, - 'legend': legend, - 'text': text, - 'color': colors.rgba(color), - 'behaviors': behaviors, - 'constraint': constraint if isConstraint else None, - 'symbol': symbol, - 'linestyle': linestyle, - 'linewidth': linewidth, - } + return _ShapeItem(x, y, shape, color, fill, overlay, z, + linestyle, linewidth, linebgcolor) - return legend, 'marker' + def addMarker(self, x, y, text, color, + symbol, linestyle, linewidth, constraint, yaxis): + return _MarkerItem(x, y, text, color, + symbol, linestyle, linewidth, constraint, yaxis) # Remove methods def remove(self, item): - legend, kind = item - - if kind == 'curve': - curve = self._plotContent.pop('curve', legend) - if curve is not None: + if isinstance(item, (GLPlotCurve2D, + GLPlotColormap, + GLPlotRGBAImage, + GLPlotTriangles)): + if isinstance(item, GLPlotCurve2D): # Check if some curves remains on the right Y axis - y2AxisItems = (item for item in self._plotContent.primitives() - if item.info.get('yAxis', 'left') == 'right') + y2AxisItems = (item for item in self._plot.getItems() + if isinstance(item, items.YAxisMixIn) and + item.getYAxis() == 'right') self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None - self._glGarbageCollector.append(curve) - - elif kind in ('image', 'triangles'): - item = self._plotContent.pop(kind, legend) - if item is not None: - self._glGarbageCollector.append(item) + self._glGarbageCollector.append(item) - elif kind == 'marker': - self._markers.pop(legend, False) - - elif kind == 'item': - self._items.pop(legend, False) + elif isinstance(item, (_MarkerItem, _ShapeItem)): + pass # No-op else: - _logger.error('Unsupported kind: %s', str(kind)) + _logger.error('Unsupported item: %s', str(item)) # Interaction methods @@ -1212,8 +1001,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): :param GLPlotCurve2D item: :param float x: X position of the mouse in widget coordinates :param float y: Y position of the mouse in widget coordinates - :return: List of indices of picked points - :rtype: List[int] + :return: List of indices of picked points or None if not picked + :rtype: Union[List[int],None] """ offset = self._PICK_OFFSET if item.marker is not None: @@ -1224,17 +1013,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): yAxis = item.info['yAxis'] inAreaPos = self._mouseInPlotArea(x - offset, y - offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) + dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1], + axis=yAxis, check=True) if dataPos is None: - return [] + return None xPick0, yPick0 = dataPos inAreaPos = self._mouseInPlotArea(x + offset, y + offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) + dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1], + axis=yAxis, check=True) if dataPos is None: - return [] + return None xPick1, yPick1 = dataPos if xPick0 < xPick1: @@ -1260,69 +1049,58 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): return item.pick(xPickMin, yPickMin, xPickMax, yPickMax) - def pickItems(self, x, y, kinds): - picked = [] - - dataPos = self.pixelToData(x, y, axis='left', check=True) - if dataPos is not None: - # Pick markers - 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 + def pickItem(self, x, y, item): + dataPos = self._plot.pixelToData(x, y, axis='left', check=True) + if dataPos is None: + return None # Outside plot area + + if item is None: + _logger.error("No item provided for picking") + return None + + # Pick markers + if isinstance(item, _MarkerItem): + yaxis = item['yaxis'] + pixelPos = self._plot.dataToPixel( + item['x'], item['y'], axis=yaxis, check=False) + if pixelPos is None: + return None # negative coord on a log axis + + if item['x'] is None: # Horizontal line + pt1 = self._plot.pixelToData( + x, y - self._PICK_OFFSET, axis=yaxis, check=False) + pt2 = self._plot.pixelToData( + x, y + self._PICK_OFFSET, axis=yaxis, check=False) + isPicked = (min(pt1[1], pt2[1]) <= item['y'] <= + max(pt1[1], pt2[1])) + + elif item['y'] is None: # Vertical line + pt1 = self._plot.pixelToData( + x - self._PICK_OFFSET, y, axis=yaxis, check=False) + pt2 = self._plot.pixelToData( + x + self._PICK_OFFSET, y, axis=yaxis, check=False) + isPicked = (min(pt1[0], pt2[0]) <= item['x'] <= + max(pt1[0], pt2[0])) - 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 - 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: - if isinstance(item, GLPlotCurve2D): - pickedIndices = self.__pickCurves(item, x, y) - if pickedIndices: - picked.append(dict(kind='curve', - legend=item.info['legend'], - indices=pickedIndices)) - - elif isinstance(item, GLPlotTriangles): - pickedIndices = item.pick(*dataPos) - if pickedIndices: - picked.append(dict(kind='curve', - legend=item.info['legend'], - indices=pickedIndices)) - return picked + else: + isPicked = ( + numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and + numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET) + + return (0,) if isPicked else None + + # Pick image, curve, triangles + elif isinstance(item, (GLPlotCurve2D, + GLPlotColormap, + GLPlotRGBAImage, + GLPlotTriangles)): + if isinstance(item, (GLPlotColormap, GLPlotRGBAImage, GLPlotTriangles)): + return item.pick(*dataPos) # Might be None + + elif isinstance(item, GLPlotCurve2D): + return self.__pickCurves(item, x, y) + else: + return None # Update curve @@ -1426,12 +1204,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): return if keepDim is None: - dataBounds = self._plotContent.getBounds( - self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog) - if dataBounds.yAxis.range_ != 0.: - dataRatio = dataBounds.xAxis.range_ - dataRatio /= float(dataBounds.yAxis.range_) - + ranges = self._plot.getDataRange() + if (ranges.y is not None and + ranges.x is not None and + (ranges.y[1] - ranges.y[0]) != 0.): + dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0]) plotRatio = plotWidth / float(plotHeight) # Test != 0 before keepDim = 'x' if dataRatio > plotRatio else 'y' @@ -1564,51 +1341,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # Data <-> Pixel coordinates conversion - def dataToPixel(self, x, y, axis, check=False): - assert axis in ('left', 'right') - - if x is None or y is None: - dataBounds = self._plotContent.getBounds( - self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog) - - if x is None: - x = dataBounds.xAxis.center - - if y is None: - if axis == 'left': - y = dataBounds.yAxis.center - else: - y = dataBounds.y2Axis.center - - result = self._plotFrame.dataToPixel(x, y, axis) - - if check and result is not None: - xPixel, yPixel = result - width, height = self._plotFrame.size - if (xPixel < self._plotFrame.margins.left or - xPixel > (width - self._plotFrame.margins.right) or - yPixel < self._plotFrame.margins.top or - yPixel > height - self._plotFrame.margins.bottom): - return None # (x, y) is out of plot area - - return result - - def pixelToData(self, x, y, axis, check): - assert axis in ("left", "right") - - if x is None: - x = self._plotFrame.size[0] / 2. - if y is None: - y = self._plotFrame.size[1] / 2. - - if check and (x < self._plotFrame.margins.left or - x > (self._plotFrame.size[0] - - self._plotFrame.margins.right) or - y < self._plotFrame.margins.top or - y > (self._plotFrame.size[1] - - self._plotFrame.margins.bottom)): - return None # (x, y) is out of plot area + def dataToPixel(self, x, y, axis): + return self._plotFrame.dataToPixel(x, y, axis) + def pixelToData(self, x, y, axis): return self._plotFrame.pixelToData(x, y, axis) def getPlotBoundsInPixels(self): |