diff options
Diffstat (limited to 'silx/gui/plot/backends/BackendOpenGL.py')
-rw-r--r-- | silx/gui/plot/backends/BackendOpenGL.py | 1627 |
1 files changed, 0 insertions, 1627 deletions
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py deleted file mode 100644 index 0420aa9..0000000 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ /dev/null @@ -1,1627 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2019 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 -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""OpenGL Plot backend.""" - -from __future__ import division - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "21/12/2018" - -from collections import OrderedDict, namedtuple -import logging -import warnings -import weakref - -import numpy - -from .._utils import FLOAT32_MINPOS -from . import BackendBase -from ... import colors -from ... import qt - -from ..._glutils import gl -from ... import _glutils as glu -from .glutils import ( - GLLines2D, GLPlotTriangles, - GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D, - mat4Ortho, mat4Identity, - LEFT, RIGHT, BOTTOM, TOP, - Text2D, FilledShape2D) -from .glutils.PlotImageFile import saveImageToFile - -_logger = logging.getLogger(__name__) - - -# TODO idea: BackendQtMixIn class to share code between mpl and gl -# 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 - - def get(self, primitiveType, legend): - """Get the corresponding primitive of given type with given legend. - - :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)) - - def pop(self, primitiveType, key): - """Pop the corresponding curve or return None if no such curve. - - :param str primitiveType: - :param str key: - :return: - """ - assert primitiveType in self._PRIMITIVE_TYPES - return self._primitives.pop((primitiveType, key), None) - - def zOrderedPrimitives(self, reverse=False): - """List of primitives sorted according to their z order. - - 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 - - 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 - - # 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. - - return Bounds(xMin, xMax, yMin, yMax, y2Min, y2Max) - - -# shaders ##################################################################### - -_baseVertShd = """ - attribute vec2 position; - uniform mat4 matrix; - uniform bvec2 isLog; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec2 posTransformed = position; - if (isLog.x) { - posTransformed.x = oneOverLog10 * log(position.x); - } - if (isLog.y) { - posTransformed.y = oneOverLog10 * log(position.y); - } - gl_Position = matrix * vec4(posTransformed, 0.0, 1.0); - } - """ - -_baseFragShd = """ - uniform vec4 color; - uniform int hatchStep; - uniform float tickLen; - - void main(void) { - if (tickLen != 0.) { - if (mod((gl_FragCoord.x + gl_FragCoord.y) / tickLen, 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } else if (hatchStep == 0 || - mod(gl_FragCoord.x - gl_FragCoord.y, float(hatchStep)) == 0.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - -_texVertShd = """ - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 coords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - coords = texCoords; - } - """ - -_texFragShd = """ - uniform sampler2D tex; - - varying vec2 coords; - - void main(void) { - gl_FragColor = texture2D(tex, coords); - gl_FragColor.a = 1.0; - } - """ - -# BackendOpenGL ############################################################### - - -class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): - """OpenGL-based Plot backend. - - WARNINGS: - Unless stated otherwise, this API is NOT thread-safe and MUST be - called from the main thread. - When numpy arrays are passed as arguments to the API (through - :func:`addCurve` and :func:`addImage`), they are copied only if - required. - So, the caller should not modify these arrays afterwards. - """ - - _sigPostRedisplay = qt.Signal() - """Signal handling automatic asynchronous replot""" - - def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()): - glu.OpenGLWidget.__init__(self, parent, - alphaBufferSize=8, - depthBufferSize=0, - stencilBufferSize=0, - version=(2, 1), - f=f) - BackendBase.BackendBase.__init__(self, plot, parent) - - self._backgroundColor = 1., 1., 1., 1. - self._dataBackgroundColor = 1., 1., 1., 1. - - self.matScreenProj = mat4Identity() - - self._progBase = glu.Program( - _baseVertShd, _baseFragShd, attrib0='position') - self._progTex = glu.Program( - _texVertShd, _texFragShd, attrib0='position') - self._plotFBOs = weakref.WeakKeyDictionary() - - self._keepDataAspectRatio = False - - self._crosshairCursor = None - self._mousePosInPixels = None - - self._markers = OrderedDict() - self._items = OrderedDict() - self._plotContent = PlotDataContent() # For images and curves - self._glGarbageCollector = [] - - self._plotFrame = GLPlotFrame2D( - foregroundColor=(0., 0., 0., 1.), - gridColor=(.7, .7, .7, 1.), - margins={'left': 100, 'right': 50, 'top': 50, 'bottom': 50}) - - # Make postRedisplay asynchronous using Qt signal - self._sigPostRedisplay.connect( - super(BackendOpenGL, self).postRedisplay, - qt.Qt.QueuedConnection) - - self.setAutoFillBackground(False) - self.setMouseTracking(True) - - # QWidget - - _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'} - - 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 sizeHint(self): - return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend - - def mousePressEvent(self, event): - if event.button() not in self._MOUSE_BTNS: - return super(BackendOpenGL, self).mousePressEvent(event) - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - btn = self._MOUSE_BTNS[event.button()] - self._plot.onMousePress(xPixel, yPixel, btn) - event.accept() - - def mouseMoveEvent(self, event): - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - - # Handle crosshair - inXPixel, inYPixel = self._mouseInPlotArea(xPixel, yPixel) - isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel - - previousMousePosInPixels = self._mousePosInPixels - self._mousePosInPixels = (xPixel, yPixel) if isCursorInPlot else None - if (self._crosshairCursor is not None and - previousMousePosInPixels != self._mousePosInPixels): - # Avoid replot when cursor remains outside plot area - self._plot._setDirtyPlot(overlayOnly=True) - - self._plot.onMouseMove(xPixel, yPixel) - event.accept() - - def mouseReleaseEvent(self, event): - if event.button() not in self._MOUSE_BTNS: - return super(BackendOpenGL, self).mouseReleaseEvent(event) - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - - btn = self._MOUSE_BTNS[event.button()] - self._plot.onMouseRelease(xPixel, yPixel, btn) - event.accept() - - def wheelEvent(self, event): - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - - if hasattr(event, 'angleDelta'): # Qt 5 - delta = event.angleDelta().y() - else: # Qt 4 support - delta = event.delta() - angleInDegrees = delta / 8. - self._plot.onMouseWheel(xPixel, yPixel, angleInDegrees) - event.accept() - - def leaveEvent(self, _): - self._plot.onMouseLeaveWidget() - - # OpenGLWidget API - - def initializeGL(self): - gl.testGL() - - gl.glClearStencil(0) - - gl.glEnable(gl.GL_BLEND) - # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA, - gl.GL_ONE_MINUS_SRC_ALPHA, - gl.GL_ONE, - gl.GL_ONE) - - # For lines - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - - # For points - gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2 - gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2 - # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE) - - def _paintDirectGL(self): - self._renderPlotAreaGL() - self._plotFrame.render() - self._renderMarkersGL() - self._renderOverlayGL() - - def _paintFBOGL(self): - context = glu.Context.getCurrent() - plotFBOTex = self._plotFBOs.get(context) - if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or - plotFBOTex is None): - self._plotVertices = ( - # Vertex coordinates - numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)), - dtype=numpy.float32), - # Texture coordinates - numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)), - dtype=numpy.float32)) - if plotFBOTex is None or \ - plotFBOTex.shape[1] != self._plotFrame.size[0] or \ - plotFBOTex.shape[0] != self._plotFrame.size[1]: - if plotFBOTex is not None: - plotFBOTex.discard() - plotFBOTex = glu.FramebufferTexture( - gl.GL_RGBA, - shape=(self._plotFrame.size[1], - self._plotFrame.size[0]), - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)) - self._plotFBOs[context] = plotFBOTex - - with plotFBOTex: - gl.glClearColor(*self._backgroundColor) - gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT) - self._renderPlotAreaGL() - self._plotFrame.render() - - # Render plot in screen coords - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - self._progTex.use() - texUnit = 0 - - gl.glUniform1i(self._progTex.uniforms['tex'], texUnit) - gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE, - mat4Identity().astype(numpy.float32)) - - gl.glEnableVertexAttribArray(self._progTex.attributes['position']) - gl.glVertexAttribPointer(self._progTex.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._plotVertices[0]) - - gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords']) - gl.glVertexAttribPointer(self._progTex.attributes['texCoords'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._plotVertices[1]) - - with plotFBOTex.texture: - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0])) - - self._renderMarkersGL() - self._renderOverlayGL() - - def paintGL(self): - with glu.Context.current(self.context()): - # Release OpenGL resources - for item in self._glGarbageCollector: - item.discard() - self._glGarbageCollector = [] - - gl.glClearColor(*self._backgroundColor) - gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT) - - # Check if window is large enough - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - if plotWidth <= 2 or plotHeight <= 2: - return - - # self._paintDirectGL() - self._paintFBOGL() - - def _renderMarkersGL(self): - if len(self._markers) == 0: - return - - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - - # 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]) - - 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 - 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) - - width = self._plotFrame.size[0] - lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]), - style=marker['linestyle'], - color=marker['color'], - width=marker['linewidth']) - lines.render(self.matScreenProj) - - else: # yCoord is None: vertical line in data space - if marker['text'] is not None: - x = pixelPos[0] + pixelOffset - y = self._plotFrame.margins.top + pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['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) - - 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]) - - # Render marker labels - 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:] - - # Scissor to plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - self._progBase.use() - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - posAttrib = self._progBase.attributes['position'] - matrixUnif = self._progBase.uniforms['matrix'] - 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) - - def _renderPlotAreaGL(self): - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - if self._dataBackgroundColor != self._backgroundColor: - gl.glClearColor(*self._dataBackgroundColor) - gl.glClear(gl.GL_COLOR_BUFFER_BIT) - - self._plotFrame.renderGrid() - - # 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) - - gl.glDisable(gl.GL_SCISSOR_TEST) - - def resizeGL(self, width, height): - if width == 0 or height == 0: # Do not resize - return - - self._plotFrame.size = ( - int(self.getDevicePixelRatio() * width), - int(self.getDevicePixelRatio() * height)) - - self.matScreenProj = mat4Ortho(0, self._plotFrame.size[0], - self._plotFrame.size[1], 0, - 1, -1) - - # Store current ranges - previousXRange = self.getGraphXLimits() - previousYRange = self.getGraphYLimits(axis='left') - previousYRightRange = self.getGraphYLimits(axis='right') - - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \ - self._plotFrame.dataRanges - self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max) - - # If plot range has changed, then emit signal - if previousXRange != self.getGraphXLimits(): - self._plot.getXAxis()._emitLimitsChanged() - if previousYRange != self.getGraphYLimits(axis='left'): - self._plot.getYAxis(axis='left')._emitLimitsChanged() - if previousYRightRange != self.getGraphYLimits(axis='right'): - self._plot.getYAxis(axis='right')._emitLimitsChanged() - - # Add methods - - @staticmethod - def _castArrayTo(v): - """Returns best floating type to cast the array to. - - :param numpy.ndarray v: Array to cast - :rtype: numpy.dtype - :raise ValueError: If dtype is not supported - """ - if numpy.issubdtype(v.dtype, numpy.floating): - return numpy.float32 if v.itemsize <= 4 else numpy.float64 - elif numpy.issubdtype(v.dtype, numpy.integer): - return numpy.float32 if v.itemsize <= 2 else numpy.float64 - else: - raise ValueError('Unsupported data type') - - def addCurve(self, x, y, legend, - 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): - assert parameter is not None - assert yaxis in ('left', 'right') - - # Convert input data - x = numpy.array(x, copy=False) - y = numpy.array(y, copy=False) - - # Check if float32 is enough - if (self._castArrayTo(x) is numpy.float32 and - self._castArrayTo(y) is numpy.float32): - dtype = numpy.float32 - else: - dtype = numpy.float64 - - x = numpy.array(x, dtype=dtype, copy=False, order='C') - y = numpy.array(y, dtype=dtype, copy=False, order='C') - - # Convert errors to float32 - if xerror is not None: - xerror = numpy.array( - xerror, dtype=numpy.float32, copy=False, order='C') - if yerror is not None: - yerror = numpy.array( - yerror, dtype=numpy.float32, copy=False, order='C') - - # Handle axes log scale: convert data - - if self._plotFrame.xAxis.isLog: - logX = numpy.log10(x) - - if xerror is not None: - # Transform xerror so that - # log10(x) +/- xerror' = log10(x +/- xerror) - if hasattr(xerror, 'shape') and len(xerror.shape) == 2: - xErrorMinus, xErrorPlus = xerror[0], xerror[1] - else: - xErrorMinus, xErrorPlus = xerror, xerror - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore divide by zero, invalid value encountered in log10 - xErrorMinus = logX - numpy.log10(x - xErrorMinus) - xErrorPlus = numpy.log10(x + xErrorPlus) - logX - xerror = numpy.array((xErrorMinus, xErrorPlus), - dtype=numpy.float32) - - x = logX - - isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or ( - yaxis == 'right' and self._plotFrame.y2Axis.isLog) - - if isYLog: - logY = numpy.log10(y) - - if yerror is not None: - # Transform yerror so that - # log10(y) +/- yerror' = log10(y +/- yerror) - if hasattr(yerror, 'shape') and len(yerror.shape) == 2: - yErrorMinus, yErrorPlus = yerror[0], yerror[1] - else: - yErrorMinus, yErrorPlus = yerror, yerror - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore divide by zero, invalid value encountered in log10 - yErrorMinus = logY - numpy.log10(y - yErrorMinus) - yErrorPlus = numpy.log10(y + yErrorPlus) - logY - yerror = numpy.array((yErrorMinus, yErrorPlus), - dtype=numpy.float32) - - y = logY - - # TODO check if need more filtering of error (e.g., clip to positive) - - # TODO check and improve this - if (len(color) == 4 and - type(color[3]) in [type(1), numpy.uint8, numpy.int8]): - color = numpy.array(color, dtype=numpy.float32) / 255. - - if isinstance(color, numpy.ndarray) and color.ndim == 2: - colorArray = color - color = None - else: - colorArray = None - color = colors.rgba(color) - - if alpha < 1.: # Apply image transparency - if colorArray is not None and colorArray.shape[1] == 4: - # multiply alpha channel - colorArray[:, 3] = colorArray[:, 3] * alpha - if color is not None: - color = color[0], color[1], color[2], color[3] * alpha - - behaviors = set() - if selectable: - behaviors.add('selectable') - - curve = GLPlotCurve2D(x, y, colorArray, - xError=xerror, - yError=yerror, - lineStyle=linestyle, - lineColor=color, - lineWidth=linewidth, - marker=symbol, - markerColor=color, - markerSize=symbolsize, - fillColor=color if fill else None, - 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 legend, 'curve' - - def addImage(self, data, legend, - origin, scale, z, - selectable, draggable, - colormap, alpha): - for parameter in (data, legend, origin, scale, z, - selectable, draggable): - 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): - data = numpy.array(data, copy=False, order='C') - else: - _logger.info( - 'addImage: Convert %s data to float32', str(data.dtype)) - data = numpy.array(data, dtype=numpy.float32, order='C') - - colormapIsLog = colormap.getNormalization() == 'log' - cmapRange = colormap.getColormapRange(data=data) - colormapLut = colormap.getNColors(nbColors=256) - - image = GLPlotColormap(data, - origin, - scale, - colormapLut, - colormapIsLog, - cmapRange, - alpha) - image.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors - } - self._plotContent.add(image) - - elif len(data.shape) == 3: - # For RGB, RGBA data - assert data.shape[2] in (3, 4) - - if numpy.issubdtype(data.dtype, numpy.floating): - data = numpy.array(data, dtype=numpy.float32, copy=False) - elif numpy.issubdtype(data.dtype, numpy.integer): - data = numpy.array(data, dtype=numpy.uint8, copy=False) - else: - raise ValueError('Unsupported data type') - - 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' - - def addTriangles(self, x, y, triangles, legend, - color, z, selectable, alpha): - - # Handle axes log scale: convert data - if self._plotFrame.xAxis.isLog: - x = numpy.log10(x) - if self._plotFrame.yAxis.isLog: - 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' - - def addItem(self, x, y, legend, 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( - 'Cannot add item with X <= 0 with X axis log scale') - if self._plotFrame.yAxis.isLog and y.min() <= 0.: - 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 legend, 'marker' - - # Remove methods - - def remove(self, item): - legend, kind = item - - if kind == 'curve': - curve = self._plotContent.pop('curve', legend) - if curve is not None: - # 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') - 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) - - elif kind == 'marker': - self._markers.pop(legend, False) - - elif kind == 'item': - self._items.pop(legend, False) - - else: - _logger.error('Unsupported kind: %s', str(kind)) - - # Interaction methods - - _QT_CURSORS = { - BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor, - BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor, - BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor, - BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor, - BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor, - } - - def setGraphCursorShape(self, cursor): - if cursor is None: - super(BackendOpenGL, self).unsetCursor() - else: - cursor = self._QT_CURSORS[cursor] - super(BackendOpenGL, self).setCursor(qt.QCursor(cursor)) - - def setGraphCursor(self, flag, color, linewidth, linestyle): - if linestyle is not '-': - _logger.warning( - "BackendOpenGL.setGraphCursor linestyle parameter ignored") - - if flag: - color = colors.rgba(color) - crosshairCursor = color, linewidth - else: - crosshairCursor = None - - if crosshairCursor != self._crosshairCursor: - self._crosshairCursor = crosshairCursor - - _PICK_OFFSET = 3 # Offset in pixel used for picking - - def _mouseInPlotArea(self, x, y): - xPlot = numpy.clip( - x, self._plotFrame.margins.left, - self._plotFrame.size[0] - self._plotFrame.margins.right - 1) - yPlot = numpy.clip( - y, self._plotFrame.margins.top, - self._plotFrame.size[1] - self._plotFrame.margins.bottom - 1) - return xPlot, yPlot - - def __pickCurves(self, item, x, y): - """Perform picking on a curve item. - - :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] - """ - 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: - return [] - 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: - return [] - 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 - - # Apply log scale if axis is log - if self._plotFrame.xAxis.isLog: - xPickMin = numpy.log10(xPickMin) - xPickMax = numpy.log10(xPickMax) - - if (yAxis == 'left' and self._plotFrame.yAxis.isLog) or ( - yAxis == 'right' and self._plotFrame.y2Axis.isLog): - yPickMin = numpy.log10(yPickMin) - yPickMax = numpy.log10(yPickMax) - - 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 - - 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 - - # Update curve - - def setCurveColor(self, curve, color): - pass # TODO - - # Misc. - - def getWidgetHandle(self): - return self - - def postRedisplay(self): - self._sigPostRedisplay.emit() - - def replot(self): - self.update() # async redraw - # self.repaint() # immediate redraw - - def saveGraph(self, fileName, fileFormat, dpi): - if dpi is not None: - _logger.warning("saveGraph ignores dpi parameter") - - if fileFormat not in ['png', 'ppm', 'svg', 'tiff']: - raise NotImplementedError('Unsupported format: %s' % fileFormat) - - if not self.isValid(): - _logger.error('OpenGL 2.1 not available, cannot save OpenGL image') - width, height = self._plotFrame.size - data = numpy.zeros((height, width, 3), dtype=numpy.uint8) - else: - self.makeCurrent() - - data = numpy.empty( - (self._plotFrame.size[1], self._plotFrame.size[0], 3), - dtype=numpy.uint8, order='C') - - context = self.context() - framebufferTexture = self._plotFBOs.get(context) - if framebufferTexture is None: - # Fallback, supports direct rendering mode: _paintDirectGL - # might have issues as it can read on-screen framebuffer - fboName = self.defaultFramebufferObject() - width, height = self._plotFrame.size - else: - fboName = framebufferTexture.name - height, width = framebufferTexture.shape - - previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glReadPixels(0, 0, width, height, - gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer) - - # glReadPixels gives bottom to top, - # while images are stored as top to bottom - data = numpy.flipud(data) - - # fileName is either a file-like object or a str - saveImageToFile(data, fileName, fileFormat) - - # Graph labels - - def setGraphTitle(self, title): - self._plotFrame.title = title - - def setGraphXLabel(self, label): - self._plotFrame.xAxis.title = label - - def setGraphYLabel(self, label, axis): - if axis == 'left': - self._plotFrame.yAxis.title = label - else: # right axis - if label: - _logger.warning('Right axis label not implemented') - - # Graph limits - - def _setDataRanges(self, xlim=None, ylim=None, y2lim=None): - """Set the visible range of data in the plot frame. - - This clips the ranges to possible values (takes care of float32 - range + positive range for log). - This also takes care of non-orthogonal axes. - - This should be moved to PlotFrame. - """ - # Update axes range with a clipped range if too wide - self._plotFrame.setDataRanges(xlim, ylim, y2lim) - - def _ensureAspectRatio(self, keepDim=None): - """Update plot bounds in order to keep aspect ratio. - - Warning: keepDim on right Y axis is not implemented ! - - :param str keepDim: The dimension to maintain: 'x', 'y' or None. - If None (the default), the dimension with the largest range. - """ - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - if plotWidth <= 2 or plotHeight <= 2: - 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_) - - plotRatio = plotWidth / float(plotHeight) # Test != 0 before - - keepDim = 'x' if dataRatio > plotRatio else 'y' - else: # Limit case - keepDim = 'x' - - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \ - self._plotFrame.dataRanges - if keepDim == 'y': - dataW = (yMax - yMin) * plotWidth / float(plotHeight) - xCenter = 0.5 * (xMin + xMax) - xMin = xCenter - 0.5 * dataW - xMax = xCenter + 0.5 * dataW - elif keepDim == 'x': - dataH = (xMax - xMin) * plotHeight / float(plotWidth) - yCenter = 0.5 * (yMin + yMax) - yMin = yCenter - 0.5 * dataH - yMax = yCenter + 0.5 * dataH - y2Center = 0.5 * (y2Min + y2Max) - y2Min = y2Center - 0.5 * dataH - y2Max = y2Center + 0.5 * dataH - else: - raise RuntimeError('Unsupported dimension to keep: %s' % keepDim) - - # Update plot frame bounds - self._setDataRanges(xlim=(xMin, xMax), - ylim=(yMin, yMax), - y2lim=(y2Min, y2Max)) - - def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None, - keepDim=None): - # Update axes range with a clipped range if too wide - self._setDataRanges(xlim=xRange, - ylim=yRange, - y2lim=y2Range) - - # Keep data aspect ratio - if self.isKeepDataAspectRatio(): - self._ensureAspectRatio(keepDim) - - def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): - assert xmin < xmax - assert ymin < ymax - - if y2min is None or y2max is None: - y2Range = None - else: - assert y2min < y2max - y2Range = y2min, y2max - self._setPlotBounds((xmin, xmax), (ymin, ymax), y2Range) - - def getGraphXLimits(self): - return self._plotFrame.dataRanges.x - - def setGraphXLimits(self, xmin, xmax): - assert xmin < xmax - self._setPlotBounds(xRange=(xmin, xmax), keepDim='x') - - def getGraphYLimits(self, axis): - assert axis in ("left", "right") - if axis == "left": - return self._plotFrame.dataRanges.y - else: - return self._plotFrame.dataRanges.y2 - - def setGraphYLimits(self, ymin, ymax, axis): - assert ymin < ymax - assert axis in ("left", "right") - - if axis == "left": - self._setPlotBounds(yRange=(ymin, ymax), keepDim='y') - else: - self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y') - - # Graph axes - - def getXAxisTimeZone(self): - return self._plotFrame.xAxis.timeZone - - def setXAxisTimeZone(self, tz): - self._plotFrame.xAxis.timeZone = tz - - def isXAxisTimeSeries(self): - return self._plotFrame.xAxis.isTimeSeries - - def setXAxisTimeSeries(self, isTimeSeries): - self._plotFrame.xAxis.isTimeSeries = isTimeSeries - - def setXAxisLogarithmic(self, flag): - if flag != self._plotFrame.xAxis.isLog: - if flag and self._keepDataAspectRatio: - _logger.warning( - "KeepDataAspectRatio is ignored with log axes") - - self._plotFrame.xAxis.isLog = flag - - def setYAxisLogarithmic(self, flag): - if (flag != self._plotFrame.yAxis.isLog or - flag != self._plotFrame.y2Axis.isLog): - if flag and self._keepDataAspectRatio: - _logger.warning( - "KeepDataAspectRatio is ignored with log axes") - - self._plotFrame.yAxis.isLog = flag - self._plotFrame.y2Axis.isLog = flag - - def setYAxisInverted(self, flag): - if flag != self._plotFrame.isYAxisInverted: - self._plotFrame.isYAxisInverted = flag - - def isYAxisInverted(self): - return self._plotFrame.isYAxisInverted - - def isKeepDataAspectRatio(self): - if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog: - return False - else: - return self._keepDataAspectRatio - - def setKeepDataAspectRatio(self, flag): - if flag and (self._plotFrame.xAxis.isLog or - self._plotFrame.yAxis.isLog): - _logger.warning("KeepDataAspectRatio is ignored with log axes") - - self._keepDataAspectRatio = flag - - def setGraphGrid(self, which): - assert which in (None, 'major', 'both') - self._plotFrame.grid = which is not None # TODO True grid support - - # 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 - - return self._plotFrame.pixelToData(x, y, axis) - - def getPlotBoundsInPixels(self): - return self._plotFrame.plotOrigin + self._plotFrame.plotSize - - def setAxesDisplayed(self, displayed): - BackendBase.BackendBase.setAxesDisplayed(self, displayed) - self._plotFrame.displayed = displayed - - def setForegroundColors(self, foregroundColor, gridColor): - self._plotFrame.foregroundColor = foregroundColor - self._plotFrame.gridColor = gridColor - - def setBackgroundColors(self, backgroundColor, dataBackgroundColor): - self._backgroundColor = backgroundColor - self._dataBackgroundColor = dataBackgroundColor |