summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends/glutils/GLPlotFrame.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends/glutils/GLPlotFrame.py')
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py1219
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