diff options
Diffstat (limited to 'silx/gui/plot/backends/glutils/GLPlotFrame.py')
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotFrame.py | 1219 |
1 files changed, 0 insertions, 1219 deletions
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py deleted file mode 100644 index c5ee75b..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotFrame.py +++ /dev/null @@ -1,1219 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# 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 -# 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. -# -# ############################################################################*/ -""" -This modules provides the rendering of plot titles, axes and grid. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -# TODO -# keep aspect ratio managed here? -# smarter dirty flag handling? - -import datetime as dt -import math -import weakref -import logging -from collections import namedtuple - -import numpy - -from ...._glutils import gl, Program -from ..._utils import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX -from .GLSupport import mat4Ortho -from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270 -from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10 -from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString -from ..._utils.dtime_ticklayout import timestamp - -_logger = logging.getLogger(__name__) - - -# PlotAxis #################################################################### - -class PlotAxis(object): - """Represents a 1D axis of the plot. - This class is intended to be used with :class:`GLPlotFrame`. - """ - - def __init__(self, plotFrame, - tickLength=(0., 0.), - foregroundColor=(0., 0., 0., 1.0), - labelAlign=CENTER, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=CENTER, - titleRotate=0, titleOffset=(0., 0.)): - self._ticks = None - - self._plotFrameRef = weakref.ref(plotFrame) - - self._isDateTime = False - self._timeZone = None - self._isLog = False - self._dataRange = 1., 100. - self._displayCoords = (0., 0.), (1., 0.) - self._title = '' - - self._tickLength = tickLength - self._foregroundColor = foregroundColor - self._labelAlign = labelAlign - self._labelVAlign = labelVAlign - self._titleAlign = titleAlign - self._titleVAlign = titleVAlign - self._titleRotate = titleRotate - self._titleOffset = titleOffset - - @property - def dataRange(self): - """The range of the data represented on the axis as a tuple - of 2 floats: (min, max).""" - return self._dataRange - - @dataRange.setter - def dataRange(self, dataRange): - assert len(dataRange) == 2 - assert dataRange[0] <= dataRange[1] - dataRange = float(dataRange[0]), float(dataRange[1]) - - if dataRange != self._dataRange: - self._dataRange = dataRange - self._dirtyTicks() - - @property - def isLog(self): - """Whether the axis is using a log10 scale or not as a bool.""" - return self._isLog - - @isLog.setter - def isLog(self, isLog): - isLog = bool(isLog) - if isLog != self._isLog: - self._isLog = isLog - self._dirtyTicks() - - @property - def timeZone(self): - """Returnss datetime.tzinfo that is used if this axis plots date times.""" - return self._timeZone - - @timeZone.setter - def timeZone(self, tz): - """Sets dateetime.tzinfo that is used if this axis plots date times.""" - self._timeZone = tz - self._dirtyTicks() - - @property - def isTimeSeries(self): - """Whether the axis is showing floats as datetime objects""" - return self._isDateTime - - @isTimeSeries.setter - def isTimeSeries(self, isTimeSeries): - isTimeSeries = bool(isTimeSeries) - if isTimeSeries != self._isDateTime: - self._isDateTime = isTimeSeries - self._dirtyTicks() - - @property - def displayCoords(self): - """The coordinates of the start and end points of the axis - in display space (i.e., in pixels) as a tuple of 2 tuples of - 2 floats: ((x0, y0), (x1, y1)). - """ - return self._displayCoords - - @displayCoords.setter - def displayCoords(self, displayCoords): - assert len(displayCoords) == 2 - assert len(displayCoords[0]) == 2 - assert len(displayCoords[1]) == 2 - displayCoords = tuple(displayCoords[0]), tuple(displayCoords[1]) - if displayCoords != self._displayCoords: - self._displayCoords = displayCoords - self._dirtyTicks() - - @property - def devicePixelRatio(self): - """Returns the ratio between qt pixels and device pixels.""" - plotFrame = self._plotFrameRef() - return plotFrame.devicePixelRatio if plotFrame is not None else 1. - - @property - def title(self): - """The text label associated with this axis as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirtyPlotFrame() - - @property - def titleOffset(self): - """Title offset in pixels (x: int, y: int)""" - return self._titleOffset - - @titleOffset.setter - def titleOffset(self, offset): - if offset != self._titleOffset: - self._titleOffset = offset - self._dirtyTicks() - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._foregroundColor = color - self._dirtyTicks() - - @property - def ticks(self): - """Ticks as tuples: ((x, y) in display, dataPos, textLabel).""" - if self._ticks is None: - self._ticks = tuple(self._ticksGenerator()) - return self._ticks - - def getVerticesAndLabels(self): - """Create the list of vertices for axis and associated text labels. - - :returns: A tuple: List of 2D line vertices, List of Text2D labels. - """ - vertices = list(self.displayCoords) # Add start and end points - labels = [] - tickLabelsSize = [0., 0.] - - xTickLength, yTickLength = self._tickLength - xTickLength *= self.devicePixelRatio - yTickLength *= self.devicePixelRatio - for (xPixel, yPixel), dataPos, text in self.ticks: - if text is None: - tickScale = 0.5 - else: - tickScale = 1. - - label = Text2D(text=text, - color=self._foregroundColor, - x=xPixel - xTickLength, - y=yPixel - yTickLength, - align=self._labelAlign, - valign=self._labelVAlign, - devicePixelRatio=self.devicePixelRatio) - - width, height = label.size - if width > tickLabelsSize[0]: - tickLabelsSize[0] = width - if height > tickLabelsSize[1]: - tickLabelsSize[1] = height - - labels.append(label) - - vertices.append((xPixel, yPixel)) - vertices.append((xPixel + tickScale * xTickLength, - yPixel + tickScale * yTickLength)) - - (x0, y0), (x1, y1) = self.displayCoords - xAxisCenter = 0.5 * (x0 + x1) - yAxisCenter = 0.5 * (y0 + y1) - - xOffset, yOffset = self.titleOffset - - # Adaptative title positioning: - # tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2) - # xOffset = -tickLabelsSize[0] * xTickLength / tickNorm - # xOffset -= 3 * xTickLength - # yOffset = -tickLabelsSize[1] * yTickLength / tickNorm - # yOffset -= 3 * yTickLength - - axisTitle = Text2D(text=self.title, - color=self._foregroundColor, - x=xAxisCenter + xOffset, - y=yAxisCenter + yOffset, - align=self._titleAlign, - valign=self._titleVAlign, - rotate=self._titleRotate, - devicePixelRatio=self.devicePixelRatio) - labels.append(axisTitle) - - return vertices, labels - - def _dirtyPlotFrame(self): - """Dirty parent GLPlotFrame""" - plotFrame = self._plotFrameRef() - if plotFrame is not None: - plotFrame._dirty() - - def _dirtyTicks(self): - """Mark ticks as dirty and notify listener (i.e., background).""" - self._ticks = None - self._dirtyPlotFrame() - - @staticmethod - def _frange(start, stop, step): - """range for float (including stop).""" - while start <= stop: - yield start - start += step - - def _ticksGenerator(self): - """Generator of ticks as tuples: - ((x, y) in display, dataPos, textLabel). - """ - dataMin, dataMax = self.dataRange - if self.isLog and dataMin <= 0.: - _logger.warning( - 'Getting ticks while isLog=True and dataRange[0]<=0.') - dataMin = 1. - if dataMax < dataMin: - dataMax = 1. - - if dataMin != dataMax: # data range is not null - (x0, y0), (x1, y1) = self.displayCoords - - if self.isLog: - - if self.isTimeSeries: - _logger.warning("Time series not implemented for log-scale") - - logMin, logMax = math.log10(dataMin), math.log10(dataMax) - tickMin, tickMax, step, _ = niceNumbersForLog10(logMin, logMax) - - xScale = (x1 - x0) / (logMax - logMin) - yScale = (y1 - y0) / (logMax - logMin) - - for logPos in self._frange(tickMin, tickMax, step): - if logMin <= logPos <= logMax: - dataPos = 10 ** logPos - xPixel = x0 + (logPos - logMin) * xScale - yPixel = y0 + (logPos - logMin) * yScale - text = '1e%+03d' % logPos - yield ((xPixel, yPixel), dataPos, text) - - if step == 1: - ticks = list(self._frange(tickMin, tickMax, step))[:-1] - for logPos in ticks: - dataOrigPos = 10 ** logPos - for index in range(2, 10): - dataPos = dataOrigPos * index - if dataMin <= dataPos <= dataMax: - logSubPos = math.log10(dataPos) - xPixel = x0 + (logSubPos - logMin) * xScale - yPixel = y0 + (logSubPos - logMin) * yScale - yield ((xPixel, yPixel), dataPos, None) - - else: - xScale = (x1 - x0) / (dataMax - dataMin) - yScale = (y1 - y0) / (dataMax - dataMin) - - nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio - - # Density of 1.3 label per 92 pixels - # i.e., 1.3 label per inch on a 92 dpi screen - tickDensity = 1.3 / 92 - - if not self.isTimeSeries: - tickMin, tickMax, step, nbFrac = niceNumbersAdaptative( - dataMin, dataMax, nbPixels, tickDensity) - - for dataPos in self._frange(tickMin, tickMax, step): - if dataMin <= dataPos <= dataMax: - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - if nbFrac == 0: - text = '%g' % dataPos - else: - text = ('%.' + str(nbFrac) + 'f') % dataPos - yield ((xPixel, yPixel), dataPos, text) - else: - # Time series - dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) - dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) - - tickDateTimes, spacing, unit = calcTicksAdaptive( - dtMin, dtMax, nbPixels, tickDensity) - - for tickDateTime in tickDateTimes: - if dtMin <= tickDateTime <= dtMax: - - dataPos = timestamp(tickDateTime) - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - fmtStr = bestFormatString(spacing, unit) - text = tickDateTime.strftime(fmtStr) - - yield ((xPixel, yPixel), dataPos, text) - - -# GLPlotFrame ################################################################# - -class GLPlotFrame(object): - """Base class for rendering a 2D frame surrounded by axes.""" - - _TICK_LENGTH_IN_PIXELS = 5 - _LINE_WIDTH = 1 - - _SHADERS = { - 'vertex': """ - attribute vec2 position; - uniform mat4 matrix; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragment': """ - uniform vec4 color; - uniform float tickFactor; /* = 1./tickLength or 0. for solid line */ - - void main(void) { - if (mod(tickFactor * (gl_FragCoord.x + gl_FragCoord.y), 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - } - - _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom')) - - # Margins used when plot frame is not displayed - _NoDisplayMargins = _Margins(0, 0, 0, 0) - - def __init__(self, marginRatios, foregroundColor, gridColor): - """ - :param List[float] marginRatios: - The ratios of margins around plot area for axis and labels. - (left, top, right, bottom) as float in [0., 1.] - :param foregroundColor: color used for the frame and labels. - :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0 - :param gridColor: color used for grid lines. - :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0 - """ - self._renderResources = None - - self.__marginRatios = marginRatios - self.__marginsCache = None - - self._foregroundColor = foregroundColor - self._gridColor = gridColor - - self.axes = [] # List of PlotAxis to be updated by subclasses - - self._grid = False - self._size = 0., 0. - self._title = '' - - self._devicePixelRatio = 1. - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return self._renderResources is None - - GRID_NONE = 0 - GRID_MAIN_TICKS = 1 - GRID_SUB_TICKS = 2 - GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS) - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._foregroundColor = color - for axis in self.axes: - axis.foregroundColor = color - self._dirty() - - @property - def gridColor(self): - """Color used for frame and labels""" - return self._gridColor - - @gridColor.setter - def gridColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "gridColor must have length 4, got {}".format(len(self._gridColor)) - if self._gridColor != color: - self._gridColor = color - self._dirty() - - @property - def marginRatios(self): - """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1]. - """ - return self.__marginRatios - - @marginRatios.setter - def marginRatios(self, ratios): - ratios = tuple(float(v) for v in ratios) - assert len(ratios) == 4 - for value in ratios: - assert 0. <= value <= 1. - assert ratios[0] + ratios[2] < 1. - assert ratios[1] + ratios[3] < 1. - - if self.__marginRatios != ratios: - self.__marginRatios = ratios - self.__marginsCache = None # Clear cached margins - self._dirty() - - @property - def margins(self): - """Margins in pixels around the plot.""" - if self.__marginsCache is None: - width, height = self.size - left, top, right, bottom = self.marginRatios - self.__marginsCache = self._Margins( - left=int(left*width), - right=int(right*width), - top=int(top*height), - bottom=int(bottom*height)) - return self.__marginsCache - - @property - def devicePixelRatio(self): - return self._devicePixelRatio - - @devicePixelRatio.setter - def devicePixelRatio(self, ratio): - if ratio != self._devicePixelRatio: - self._devicePixelRatio = ratio - self._dirty() - - @property - def grid(self): - """Grid display mode: - - 0: No grid. - - 1: Grid on main ticks. - - 2: Grid on sub-ticks for log scale axes. - - 3: Grid on main and sub ticks.""" - return self._grid - - @grid.setter - def grid(self, grid): - assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS, - self.GRID_SUB_TICKS, self.GRID_ALL_TICKS) - if grid != self._grid: - self._grid = grid - self._dirty() - - @property - def size(self): - """Size in device pixels of the plot area including margins.""" - return self._size - - @size.setter - def size(self, size): - assert len(size) == 2 - size = tuple(size) - if size != self._size: - self._size = size - self.__marginsCache = None # Clear cached margins - self._dirty() - - @property - def plotOrigin(self): - """Plot area origin (left, top) in widget coordinates in pixels.""" - return self.margins.left, self.margins.top - - @property - def plotSize(self): - """Plot area size (width, height) in pixels.""" - w, h = self.size - w -= self.margins.left + self.margins.right - h -= self.margins.top + self.margins.bottom - return w, h - - @property - def title(self): - """Main title as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirty() - - # In-place update - # if self._renderResources is not None: - # self._renderResources[-1][-1].text = title - - def _dirty(self): - # When Text2D require discard we need to handle it - self._renderResources = None - - def _buildGridVertices(self): - if self._grid == self.GRID_NONE: - return [] - - elif self._grid == self.GRID_MAIN_TICKS: - def test(text): - return text is not None - elif self._grid == self.GRID_SUB_TICKS: - def test(text): - return text is None - elif self._grid == self.GRID_ALL_TICKS: - def test(_): - return True - else: - logging.warning('Wrong grid mode: %d' % self._grid) - return [] - - return self._buildGridVerticesWithTest(test) - - def _buildGridVerticesWithTest(self, test): - """Override in subclass to generate grid vertices""" - return [] - - def _buildVerticesAndLabels(self): - # To fill with copy of axes lists - vertices = [] - labels = [] - - for axis in self.axes: - axisVertices, axisLabels = axis.getVerticesAndLabels() - vertices += axisVertices - labels += axisLabels - - vertices = numpy.array(vertices, dtype=numpy.float32) - - # Add main title - xTitle = (self.size[0] + self.margins.left - - self.margins.right) // 2 - yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS - labels.append(Text2D(text=self.title, - color=self._foregroundColor, - x=xTitle, - y=yTitle, - align=CENTER, - valign=BOTTOM, - devicePixelRatio=self.devicePixelRatio)) - - # grid - gridVertices = numpy.array(self._buildGridVertices(), - dtype=numpy.float32) - - self._renderResources = (vertices, gridVertices, labels) - - _program = Program( - _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position') - - def render(self): - if self.margins == self._NoDisplayMargins: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - for label in labels: - label.render(matProj) - - def renderGrid(self): - if self._grid == self.GRID_NONE: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], *self._gridColor) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, gridVertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices)) - - -# GLPlotFrame2D ############################################################### - -class GLPlotFrame2D(GLPlotFrame): - def __init__(self, marginRatios, foregroundColor, gridColor): - """ - :param List[float] marginRatios: - The ratios of margins around plot area for axis and labels. - (left, top, right, bottom) as float in [0., 1.] - :param foregroundColor: color used for the frame and labels. - :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0 - :param gridColor: color used for grid lines. - :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0 - - """ - super(GLPlotFrame2D, self).__init__(marginRatios, foregroundColor, gridColor) - self.axes.append(PlotAxis(self, - tickLength=(0., -5.), - foregroundColor=self._foregroundColor, - labelAlign=CENTER, labelVAlign=TOP, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=0)) - - self._x2AxisCoords = () - - self.axes.append(PlotAxis(self, - tickLength=(5., 0.), - foregroundColor=self._foregroundColor, - labelAlign=RIGHT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=BOTTOM, - titleRotate=ROTATE_270)) - - self._y2Axis = PlotAxis(self, - tickLength=(-5., 0.), - foregroundColor=self._foregroundColor, - labelAlign=LEFT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=ROTATE_270) - - self._isYAxisInverted = False - - self._dataRanges = { - 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)} - - self._baseVectors = (1., 0.), (0., 1.) - - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - def _dirty(self): - super(GLPlotFrame2D, self)._dirty() - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return (super(GLPlotFrame2D, self).isDirty or - self._transformedDataRanges is None or - self._transformedDataProjMat is None or - self._transformedDataY2ProjMat is None) - - @property - def xAxis(self): - return self.axes[0] - - @property - def yAxis(self): - return self.axes[1] - - @property - def y2Axis(self): - return self._y2Axis - - @property - def isY2Axis(self): - """Whether to display the left Y axis or not.""" - return len(self.axes) == 3 - - @isY2Axis.setter - def isY2Axis(self, isY2Axis): - if isY2Axis != self.isY2Axis: - if isY2Axis: - self.axes.append(self._y2Axis) - else: - self.axes = self.axes[:2] - - self._dirty() - - @property - def isYAxisInverted(self): - """Whether Y axes are inverted or not as a bool.""" - return self._isYAxisInverted - - @isYAxisInverted.setter - def isYAxisInverted(self, value): - value = bool(value) - if value != self._isYAxisInverted: - self._isYAxisInverted = value - self._dirty() - - DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.) - """Values of baseVectors for orthogonal axes.""" - - @property - def baseVectors(self): - """Coordinates of the X and Y axes in the orthogonal plot coords. - - Raises ValueError if corresponding matrix is singular. - - 2 tuples of 2 floats: (xx, xy), (yx, yy) - """ - return self._baseVectors - - @baseVectors.setter - def baseVectors(self, baseVectors): - self._dirty() - - (xx, xy), (yx, yy) = baseVectors - vectors = (float(xx), float(xy)), (float(yx), float(yy)) - - det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]) - if det == 0.: - raise ValueError("Singular matrix for base vectors: " + - str(vectors)) - - if vectors != self._baseVectors: - self._baseVectors = vectors - self._dirty() - - def _updateTitleOffset(self): - """Update axes title offset according to margins""" - margins = self.margins - self.xAxis.titleOffset = 0, margins.bottom // 2 - self.yAxis.titleOffset = -3 * margins.left // 4, 0 - self.y2Axis.titleOffset = 3 * margins.right // 4, 0 - - # Override size and marginRatios setters to update titleOffsets - @GLPlotFrame.size.setter - def size(self, size): - GLPlotFrame.size.fset(self, size) - self._updateTitleOffset() - - @GLPlotFrame.marginRatios.setter - def marginRatios(self, ratios): - GLPlotFrame.marginRatios.fset(self, ratios) - self._updateTitleOffset() - - @property - def dataRanges(self): - """Ranges of data visible in the plot on x, y and y2 axes. - - This is different to the axes range when axes are not orthogonal. - - Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - """ - return self._DataRanges(self._dataRanges['x'], - self._dataRanges['y'], - self._dataRanges['y2']) - - @staticmethod - def _clipToSafeRange(min_, max_, isLog): - # Clip range if needed - minLimit = FLOAT32_MINPOS if isLog else FLOAT32_SAFE_MIN - min_ = numpy.clip(min_, minLimit, FLOAT32_SAFE_MAX) - max_ = numpy.clip(max_, minLimit, FLOAT32_SAFE_MAX) - assert min_ < max_ - return min_, max_ - - def setDataRanges(self, x=None, y=None, y2=None): - """Set data range over each axes. - - The provided ranges are clipped to possible values - (i.e., 32 float range + positive range for log scale). - - :param x: (min, max) data range over X axis - :param y: (min, max) data range over Y axis - :param y2: (min, max) data range over Y2 axis - """ - if x is not None: - self._dataRanges['x'] = \ - self._clipToSafeRange(x[0], x[1], self.xAxis.isLog) - - if y is not None: - self._dataRanges['y'] = \ - self._clipToSafeRange(y[0], y[1], self.yAxis.isLog) - - if y2 is not None: - self._dataRanges['y2'] = \ - self._clipToSafeRange(y2[0], y2[1], self.y2Axis.isLog) - - self.xAxis.dataRange = self._dataRanges['x'] - self.yAxis.dataRange = self._dataRanges['y'] - self.y2Axis.dataRange = self._dataRanges['y2'] - - _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2')) - - @property - def transformedDataRanges(self): - """Bounds of the displayed area in transformed data coordinates - (i.e., log scale applied if any as well as skew) - - 3-tuple of 2-tuple (min, max) for each axis: x, y, y2. - """ - if self._transformedDataRanges is None: - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self.dataRanges - - if self.xAxis.isLog: - try: - xMin = math.log10(xMin) - except ValueError: - _logger.info('xMin: warning log10(%f)', xMin) - xMin = 0. - try: - xMax = math.log10(xMax) - except ValueError: - _logger.info('xMax: warning log10(%f)', xMax) - xMax = 0. - - if self.yAxis.isLog: - try: - yMin = math.log10(yMin) - except ValueError: - _logger.info('yMin: warning log10(%f)', yMin) - yMin = 0. - try: - yMax = math.log10(yMax) - except ValueError: - _logger.info('yMax: warning log10(%f)', yMax) - yMax = 0. - - try: - y2Min = math.log10(y2Min) - except ValueError: - _logger.info('yMin: warning log10(%f)', y2Min) - y2Min = 0. - try: - y2Max = math.log10(y2Max) - except ValueError: - _logger.info('yMax: warning log10(%f)', y2Max) - y2Max = 0. - - self._transformedDataRanges = self._DataRanges( - (xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - - return self._transformedDataRanges - - @property - def transformedDataProjMat(self): - """Orthographic projection matrix for rendering transformed data - - :type: numpy.matrix - """ - if self._transformedDataProjMat is None: - xMin, xMax = self.transformedDataRanges.x - yMin, yMax = self.transformedDataRanges.y - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1) - self._transformedDataProjMat = mat - - return self._transformedDataProjMat - - @property - def transformedDataY2ProjMat(self): - """Orthographic projection matrix for rendering transformed data - for the 2nd Y axis - - :type: numpy.matrix - """ - if self._transformedDataY2ProjMat is None: - xMin, xMax = self.transformedDataRanges.x - y2Min, y2Max = self.transformedDataRanges.y2 - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1) - self._transformedDataY2ProjMat = mat - - return self._transformedDataY2ProjMat - - def dataToPixel(self, x, y, axis='left'): - """Convert data coordinate to widget pixel coordinate. - """ - assert axis in ('left', 'right') - - trBounds = self.transformedDataRanges - - if self.xAxis.isLog: - if x < FLOAT32_MINPOS: - return None - xDataTr = math.log10(x) - else: - xDataTr = x - - if self.yAxis.isLog: - if y < FLOAT32_MINPOS: - return None - yDataTr = math.log10(y) - else: - yDataTr = y - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - coords = numpy.dot(skew_mat, numpy.array((xDataTr, yDataTr))) - xDataTr, yDataTr = coords - - plotWidth, plotHeight = self.plotSize - - xPixel = int(self.margins.left + - plotWidth * (xDataTr - trBounds.x[0]) / - (trBounds.x[1] - trBounds.x[0])) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - yOffset = (plotHeight * (yDataTr - usedAxis[0]) / - (usedAxis[1] - usedAxis[0])) - - if self.isYAxisInverted: - yPixel = int(self.margins.top + yOffset) - else: - yPixel = int(self.size[1] - self.margins.bottom - yOffset) - - return xPixel, yPixel - - def pixelToData(self, x, y, axis="left"): - """Convert pixel position to data coordinates. - - :param float x: X coord - :param float y: Y coord - :param str axis: Y axis to use in ('left', 'right') - :return: (x, y) position in data coords - """ - assert axis in ("left", "right") - - plotWidth, plotHeight = self.plotSize - - trBounds = self.transformedDataRanges - - xData = (x - self.margins.left + 0.5) / float(plotWidth) - xData = trBounds.x[0] + xData * (trBounds.x[1] - trBounds.x[0]) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - if self.isYAxisInverted: - yData = (y - self.margins.top + 0.5) / float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - else: - yData = self.size[1] - self.margins.bottom - y - 0.5 - yData /= float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - - # non-orthogonal axis - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - skew_mat = numpy.linalg.inv(skew_mat) - - coords = numpy.dot(skew_mat, numpy.array((xData, yData))) - xData, yData = coords - - if self.xAxis.isLog: - xData = pow(10, xData) - if self.yAxis.isLog: - yData = pow(10, yData) - - return xData, yData - - def _buildGridVerticesWithTest(self, test): - vertices = [] - - if self.baseVectors == self.DEFAULT_BASE_VECTORS: - for axis in self.axes: - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - vertices.append((xPixel, yPixel)) - if axis == self.xAxis: - vertices.append((xPixel, self.margins.top)) - elif axis == self.yAxis: - vertices.append((self.size[0] - self.margins.right, - yPixel)) - else: # axis == self.y2Axis - vertices.append((self.margins.left, yPixel)) - - else: - # Get plot corners in data coords - plotLeft, plotTop = self.plotOrigin - plotWidth, plotHeight = self.plotSize - - corners = [(plotLeft, plotTop), - (plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop)] - - for axis in self.axes: - if axis == self.xAxis: - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ((cornersInData[0], cornersInData[3]), # top - (cornersInData[1], cornersInData[0]), # left - (cornersInData[3], cornersInData[2])) # right - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(x0, x1) <= data < max(x0, x1): - yIntersect = (data - x0) * \ - (y1 - y0) / (x1 - x0) + y0 - - pixelPos = self.dataToPixel( - data, yIntersect) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - else: # y or y2 axes - if axis == self.yAxis: - axis_name = 'left' - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ( - (cornersInData[3], cornersInData[2]), # right - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - else: # axis == self.y2Axis - axis_name = 'right' - corners = numpy.array([self.pixelToData( - x, y, axis='right') for (x, y) in corners]) - borders = ( - (cornersInData[1], cornersInData[0]), # left - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(y0, y1) <= data < max(y0, y1): - xIntersect = (data - y0) * \ - (x1 - x0) / (y1 - y0) + x0 - - pixelPos = self.dataToPixel( - xIntersect, data, axis=axis_name) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - return vertices - - def _buildVerticesAndLabels(self): - width, height = self.size - - xCoords = (self.margins.left - 0.5, - width - self.margins.right + 0.5) - yCoords = (height - self.margins.bottom + 0.5, - self.margins.top - 0.5) - - self.axes[0].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[1], yCoords[0])) - - self._x2AxisCoords = ((xCoords[0], yCoords[1]), - (xCoords[1], yCoords[1])) - - if self.isYAxisInverted: - # Y axes are inverted, axes coordinates are inverted - yCoords = yCoords[1], yCoords[0] - - self.axes[1].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[0], yCoords[1])) - - self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]), - (xCoords[1], yCoords[1])) - - super(GLPlotFrame2D, self)._buildVerticesAndLabels() - - vertices, gridVertices, labels = self._renderResources - - # Adds vertices for borders without axis - extraVertices = [] - extraVertices += self._x2AxisCoords - if not self.isY2Axis: - extraVertices += self._y2Axis.displayCoords - - extraVertices = numpy.array( - extraVertices, copy=False, dtype=numpy.float32) - vertices = numpy.append(vertices, extraVertices, axis=0) - - self._renderResources = (vertices, gridVertices, labels) - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._y2Axis.foregroundColor = color - GLPlotFrame.foregroundColor.fset(self, color) # call parent property |