summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
authorFrédéric-Emmanuel Picca <picca@debian.org>2018-08-08 14:09:02 +0200
committerFrédéric-Emmanuel Picca <picca@debian.org>2018-08-08 14:09:02 +0200
commit75271d5d9979b204baf8172b2a03da4330b14083 (patch)
tree22a2e31692654f1464c6fc8463cafe6598d9bdc4 /silx/gui/plot/backends
parent989033673e36f8d9959dd2d3a8285e5339bfae0c (diff)
parent302d3bcf3ef555284ce1bcf5cd7cd371addfa608 (diff)
Merge tag 'debian/0.8.0+dfsg-1' into debian/stretch-backports
silx release 0.8.0+dfsg-1 for unstable (sid) (maintainer view tag generated by dgit --quilt=gbp)
Diffstat (limited to 'silx/gui/plot/backends')
-rw-r--r--silx/gui/plot/backends/BackendBase.py43
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py238
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py254
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py1070
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py97
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py31
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py37
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py5
8 files changed, 957 insertions, 818 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index 45bf785..8352ea0 100644
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -31,8 +31,7 @@ This API is a simplified version of PyMca PlotBackend API.
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "16/08/2017"
-
+__date__ = "24/04/2018"
import weakref
from ... import qt
@@ -59,6 +58,7 @@ class BackendBase(object):
self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)}
self.__yAxisInverted = False
self.__keepDataAspectRatio = False
+ self._xAxisTimeZone = None
self._axesDisplayed = True
# Store a weakref to get access to the plot state.
self._setPlot(plot)
@@ -109,7 +109,7 @@ class BackendBase(object):
:param str legend: The legend to be associated to the curve
:param color: color(s) to be used
:type color: string ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in Colors.py
+ one of the predefined color names defined in colors.py
:param str symbol: Symbol to be drawn at each (x, y) position::
- ' ' or '' no symbol
@@ -252,7 +252,7 @@ class BackendBase(object):
:param bool flag: Toggle the display of a crosshair cursor.
:param color: The color to use for the crosshair.
- :type color: A string (either a predefined color name in Colors.py
+ :type color: A string (either a predefined color name in colors.py
or "#RRGGBB")) or a 4 columns unsigned byte array.
:param int linewidth: The width of the lines of the crosshair.
:param linestyle: Type of line::
@@ -406,6 +406,39 @@ class BackendBase(object):
# Graph axes
+
+ def getXAxisTimeZone(self):
+ """Returns tzinfo that is used if the X-Axis plots date-times.
+
+ None means the datetimes are interpreted as local time.
+
+ :rtype: datetime.tzinfo of None.
+ """
+ return self._xAxisTimeZone
+
+ def setXAxisTimeZone(self, tz):
+ """Sets tzinfo that is used if the X-Axis plots date-times.
+
+ Use None to let the datetimes be interpreted as local time.
+
+ :rtype: datetime.tzinfo of None.
+ """
+ self._xAxisTimeZone = tz
+
+ def isXAxisTimeSeries(self):
+ """Return True if the X-axis scale shows datetime objects.
+
+ :rtype: bool
+ """
+ raise NotImplementedError()
+
+ def setXAxisTimeSeries(self, isTimeSeries):
+ """Set whether the X-axis is a time series
+
+ :param bool flag: True to switch to time series, False for regular axis.
+ """
+ raise NotImplementedError()
+
def setXAxisLogarithmic(self, flag):
"""Set the X axis scale between linear and log.
@@ -503,4 +536,4 @@ class BackendBase(object):
are displayed and not the other.
This only check status set to axes from the public API
"""
- return self._axesDisplayed \ No newline at end of file
+ return self._axesDisplayed
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index f9a1fe5..49c4540 100644
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -32,9 +32,11 @@ __date__ = "18/10/2017"
import logging
-
+import datetime as dt
import numpy
+from pkg_resources import parse_version as _parse_version
+
_logger = logging.getLogger(__name__)
@@ -42,7 +44,6 @@ _logger = logging.getLogger(__name__)
from ... import qt
# First of all init matplotlib and set its backend
-from ..matplotlib import Colormap as MPLColormap
from ..matplotlib import FigureCanvasQTAgg
import matplotlib
from matplotlib.container import Container
@@ -52,10 +53,103 @@ from matplotlib.image import AxesImage
from matplotlib.backend_bases import MouseEvent
from matplotlib.lines import Line2D
from matplotlib.collections import PathCollection, LineCollection
+from matplotlib.ticker import Formatter, ScalarFormatter, Locator
+
+
from ..matplotlib.ModestImage import ModestImage
from . import BackendBase
from .._utils import FLOAT32_MINPOS
+from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
+
+
+
+class NiceDateLocator(Locator):
+ """
+ Matplotlib Locator that uses Nice Numbers algorithm (adapted to dates)
+ to find the tick locations. This results in the same number behaviour
+ as when using the silx Open GL backend.
+
+ Expects the data to be posix timestampes (i.e. seconds since 1970)
+ """
+ def __init__(self, numTicks=5, tz=None):
+ """
+ :param numTicks: target number of ticks
+ :param datetime.tzinfo tz: optional time zone. None is local time.
+ """
+ super(NiceDateLocator, self).__init__()
+ self.numTicks = numTicks
+
+ self._spacing = None
+ self._unit = None
+ self.tz = tz
+
+ @property
+ def spacing(self):
+ """ The current spacing. Will be updated when new tick value are made"""
+ return self._spacing
+
+ @property
+ def unit(self):
+ """ The current DtUnit. Will be updated when new tick value are made"""
+ return self._unit
+
+ def __call__(self):
+ """Return the locations of the ticks"""
+ vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+
+ def tick_values(self, vmin, vmax):
+ """ Calculates tick values
+ """
+ if vmax < vmin:
+ vmin, vmax = vmax, vmin
+
+ # vmin and vmax should be timestamps (i.e. seconds since 1 Jan 1970)
+ dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz)
+ dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz)
+ dtTicks, self._spacing, self._unit = \
+ calcTicks(dtMin, dtMax, self.numTicks)
+
+ # Convert datetime back to time stamps.
+ ticks = [timestamp(dtTick) for dtTick in dtTicks]
+ return ticks
+
+
+
+class NiceAutoDateFormatter(Formatter):
+ """
+ Matplotlib FuncFormatter that is linked to a NiceDateLocator and gives the
+ best possible formats given the locators current spacing an date unit.
+ """
+
+ def __init__(self, locator, tz=None):
+ """
+ :param niceDateLocator: a NiceDateLocator object
+ :param datetime.tzinfo tz: optional time zone. None is local time.
+ """
+ super(NiceAutoDateFormatter, self).__init__()
+ self.locator = locator
+ self.tz = tz
+
+ @property
+ def formatString(self):
+ if self.locator.spacing is None or self.locator.unit is None:
+ # Locator has no spacing or units yet. Return elaborate fmtString
+ return "Y-%m-%d %H:%M:%S"
+ else:
+ return bestFormatString(self.locator.spacing, self.locator.unit)
+
+
+ def __call__(self, x, pos=None):
+ """Return the format for tick val *x* at position *pos*
+ Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
+ """
+ dateTime = dt.datetime.fromtimestamp(x, tz=self.tz)
+ tickStr = dateTime.strftime(self.formatString)
+ return tickStr
+
+
class _MarkerContainer(Container):
@@ -130,6 +224,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
# when getting the limits at the expense of a replot
self._dirtyLimits = True
self._axesDisplayed = True
+ self._matplotlibVersion = _parse_version(matplotlib.__version__)
self.fig = Figure()
self.fig.set_facecolor("w")
@@ -153,7 +248,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax2.set_autoscaley_on(True)
self.ax.set_zorder(1)
# this works but the figure color is left
- if matplotlib.__version__[0] < '2':
+ if self._matplotlibVersion < _parse_version('2'):
self.ax.set_axis_bgcolor('none')
else:
self.ax.set_facecolor('none')
@@ -165,9 +260,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._colormaps = {}
self._graphCursor = tuple()
- self.matplotlibVersion = matplotlib.__version__
self._enableAxis('right', False)
+ self._isXAxisTimeSeries = False
# Add methods
@@ -235,7 +330,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
color=actualColor,
marker=symbol,
picker=picker,
- s=symbolsize)
+ s=symbolsize**2)
artists.append(scatter)
if fill:
@@ -286,7 +381,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
# No transparent colormap with matplotlib < 1.2.0
# Add support for transparent colormap for uint8 data with
# colormap with 256 colors, linear norm, [0, 255] range
- if matplotlib.__version__ < '1.2.0':
+ if self._matplotlibVersion < _parse_version('1.2.0'):
if (len(data.shape) == 2 and colormap.getName() is None and
colormap.getColormapLUT() is not None):
colors = colormap.getColormapLUT()
@@ -313,29 +408,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
imageClass = AxesImage
- # the normalization can be a source of time waste
- # Two possibilities, we receive data or a ready to show image
- if len(data.shape) == 3: # RGBA image
- image = imageClass(self.ax,
- label="__IMAGE__" + legend,
- interpolation='nearest',
- picker=picker,
- zorder=z,
- origin='lower')
+ # All image are shown as RGBA image
+ image = imageClass(self.ax,
+ label="__IMAGE__" + legend,
+ interpolation='nearest',
+ picker=picker,
+ zorder=z,
+ origin='lower')
- else:
- # Convert colormap argument to matplotlib colormap
- scalarMappable = MPLColormap.getScalarMappable(colormap, data)
-
- # try as data
- image = imageClass(self.ax,
- label="__IMAGE__" + legend,
- interpolation='nearest',
- cmap=scalarMappable.cmap,
- picker=picker,
- zorder=z,
- norm=scalarMappable.norm,
- origin='lower')
if alpha < 1:
image.set_alpha(alpha)
@@ -359,14 +439,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
ystep = 1 if scale[1] >= 0. else -1
data = data[::ystep, ::xstep]
- if matplotlib.__version__ < "2.1":
+ if self._matplotlibVersion < _parse_version('2.1'):
# matplotlib 1.4.2 do not support float128
dtype = data.dtype
if dtype.kind == "f" and dtype.itemsize >= 16:
_logger.warning("Your matplotlib version do not support "
- "float128. Data converted to floa64.")
+ "float128. Data converted to float64.")
data = data.astype(numpy.float64)
+ if data.ndim == 2: # Data image, convert to RGBA image
+ data = colormap.applyToData(data)
+
image.set_data(data)
self.ax.add_artist(image)
@@ -671,11 +754,39 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Graph axes
+ def setXAxisTimeZone(self, tz):
+ super(BackendMatplotlib, self).setXAxisTimeZone(tz)
+
+ # Make new formatter and locator with the time zone.
+ self.setXAxisTimeSeries(self.isXAxisTimeSeries())
+
+ def isXAxisTimeSeries(self):
+ return self._isXAxisTimeSeries
+
+ def setXAxisTimeSeries(self, isTimeSeries):
+ self._isXAxisTimeSeries = isTimeSeries
+ if self._isXAxisTimeSeries:
+ # We can't use a matplotlib.dates.DateFormatter because it expects
+ # the data to be in datetimes. Silx works internally with
+ # timestamps (floats).
+ locator = NiceDateLocator(tz=self.getXAxisTimeZone())
+ self.ax.xaxis.set_major_locator(locator)
+ self.ax.xaxis.set_major_formatter(
+ NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone()))
+ else:
+ try:
+ scalarFormatter = ScalarFormatter(useOffset=False)
+ except:
+ _logger.warning('Cannot disabled axes offsets in %s ' %
+ matplotlib.__version__)
+ scalarFormatter = ScalarFormatter()
+ self.ax.xaxis.set_major_formatter(scalarFormatter)
+
def setXAxisLogarithmic(self, flag):
# Workaround for matplotlib 2.1.0 when one tries to set an axis
# to log scale with both limits <= 0
# In this case a draw with positive limits is needed first
- if flag and matplotlib.__version__ >= '2.1.0':
+ if flag and self._matplotlibVersion >= _parse_version('2.1.0'):
xlim = self.ax.get_xlim()
if xlim[0] <= 0 and xlim[1] <= 0:
self.ax.set_xlim(1, 10)
@@ -685,15 +796,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.set_xscale('log' if flag else 'linear')
def setYAxisLogarithmic(self, flag):
- # Workaround for matplotlib 2.1.0 when one tries to set an axis
- # to log scale with both limits <= 0
- # In this case a draw with positive limits is needed first
- if flag and matplotlib.__version__ >= '2.1.0':
+ # Workaround for matplotlib 2.0 issue with negative bounds
+ # before switching to log scale
+ if flag and self._matplotlibVersion >= _parse_version('2.0.0'):
redraw = False
- for axis in (self.ax, self.ax2):
+ for axis, dataRangeIndex in ((self.ax, 1), (self.ax2, 2)):
ylim = axis.get_ylim()
- if ylim[0] <= 0 and ylim[1] <= 0:
- axis.set_ylim(1, 10)
+ if ylim[0] <= 0 or ylim[1] <= 0:
+ dataRange = self._plot.getDataRange()[dataRangeIndex]
+ if dataRange is None:
+ dataRange = 1, 100 # Fallback
+ axis.set_ylim(*dataRange)
redraw = True
if redraw:
self.draw()
@@ -722,16 +835,31 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Data <-> Pixel coordinates conversion
+ def _mplQtYAxisCoordConversion(self, y):
+ """Qt origin (top) to/from matplotlib origin (bottom) conversion.
+
+ :rtype: float
+ """
+ height = self.fig.get_window_extent().height
+ return height - y
+
def dataToPixel(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
pixels = ax.transData.transform_point((x, y))
xPixel, yPixel = pixels.T
+
+ # Convert from matplotlib origin (bottom) to Qt origin (top)
+ yPixel = self._mplQtYAxisCoordConversion(yPixel)
+
return xPixel, yPixel
def pixelToData(self, x, y, axis, check):
ax = self.ax2 if axis == "right" else self.ax
+ # Convert from Qt origin (top) to matplotlib origin (bottom)
+ y = self._mplQtYAxisCoordConversion(y)
+
inv = ax.transData.inverted()
x, y = inv.transform_point((x, y))
@@ -745,12 +873,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
return x, y
def getPlotBoundsInPixels(self):
- bbox = self.ax.get_window_extent().transformed(
- self.fig.dpi_scale_trans.inverted())
- dpi = self.fig.dpi
+ bbox = self.ax.get_window_extent()
# Warning this is not returning int...
- return (bbox.bounds[0] * dpi, bbox.bounds[1] * dpi,
- bbox.bounds[2] * dpi, bbox.bounds[3] * dpi)
+ return (bbox.xmin,
+ self._mplQtYAxisCoordConversion(bbox.ymax),
+ bbox.width,
+ bbox.height)
def setAxesDisplayed(self, displayed):
"""Display or not the axes.
@@ -822,7 +950,8 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def _onMousePress(self, event):
self._plot.onMousePress(
- event.x, event.y, self._MPL_TO_PLOT_BUTTONS[event.button])
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ self._MPL_TO_PLOT_BUTTONS[event.button])
def _onMouseMove(self, event):
if self._graphCursor:
@@ -839,14 +968,17 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self._plot._setDirtyPlot(overlayOnly=True)
# onMouseMove must trigger replot if dirty flag is raised
- self._plot.onMouseMove(event.x, event.y)
+ self._plot.onMouseMove(
+ event.x, self._mplQtYAxisCoordConversion(event.y))
def _onMouseRelease(self, event):
self._plot.onMouseRelease(
- event.x, event.y, self._MPL_TO_PLOT_BUTTONS[event.button])
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ self._MPL_TO_PLOT_BUTTONS[event.button])
def _onMouseWheel(self, event):
- self._plot.onMouseWheel(event.x, event.y, event.step)
+ self._plot.onMouseWheel(
+ event.x, self._mplQtYAxisCoordConversion(event.y), event.step)
def leaveEvent(self, event):
"""QWidget event handler"""
@@ -880,7 +1012,8 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self._picked = []
# Weird way to do an explicit picking: Simulate a button press event
- mouseEvent = MouseEvent('button_press_event', self, x, y)
+ mouseEvent = MouseEvent('button_press_event',
+ self, x, self._mplQtYAxisCoordConversion(y))
cid = self.mpl_connect('pick_event', self._onPick)
self.fig.pick(mouseEvent)
self.mpl_disconnect(cid)
@@ -924,7 +1057,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
"""
# Starting with mpl 2.1.0, toggling autoscale raises a ValueError
# in some situations. See #1081, #1136, #1163,
- if matplotlib.__version__ >= "2.0.0":
+ if self._matplotlibVersion >= _parse_version("2.0.0"):
try:
FigureCanvasQTAgg.draw(self)
except ValueError as err:
@@ -956,7 +1089,6 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
if yRightLimits != self.ax2.get_ybound():
self._plot.getYAxis(axis='right')._emitLimitsChanged()
-
self._drawOverlays()
def replot(self):
@@ -975,6 +1107,12 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
elif dirtyFlag: # Need full redraw
self.draw()
+ # Workaround issue of rendering overlays with some matplotlib versions
+ if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
+ not hasattr(self, '_firstReplot')):
+ self._firstReplot = False
+ if self._overlays or self._graphCursor:
+ qt.QTimer.singleShot(0, self.draw) # Request async draw
# cursor
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index 3c18f4f..0001bb9 100644
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -28,7 +28,7 @@ from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "16/08/2017"
+__date__ = "24/04/2018"
from collections import OrderedDict, namedtuple
from ctypes import c_void_p
@@ -38,8 +38,7 @@ import numpy
from .._utils import FLOAT32_MINPOS
from . import BackendBase
-from .. import Colors
-from ..Colormap import Colormap
+from ... import colors
from ... import qt
from ..._glutils import gl
@@ -355,7 +354,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._markers = OrderedDict()
self._items = OrderedDict()
self._plotContent = PlotDataContent() # For images and curves
- self._selectionAreas = OrderedDict()
self._glGarbageCollector = []
self._plotFrame = GLPlotFrame2D(
@@ -399,7 +397,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
previousMousePosInPixels = self._mousePosInPixels
self._mousePosInPixels = (xPixel, yPixel) if isCursorInPlot else None
if (self._crosshairCursor is not None and
- previousMousePosInPixels != self._crosshairCursor):
+ previousMousePosInPixels != self._mousePosInPixels):
# Avoid replot when cursor remains outside plot area
self._plot._setDirtyPlot(overlayOnly=True)
@@ -431,14 +429,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# OpenGLWidget API
- @staticmethod
- def _setBlendFuncGL():
- # 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)
-
def initializeGL(self):
gl.testGL()
@@ -446,7 +436,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glClearStencil(0)
gl.glEnable(gl.GL_BLEND)
- self._setBlendFuncGL()
+ # 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)
@@ -500,7 +494,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glUniform1i(self._progTex.uniforms['tex'], texUnit)
gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
- mat4Identity())
+ mat4Identity().astype(numpy.float32))
stride = self._plotVertices.shape[-1] * self._plotVertices.itemsize
gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
@@ -649,24 +643,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- isXLog = self._plotFrame.xAxis.isLog
- isYLog = self._plotFrame.yAxis.isLog
-
# Render in plot area
gl.glScissor(self._plotFrame.margins.left,
self._plotFrame.margins.bottom,
plotWidth, plotHeight)
gl.glEnable(gl.GL_SCISSOR_TEST)
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
# Prepare vertical and horizontal markers rendering
self._progBase.use()
- gl.glUniformMatrix4fv(self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self._plotFrame.transformedDataProjMat)
- gl.glUniform2i(self._progBase.uniforms['isLog'], isXLog, isYLog)
+ gl.glUniformMatrix4fv(
+ self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
+ self.matScreenProj.astype(numpy.float32))
+ gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
gl.glUniform1i(self._progBase.uniforms['hatchStep'], 0)
gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
posAttrib = self._progBase.attributes['position']
@@ -677,10 +667,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
for marker in self._markers.values():
xCoord, yCoord = marker['x'], marker['y']
- if ((isXLog and xCoord is not None and
- xCoord < FLOAT32_MINPOS) or
- (isYLog and yCoord is not None and
- yCoord < FLOAT32_MINPOS)):
+ 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
@@ -706,9 +698,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
align=RIGHT, valign=BOTTOM)
labels.append(label)
- xMin, xMax = self._plotFrame.dataRanges.x
- vertices = numpy.array(((xMin, yCoord),
- (xMax, yCoord)),
+ width = self._plotFrame.size[0]
+ vertices = numpy.array(((0, pixelPos[1]),
+ (width, pixelPos[1])),
dtype=numpy.float32)
else: # yCoord is None: vertical line in data space
@@ -721,13 +713,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
align=LEFT, valign=TOP)
labels.append(label)
- yMin, yMax = self._plotFrame.dataRanges.y
- vertices = numpy.array(((xCoord, yMin),
- (xCoord, yMax)),
+ height = self._plotFrame.size[1]
+ vertices = numpy.array(((pixelPos[0], 0),
+ (pixelPos[0], height)),
dtype=numpy.float32)
self._progBase.use()
-
gl.glUniform4f(self._progBase.uniforms['color'],
*marker['color'])
@@ -759,13 +750,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# For now simple implementation: using a curve for each marker
# Should pack all markers to a single set of points
markerCurve = GLPlotCurve2D(
- numpy.array((xCoord,), dtype=numpy.float32),
- numpy.array((yCoord,), dtype=numpy.float32),
+ 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._plotFrame.transformedDataProjMat,
- isXLog, isYLog)
+ markerCurve.render(self.matScreenProj, False, False)
markerCurve.discard()
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
@@ -777,8 +767,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glDisable(gl.GL_SCISSOR_TEST)
def _renderOverlayGL(self):
- # Render selection area and crosshair cursor
- if self._selectionAreas or self._crosshairCursor is not None:
+ # Render crosshair cursor
+ if self._crosshairCursor is not None:
plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
# Scissor to plot area
@@ -788,41 +778,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glEnable(gl.GL_SCISSOR_TEST)
self._progBase.use()
- gl.glUniform2i(self._progBase.uniforms['isLog'],
- self._plotFrame.xAxis.isLog,
- self._plotFrame.yAxis.isLog)
+ 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 selection area in plot area
- if self._selectionAreas:
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
-
- gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE,
- self._plotFrame.transformedDataProjMat)
-
- for shape in self._selectionAreas.values():
- if shape.isVideoInverted:
- gl.glBlendFunc(gl.GL_ONE_MINUS_DST_COLOR, gl.GL_ZERO)
-
- shape.render(posAttrib, colorUnif, hatchStepUnif)
-
- if shape.isVideoInverted:
- self._setBlendFuncGL()
-
- # Render crosshair cursor is screen frame but with scissor
+ # 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)
+ self.matScreenProj.astype(numpy.float32))
color, lineWidth = self._crosshairCursor
gl.glUniform4f(colorUnif, *color)
@@ -881,31 +851,30 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
isXLog, isYLog)
# Render Items
+ gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
+
self._progBase.use()
gl.glUniformMatrix4fv(self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self._plotFrame.transformedDataProjMat)
- gl.glUniform2i(self._progBase.uniforms['isLog'],
- self._plotFrame.xAxis.isLog,
- self._plotFrame.yAxis.isLog)
+ self.matScreenProj.astype(numpy.float32))
+ gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
for item in self._items.values():
- shape2D = item.get('_shape2D')
- if shape2D is None:
- closed = item['shape'] != 'polylines'
- shape2D = Shape2D(tuple(zip(item['x'], item['y'])),
- fill=item['fill'],
- fillColor=item['color'],
- stroke=True,
- strokeColor=item['color'],
- strokeClosed=closed)
- item['_shape2D'] = shape2D
-
- if ((isXLog and shape2D.xMin < FLOAT32_MINPOS) or
- (isYLog and shape2D.yMin < FLOAT32_MINPOS)):
+ 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
+ closed = item['shape'] != 'polylines'
+ points = [self.dataToPixel(x, y, axis='left', check=False)
+ for (x, y) in zip(item['x'], item['y'])]
+ shape2D = Shape2D(points,
+ fill=item['fill'],
+ fillColor=item['color'],
+ stroke=True,
+ strokeColor=item['color'],
+ strokeClosed=closed)
+
posAttrib = self._progBase.attributes['position']
colorUnif = self._progBase.uniforms['color']
hatchStepUnif = self._progBase.uniforms['hatchStep']
@@ -944,6 +913,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# 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,
@@ -954,8 +938,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
assert parameter is not None
assert yaxis in ('left', 'right')
- x = numpy.array(x, dtype=numpy.float32, copy=False, order='C')
- y = numpy.array(y, dtype=numpy.float32, copy=False, order='C')
+ # 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')
@@ -963,6 +960,47 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
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
+ 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
+ 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]):
@@ -973,7 +1011,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
color = None
else:
colorArray = None
- color = Colors.rgba(color)
+ color = colors.rgba(color)
if alpha < 1.: # Apply image transparency
if colorArray is not None and colorArray.shape[1] == 4:
@@ -995,7 +1033,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
marker=symbol,
markerColor=color,
markerSize=symbolsize,
- fillColor=color if fill else None)
+ fillColor=color if fill else None,
+ isYLog=isYLog)
curve.info = {
'legend': legend,
'zOrder': z,
@@ -1054,7 +1093,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif len(data.shape) == 3:
# For RGB, RGBA data
assert data.shape[2] in (3, 4)
- assert data.dtype in (numpy.float32, numpy.uint8)
+
+ 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)
@@ -1106,7 +1151,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._items[legend] = {
'shape': shape,
- 'color': Colors.rgba(color),
+ 'color': colors.rgba(color),
'fill': 'hatch' if fill else None,
'x': x,
'y': y
@@ -1133,19 +1178,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if isConstraint:
x, y = constraint(x, y)
- if x is not None and self._plotFrame.xAxis.isLog and x <= 0.:
- raise RuntimeError(
- 'Cannot add marker with X <= 0 with X axis log scale')
- if y is not None and self._plotFrame.yAxis.isLog and y <= 0.:
- raise RuntimeError(
- 'Cannot add marker with Y <= 0 with Y axis log scale')
-
self._markers[legend] = {
'x': x,
'y': y,
'legend': legend,
'text': text,
- 'color': Colors.rgba(color),
+ 'color': colors.rgba(color),
'behaviors': behaviors,
'constraint': constraint if isConstraint else None,
'symbol': symbol,
@@ -1204,7 +1242,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
"BackendOpenGL.setGraphCursor linestyle parameter ignored")
if flag:
- color = Colors.rgba(color)
+ color = colors.rgba(color)
crosshairCursor = color, linewidth
else:
crosshairCursor = None
@@ -1304,6 +1342,16 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
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)
+
pickedIndices = item.pick(xPickMin, yPickMin,
xPickMax, yPickMax)
if pickedIndices:
@@ -1548,6 +1596,18 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# 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:
@@ -1657,4 +1717,4 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setAxesDisplayed(self, displayed):
BackendBase.BackendBase.setAxesDisplayed(self, displayed)
- self._plotFrame.displayed = displayed \ No newline at end of file
+ self._plotFrame.displayed = displayed
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 124a3da..12b6bbe 100644
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,8 @@
This module provides classes to render 2D lines and scatter plots
"""
+from __future__ import division
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -33,73 +35,73 @@ __date__ = "03/04/2017"
import math
import logging
+import warnings
import numpy
from silx.math.combo import min_max
from ...._glutils import gl
-from ...._glutils import numpyToGLType, Program, vertexBuffer
-from ..._utils import FLOAT32_MINPOS
-from .GLSupport import buildFillMaskIndices
+from ...._glutils import Program, vertexBuffer
+from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate
_logger = logging.getLogger(__name__)
_MPL_NONES = None, 'None', '', ' '
+"""Possible values for None"""
-# fill ########################################################################
+def _notNaNSlices(array, length=1):
+ """Returns slices of none NaN values in the array.
-class _Fill2D(object):
- _LINEAR, _LOG10_X, _LOG10_Y, _LOG10_X_Y = 0, 1, 2, 3
+ :param numpy.ndarray array: 1D array from which to get slices
+ :param int length: Slices shorter than length gets discarded
+ :return: Array of (start, end) slice indices
+ :rtype: numpy.ndarray
+ """
+ isnan = numpy.isnan(numpy.array(array, copy=False).reshape(-1))
+ notnan = numpy.logical_not(isnan)
+ start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1
+ if notnan[0]:
+ start = numpy.append(0, start)
+ end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1
+ if notnan[-1]:
+ end = numpy.append(end, len(array))
+ slices = numpy.transpose((start, end))
+ if length > 1:
+ # discard slices with less than length values
+ slices = slices[numpy.diff(slices, axis=1).ravel() >= length]
+ return slices
- _SHADERS = {
- 'vertexTransforms': {
- _LINEAR: """
- vec4 transformXY(float x, float y) {
- return vec4(x, y, 0.0, 1.0);
- }
- """,
- _LOG10_X: """
- const float oneOverLog10 = 0.43429448190325176;
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x), y, 0.0, 1.0);
- }
- """,
- _LOG10_Y: """
- const float oneOverLog10 = 0.43429448190325176;
+# fill ########################################################################
- vec4 transformXY(float x, float y) {
- return vec4(x, oneOverLog10 * log(y), 0.0, 1.0);
- }
- """,
- _LOG10_X_Y: """
- const float oneOverLog10 = 0.43429448190325176;
+class _Fill2D(object):
+ """Object rendering curve filling as polygons
+
+ :param numpy.ndarray xData: X coordinates of points
+ :param numpy.ndarray yData: Y coordinates of points
+ :param float baseline: Y value of the 'bottom' of the fill.
+ 0 for linear Y scale, -38 for log Y scale
+ :param List[float] color: RGBA color as 4 float in [0, 1]
+ :param List[float] offset: Translation of coordinates (ox, oy)
+ """
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x),
- oneOverLog10 * log(y),
- 0.0, 1.0);
- }
- """
- },
- 'vertex': """
+ _PROGRAM = Program(
+ vertexShader="""
#version 120
uniform mat4 matrix;
attribute float xPos;
attribute float yPos;
- %s
-
void main(void) {
- gl_Position = matrix * transformXY(xPos, yPos);
+ gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0);
}
""",
- 'fragment': """
+ fragmentShader="""
#version 120
uniform vec4 color;
@@ -107,72 +109,95 @@ class _Fill2D(object):
void main(void) {
gl_FragColor = color;
}
- """
- }
-
- _programs = {
- _LINEAR: Program(
- _SHADERS['vertex'] % _SHADERS['vertexTransforms'][_LINEAR],
- _SHADERS['fragment'], attrib0='xPos'),
- _LOG10_X: Program(
- _SHADERS['vertex'] % _SHADERS['vertexTransforms'][_LOG10_X],
- _SHADERS['fragment'], attrib0='xPos'),
- _LOG10_Y: Program(
- _SHADERS['vertex'] % _SHADERS['vertexTransforms'][_LOG10_Y],
- _SHADERS['fragment'], attrib0='xPos'),
- _LOG10_X_Y: Program(
- _SHADERS['vertex'] % _SHADERS['vertexTransforms'][_LOG10_X_Y],
- _SHADERS['fragment'], attrib0='xPos'),
- }
-
- def __init__(self, xFillVboData=None, yFillVboData=None,
- xMin=None, yMin=None, xMax=None, yMax=None,
- color=(0., 0., 0., 1.)):
- self.xFillVboData = xFillVboData
- self.yFillVboData = yFillVboData
- self.xMin, self.yMin = xMin, yMin
- self.xMax, self.yMax = xMax, yMax
+ """,
+ attrib0='xPos')
+
+ def __init__(self, xData=None, yData=None,
+ baseline=0,
+ color=(0., 0., 0., 1.),
+ offset=(0., 0.)):
+ self.xData = xData
+ self.yData = yData
+ self._xFillVboData = None
+ self._yFillVboData = None
self.color = color
+ self.offset = offset
- self._bboxVertices = None
- self._indices = None
- self._indicesType = None
+ # Offset baseline
+ self.baseline = baseline - self.offset[1]
def prepare(self):
- if self._indices is None:
- self._indices = buildFillMaskIndices(self.xFillVboData.size)
- self._indicesType = numpyToGLType(self._indices.dtype)
-
- if self._bboxVertices is None:
- yMin, yMax = min(self.yMin, 1e-32), max(self.yMax, 1e-32)
- self._bboxVertices = numpy.array(((self.xMin, self.xMin,
- self.xMax, self.xMax),
- (yMin, yMax, yMin, yMax)),
- dtype=numpy.float32)
-
- def render(self, matrix, isXLog, isYLog):
+ """Rendering preparation: build indices and bounding box vertices"""
+ if (self._xFillVboData is None and
+ self.xData is not None and self.yData is not None):
+
+ # Get slices of not NaN values longer than 1 element
+ isnan = numpy.logical_or(numpy.isnan(self.xData),
+ numpy.isnan(self.yData))
+ notnan = numpy.logical_not(isnan)
+ start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1
+ if notnan[0]:
+ start = numpy.append(0, start)
+ end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1
+ if notnan[-1]:
+ end = numpy.append(end, len(isnan))
+ slices = numpy.transpose((start, end))
+ # discard slices with less than length values
+ slices = slices[numpy.diff(slices, axis=1).reshape(-1) >= 2]
+
+ # Number of points: slice + 2 * leading and trailing points
+ # Twice leading and trailing points to produce degenerated triangles
+ nbPoints = numpy.sum(numpy.diff(slices, axis=1)) + 4 * len(slices)
+ points = numpy.empty((nbPoints, 2), dtype=numpy.float32)
+
+ offset = 0
+ for start, end in slices:
+ # Duplicate first point for connecting degenerated triangle
+ points[offset:offset+2] = self.xData[start], self.baseline
+
+ # 2nd point of the polygon is last point
+ points[offset+2] = self.xData[end-1], self.baseline
+
+ # Add all points from the data
+ indices = start + buildFillMaskIndices(end - start)
+
+ points[offset+3:offset+3+len(indices), 0] = self.xData[indices]
+ points[offset+3:offset+3+len(indices), 1] = self.yData[indices]
+
+ # Duplicate last point for connecting degenerated triangle
+ points[offset+3+len(indices)] = points[offset+3+len(indices)-1]
+
+ offset += len(indices) + 4
+
+ self._xFillVboData, self._yFillVboData = vertexBuffer(points.T)
+
+ def render(self, matrix):
+ """Perform rendering
+
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ """
self.prepare()
- if isXLog:
- transform = self._LOG10_X_Y if isYLog else self._LOG10_X
- else:
- transform = self._LOG10_Y if isYLog else self._LINEAR
+ if self._xFillVboData is None:
+ return # Nothing to display
- prog = self._programs[transform]
- prog.use()
+ self._PROGRAM.use()
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
+ gl.glUniformMatrix4fv(
+ self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
+ numpy.dot(matrix,
+ mat4Translate(*self.offset)).astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self.color)
+ gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color)
- xPosAttrib = prog.attributes['xPos']
- yPosAttrib = prog.attributes['yPos']
+ xPosAttrib = self._PROGRAM.attributes['xPos']
+ yPosAttrib = self._PROGRAM.attributes['yPos']
gl.glEnableVertexAttribArray(xPosAttrib)
- self.xFillVboData.setVertexAttrib(xPosAttrib)
+ self._xFillVboData.setVertexAttrib(xPosAttrib)
gl.glEnableVertexAttribArray(yPosAttrib)
- self.yFillVboData.setVertexAttrib(yPosAttrib)
+ self._yFillVboData.setVertexAttrib(yPosAttrib)
# Prepare fill mask
gl.glEnable(gl.GL_STENCIL_TEST)
@@ -182,8 +207,7 @@ class _Fill2D(object):
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDrawElements(gl.GL_TRIANGLE_STRIP, self._indices.size,
- self._indicesType, self._indices)
+ gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self._xFillVboData.size)
gl.glStencilFunc(gl.GL_EQUAL, 1, 1)
# Reset stencil while drawing
@@ -191,14 +215,30 @@ class _Fill2D(object):
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glVertexAttribPointer(xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- self._bboxVertices[0])
- gl.glVertexAttribPointer(yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- self._bboxVertices[1])
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self._bboxVertices[0].size)
+ # Draw directly in NDC
+ gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
+ mat4Identity().astype(numpy.float32))
+
+ # NDC vertices
+ gl.glVertexAttribPointer(
+ xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
+ numpy.array((-1., -1., 1., 1.), dtype=numpy.float32))
+ gl.glVertexAttribPointer(
+ yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
+ numpy.array((-1., 1., -1., 1.), dtype=numpy.float32))
+
+ gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glDisable(gl.GL_STENCIL_TEST)
+ def discard(self):
+ """Release VBOs"""
+ if self._xFillVboData is not None:
+ self._xFillVboData.vbo.discard()
+
+ self._xFillVboData = None
+ self._yFillVboData = None
+
# line ########################################################################
@@ -206,44 +246,25 @@ SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
class _Lines2D(object):
+ """Object rendering curve as a polyline
+
+ :param xVboData: X coordinates VBO
+ :param yVboData: Y coordinates VBO
+ :param colorVboData: VBO of colors
+ :param distVboData: VBO of distance along the polyline
+ :param str style: Line style in: '-', '--', '-.', ':'
+ :param List[float] color: RGBA color as 4 float in [0, 1]
+ :param float width: Line width
+ :param float dashPeriod: Period of dashes
+ :param drawMode: OpenGL drawing mode
+ :param List[float] offset: Translation of coordinates (ox, oy)
+ """
+
STYLES = SOLID, DASHED, DASHDOT, DOTTED
"""Supported line styles"""
- _LINEAR, _LOG10_X, _LOG10_Y, _LOG10_X_Y = 0, 1, 2, 3
-
- _SHADERS = {
- 'vertexTransforms': {
- _LINEAR: """
- vec4 transformXY(float x, float y) {
- return vec4(x, y, 0.0, 1.0);
- }
- """,
- _LOG10_X: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x), y, 0.0, 1.0);
- }
- """,
- _LOG10_Y: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(x, oneOverLog10 * log(y), 0.0, 1.0);
- }
- """,
- _LOG10_X_Y: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x),
- oneOverLog10 * log(y),
- 0.0, 1.0);
- }
- """
- },
- 'solid': {
- 'vertex': """
+ _SOLID_PROGRAM = Program(
+ vertexShader="""
#version 120
uniform mat4 matrix;
@@ -253,14 +274,12 @@ class _Lines2D(object):
varying vec4 vColor;
- %s
-
void main(void) {
- gl_Position = matrix * transformXY(xPos, yPos);
+ gl_Position = matrix * vec4(xPos, yPos, 0., 1.) ;
vColor = color;
}
""",
- 'fragment': """
+ fragmentShader="""
#version 120
varying vec4 vColor;
@@ -268,15 +287,14 @@ class _Lines2D(object):
void main(void) {
gl_FragColor = vColor;
}
- """
- },
-
+ """,
+ attrib0='xPos')
- # Limitation: Dash using an estimate of distance in screen coord
- # to avoid computing distance when viewport is resized
- # results in inequal dashes when viewport aspect ratio is far from 1
- 'dashed': {
- 'vertex': """
+ # Limitation: Dash using an estimate of distance in screen coord
+ # to avoid computing distance when viewport is resized
+ # results in inequal dashes when viewport aspect ratio is far from 1
+ _DASH_PROGRAM = Program(
+ vertexShader="""
#version 120
uniform mat4 matrix;
@@ -289,10 +307,8 @@ class _Lines2D(object):
varying float vDist;
varying vec4 vColor;
- %s
-
void main(void) {
- gl_Position = matrix * transformXY(xPos, yPos);
+ gl_Position = matrix * vec4(xPos, yPos, 0., 1.);
//Estimate distance in pixels
vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) *
halfViewportSize;
@@ -301,7 +317,7 @@ class _Lines2D(object):
vColor = color;
}
""",
- 'fragment': """
+ fragmentShader="""
#version 120
/* Dashes: [0, x], [y, z]
@@ -318,16 +334,14 @@ class _Lines2D(object):
}
gl_FragColor = vColor;
}
- """
- }
- }
-
- _programs = {}
+ """,
+ attrib0='xPos')
def __init__(self, xVboData=None, yVboData=None,
colorVboData=None, distVboData=None,
style=SOLID, color=(0., 0., 0., 1.),
- width=1, dashPeriod=20, drawMode=None):
+ width=1, dashPeriod=20, drawMode=None,
+ offset=(0., 0.)):
self.xVboData = xVboData
self.yVboData = yVboData
self.distVboData = distVboData
@@ -335,136 +349,83 @@ class _Lines2D(object):
self.useColorVboData = colorVboData is not None
self.color = color
- self._width = 1
self.width = width
self._style = None
self.style = style
self.dashPeriod = dashPeriod
+ self.offset = offset
self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP
@property
def style(self):
+ """Line style (Union[str,None])"""
return self._style
@style.setter
def style(self, style):
if style in _MPL_NONES:
self._style = None
- self.render = self._renderNone
else:
assert style in self.STYLES
self._style = style
- if style == SOLID:
- self.render = self._renderSolid
- else: # DASHED, DASHDOT, DOTTED
- self.render = self._renderDash
-
- @property
- def width(self):
- return self._width
-
- @width.setter
- def width(self, width):
- # try:
- # widthRange = self._widthRange
- # except AttributeError:
- # widthRange = gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE)
- # # Shared among contexts, this should be enough..
- # _Lines2D._widthRange = widthRange
- # assert width >= widthRange[0] and width <= widthRange[1]
- self._width = width
-
- @classmethod
- def _getProgram(cls, transform, style):
- try:
- prgm = cls._programs[(transform, style)]
- except KeyError:
- sources = cls._SHADERS[style]
- vertexShdr = sources['vertex'] % \
- cls._SHADERS['vertexTransforms'][transform]
- prgm = Program(vertexShdr, sources['fragment'], attrib0='xPos')
- cls._programs[(transform, style)] = prgm
- return prgm
@classmethod
def init(cls):
+ """OpenGL context initialization"""
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
- def _renderNone(self, matrix, isXLog, isYLog):
- pass
-
- render = _renderNone # Overridden in style setter
-
- def _renderSolid(self, matrix, isXLog, isYLog):
- if isXLog:
- transform = self._LOG10_X_Y if isYLog else self._LOG10_X
- else:
- transform = self._LOG10_Y if isYLog else self._LINEAR
-
- prog = self._getProgram(transform, 'solid')
- prog.use()
-
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
-
- colorAttrib = prog.attributes['color']
- if self.useColorVboData and self.colorVboData is not None:
- gl.glEnableVertexAttribArray(colorAttrib)
- self.colorVboData.setVertexAttrib(colorAttrib)
- else:
- gl.glDisableVertexAttribArray(colorAttrib)
- gl.glVertexAttrib4f(colorAttrib, *self.color)
-
- xPosAttrib = prog.attributes['xPos']
- gl.glEnableVertexAttribArray(xPosAttrib)
- self.xVboData.setVertexAttrib(xPosAttrib)
-
- yPosAttrib = prog.attributes['yPos']
- gl.glEnableVertexAttribArray(yPosAttrib)
- self.yVboData.setVertexAttrib(yPosAttrib)
+ def render(self, matrix):
+ """Perform rendering
- gl.glLineWidth(self.width)
- gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ """
+ style = self.style
+ if style is None:
+ return
- gl.glDisable(gl.GL_LINE_SMOOTH)
+ elif style == SOLID:
+ program = self._SOLID_PROGRAM
+ program.use()
+
+ else: # DASHED, DASHDOT, DOTTED
+ program = self._DASH_PROGRAM
+ program.use()
+
+ x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT)
+ gl.glUniform2f(program.uniforms['halfViewportSize'],
+ 0.5 * viewWidth, 0.5 * viewHeight)
+
+ if self.style == DOTTED:
+ dash = (0.1 * self.dashPeriod,
+ 0.6 * self.dashPeriod,
+ 0.7 * self.dashPeriod,
+ self.dashPeriod)
+ elif self.style == DASHDOT:
+ dash = (0.3 * self.dashPeriod,
+ 0.5 * self.dashPeriod,
+ 0.6 * self.dashPeriod,
+ self.dashPeriod)
+ else:
+ dash = (0.5 * self.dashPeriod,
+ self.dashPeriod,
+ self.dashPeriod,
+ self.dashPeriod)
- def _renderDash(self, matrix, isXLog, isYLog):
- if isXLog:
- transform = self._LOG10_X_Y if isYLog else self._LOG10_X
- else:
- transform = self._LOG10_Y if isYLog else self._LINEAR
+ gl.glUniform4f(program.uniforms['dash'], *dash)
- prog = self._getProgram(transform, 'dashed')
- prog.use()
+ distAttrib = program.attributes['distance']
+ gl.glEnableVertexAttribArray(distAttrib)
+ self.distVboData.setVertexAttrib(distAttrib)
gl.glEnable(gl.GL_LINE_SMOOTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
- x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT)
- gl.glUniform2f(prog.uniforms['halfViewportSize'],
- 0.5 * viewWidth, 0.5 * viewHeight)
-
- if self.style == DOTTED:
- dash = (0.1 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- 0.7 * self.dashPeriod,
- self.dashPeriod)
- elif self.style == DASHDOT:
- dash = (0.3 * self.dashPeriod,
- 0.5 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- self.dashPeriod)
- else:
- dash = (0.5 * self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod)
+ matrix = numpy.dot(matrix,
+ mat4Translate(*self.offset)).astype(numpy.float32)
+ gl.glUniformMatrix4fv(program.uniforms['matrix'],
+ 1, gl.GL_TRUE, matrix)
- gl.glUniform4f(prog.uniforms['dash'], *dash)
-
- colorAttrib = prog.attributes['color']
+ colorAttrib = program.attributes['color']
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(colorAttrib)
self.colorVboData.setVertexAttrib(colorAttrib)
@@ -472,15 +433,11 @@ class _Lines2D(object):
gl.glDisableVertexAttribArray(colorAttrib)
gl.glVertexAttrib4f(colorAttrib, *self.color)
- distAttrib = prog.attributes['distance']
- gl.glEnableVertexAttribArray(distAttrib)
- self.distVboData.setVertexAttrib(distAttrib)
-
- xPosAttrib = prog.attributes['xPos']
+ xPosAttrib = program.attributes['xPos']
gl.glEnableVertexAttribArray(xPosAttrib)
self.xVboData.setVertexAttrib(xPosAttrib)
- yPosAttrib = prog.attributes['yPos']
+ yPosAttrib = program.attributes['yPos']
gl.glEnableVertexAttribArray(yPosAttrib)
self.yVboData.setVertexAttrib(yPosAttrib)
@@ -491,6 +448,12 @@ class _Lines2D(object):
def _distancesFromArrays(xData, yData):
+ """Returns distances between each points
+
+ :param numpy.ndarray xData: X coordinate of points
+ :param numpy.ndarray yData: Y coordinate of points
+ :rtype: numpy.ndarray
+ """
deltas = numpy.dstack((
numpy.ediff1d(xData, to_begin=numpy.float32(0.)),
numpy.ediff1d(yData, to_begin=numpy.float32(0.))))[0]
@@ -506,43 +469,22 @@ H_LINE, V_LINE = '_', '|'
class _Points2D(object):
+ """Object rendering curve markers
+
+ :param xVboData: X coordinates VBO
+ :param yVboData: Y coordinates VBO
+ :param colorVboData: VBO of colors
+ :param str marker: Kind of symbol to use, see :attr:`MARKERS`.
+ :param List[float] color: RGBA color as 4 float in [0, 1]
+ :param float size: Marker size
+ :param List[float] offset: Translation of coordinates (ox, oy)
+ """
+
MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK,
H_LINE, V_LINE)
+ """List of supported markers"""
- _LINEAR, _LOG10_X, _LOG10_Y, _LOG10_X_Y = 0, 1, 2, 3
-
- _SHADERS = {
- 'vertexTransforms': {
- _LINEAR: """
- vec4 transformXY(float x, float y) {
- return vec4(x, y, 0.0, 1.0);
- }
- """,
- _LOG10_X: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x), y, 0.0, 1.0);
- }
- """,
- _LOG10_Y: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(x, oneOverLog10 * log(y), 0.0, 1.0);
- }
- """,
- _LOG10_X_Y: """
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 transformXY(float x, float y) {
- return vec4(oneOverLog10 * log(x),
- oneOverLog10 * log(y),
- 0.0, 1.0);
- }
- """
- },
- 'vertex': """
+ _VERTEX_SHADER = """
#version 120
uniform mat4 matrix;
@@ -554,16 +496,14 @@ class _Points2D(object):
varying vec4 vColor;
- %s
-
void main(void) {
- gl_Position = matrix * transformXY(xPos, yPos);
+ gl_Position = matrix * vec4(xPos, yPos, 0., 1.);
vColor = color;
gl_PointSize = size;
}
- """,
+ """
- 'fragmentSymbols': {
+ _FRAGMENT_SHADER_SYMBOLS = {
DIAMOND: """
float alphaSymbol(vec2 coord, float size) {
vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
@@ -640,9 +580,9 @@ class _Points2D(object):
}
}
"""
- },
+ }
- 'fragment': """
+ _FRAGMENT_SHADER_TEMPLATE = """
#version 120
uniform float size;
@@ -660,17 +600,17 @@ class _Points2D(object):
}
}
"""
- }
- _programs = {}
+ _PROGRAMS = {}
def __init__(self, xVboData=None, yVboData=None, colorVboData=None,
- marker=SQUARE, color=(0., 0., 0., 1.), size=7):
+ marker=SQUARE, color=(0., 0., 0., 1.), size=7,
+ offset=(0., 0.)):
self.color = color
self._marker = None
self.marker = marker
- self._size = 1
self.size = size
+ self.offset = offset
self.xVboData = xVboData
self.yVboData = yVboData
@@ -679,54 +619,37 @@ class _Points2D(object):
@property
def marker(self):
+ """Symbol used to display markers (str)"""
return self._marker
@marker.setter
def marker(self, marker):
if marker in _MPL_NONES:
self._marker = None
- self.render = self._renderNone
else:
assert marker in self.MARKERS
self._marker = marker
- self.render = self._renderMarkers
-
- @property
- def size(self):
- return self._size
-
- @size.setter
- def size(self, size):
- # try:
- # sizeRange = self._sizeRange
- # except AttributeError:
- # sizeRange = gl.glGetFloatv(gl.GL_POINT_SIZE_RANGE)
- # # Shared among contexts, this should be enough..
- # _Points2D._sizeRange = sizeRange
- # assert size >= sizeRange[0] and size <= sizeRange[1]
- self._size = size
@classmethod
- def _getProgram(cls, transform, marker):
+ def _getProgram(cls, marker):
"""On-demand shader program creation."""
if marker == PIXEL:
marker = SQUARE
elif marker == POINT:
marker = CIRCLE
- try:
- prgm = cls._programs[(transform, marker)]
- except KeyError:
- vertShdr = cls._SHADERS['vertex'] % \
- cls._SHADERS['vertexTransforms'][transform]
- fragShdr = cls._SHADERS['fragment'] % \
- cls._SHADERS['fragmentSymbols'][marker]
- prgm = Program(vertShdr, fragShdr, attrib0='xPos')
-
- cls._programs[(transform, marker)] = prgm
- return prgm
+
+ if marker not in cls._PROGRAMS:
+ cls._PROGRAMS[marker] = Program(
+ vertexShader=cls._VERTEX_SHADER,
+ fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE %
+ cls._FRAGMENT_SHADER_SYMBOLS[marker]),
+ attrib0='xPos')
+
+ return cls._PROGRAMS[marker]
@classmethod
def init(cls):
+ """OpenGL context initialization"""
version = gl.glGetString(gl.GL_VERSION)
majorVersion = int(version[0])
assert majorVersion >= 2
@@ -735,30 +658,31 @@ class _Points2D(object):
if majorVersion >= 3: # OpenGL 3
gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- def _renderNone(self, matrix, isXLog, isYLog):
- pass
+ def render(self, matrix):
+ """Perform rendering
- render = _renderNone
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ """
+ if self.marker is None:
+ return
- def _renderMarkers(self, matrix, isXLog, isYLog):
- if isXLog:
- transform = self._LOG10_X_Y if isYLog else self._LOG10_X
- else:
- transform = self._LOG10_Y if isYLog else self._LINEAR
+ program = self._getProgram(self.marker)
+ program.use()
+
+ matrix = numpy.dot(matrix,
+ mat4Translate(*self.offset)).astype(numpy.float32)
+ gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
- prog = self._getProgram(transform, self.marker)
- prog.use()
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
if self.marker == PIXEL:
size = 1
elif self.marker == POINT:
size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point
else:
size = self.size
- gl.glUniform1f(prog.uniforms['size'], size)
+ gl.glUniform1f(program.uniforms['size'], size)
# gl.glPointSize(self.size)
- cAttrib = prog.attributes['color']
+ cAttrib = program.attributes['color']
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(cAttrib)
self.colorVboData.setVertexAttrib(cAttrib)
@@ -766,11 +690,11 @@ class _Points2D(object):
gl.glDisableVertexAttribArray(cAttrib)
gl.glVertexAttrib4f(cAttrib, *self.color)
- xAttrib = prog.attributes['xPos']
+ xAttrib = program.attributes['xPos']
gl.glEnableVertexAttribArray(xAttrib)
self.xVboData.setVertexAttrib(xAttrib)
- yAttrib = prog.attributes['yPos']
+ yAttrib = program.attributes['yPos']
gl.glEnableVertexAttribArray(yAttrib)
self.yVboData.setVertexAttrib(yAttrib)
@@ -786,40 +710,35 @@ class _ErrorBars(object):
This is using its own VBO as opposed to fill/points/lines.
There is no picking on error bars.
- As is, there is no way to update data and errors, but it handles
- log scales by removing data <= 0 and clipping error bars to positive
- range.
It uses 2 vertices per error bars and uses :class:`_Lines2D` to
render error bars and :class:`_Points2D` to render the ends.
+
+ :param numpy.ndarray xData: X coordinates of the data.
+ :param numpy.ndarray yData: Y coordinates of the data.
+ :param xError: The absolute error on the X axis.
+ :type xError: A float, or a numpy.ndarray of float32.
+ If it is an array, it can either be a 1D array of
+ same length as the data or a 2D array with 2 rows
+ of same length as the data: row 0 for negative errors,
+ row 1 for positive errors.
+ :param yError: The absolute error on the Y axis.
+ :type yError: A float, or a numpy.ndarray of float32. See xError.
+ :param float xMin: The min X value already computed by GLPlotCurve2D.
+ :param float yMin: The min Y value already computed by GLPlotCurve2D.
+ :param List[float] color: RGBA color as 4 float in [0, 1]
+ :param List[float] offset: Translation of coordinates (ox, oy)
"""
def __init__(self, xData, yData, xError, yError,
xMin, yMin,
- color=(0., 0., 0., 1.)):
- """Initialization.
-
- :param numpy.ndarray xData: X coordinates of the data.
- :param numpy.ndarray yData: Y coordinates of the data.
- :param xError: The absolute error on the X axis.
- :type xError: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for negative errors,
- row 1 for positive errors.
- :param yError: The absolute error on the Y axis.
- :type yError: A float, or a numpy.ndarray of float32. See xError.
- :param float xMin: The min X value already computed by GLPlotCurve2D.
- :param float yMin: The min Y value already computed by GLPlotCurve2D.
- :param color: The color to use for both lines and ending points.
- :type color: tuple of 4 floats
- """
+ color=(0., 0., 0., 1.),
+ offset=(0., 0.)):
self._attribs = None
- self._isXLog, self._isYLog = False, False
self._xMin, self._yMin = xMin, yMin
+ self.offset = offset
if xError is not None or yError is not None:
- assert len(xData) == len(yData)
self._xData = numpy.array(
xData, order='C', dtype=numpy.float32, copy=False)
self._yData = numpy.array(
@@ -834,61 +753,19 @@ class _ErrorBars(object):
self._xData, self._yData = None, None
self._xError, self._yError = None, None
- self._lines = _Lines2D(None, None, color=color, drawMode=gl.GL_LINES)
- self._xErrPoints = _Points2D(None, None, color=color, marker=V_LINE)
- self._yErrPoints = _Points2D(None, None, color=color, marker=H_LINE)
+ self._lines = _Lines2D(
+ None, None, color=color, drawMode=gl.GL_LINES, offset=offset)
+ self._xErrPoints = _Points2D(
+ None, None, color=color, marker=V_LINE, offset=offset)
+ self._yErrPoints = _Points2D(
+ None, None, color=color, marker=H_LINE, offset=offset)
- def _positiveValueFilter(self, onlyXPos, onlyYPos):
- """Filter data (x, y) and errors (xError, yError) to remove
- negative and null data values on required axis (onlyXPos, onlyYPos).
+ def _buildVertices(self):
+ """Generates error bars vertices"""
+ nbLinesPerDataPts = (0 if self._xError is None else 2) + \
+ (0 if self._yError is None else 2)
- Returned arrays might be NOT contiguous.
-
- :return: Filtered xData, yData, xError and yError arrays.
- """
- if ((not onlyXPos or self._xMin > 0.) and
- (not onlyYPos or self._yMin > 0.)):
- # No need to filter, all values are > 0 on log axes
- return self._xData, self._yData, self._xError, self._yError
-
- _logger.warning(
- 'Removing values <= 0 of curve with error bars on a log axis.')
-
- x, y = self._xData, self._yData
- xError, yError = self._xError, self._yError
-
- # First remove negative data
- if onlyXPos and onlyYPos:
- mask = (x > 0.) & (y > 0.)
- elif onlyXPos:
- mask = x > 0.
- else: # onlyYPos
- mask = y > 0.
- x, y = x[mask], y[mask]
-
- # Remove corresponding values from error arrays
- if xError is not None and xError.size != 1:
- if len(xError.shape) == 1:
- xError = xError[mask]
- else: # 2 rows
- xError = xError[:, mask]
- if yError is not None and yError.size != 1:
- if len(yError.shape) == 1:
- yError = yError[mask]
- else: # 2 rows
- yError = yError[:, mask]
-
- return x, y, xError, yError
-
- def _buildVertices(self, isXLog, isYLog):
- """Generates error bars vertices according to log scales."""
- xData, yData, xError, yError = self._positiveValueFilter(
- isXLog, isYLog)
-
- nbLinesPerDataPts = 1 if xError is not None else 0
- nbLinesPerDataPts += 1 if yError is not None else 0
-
- nbDataPts = len(xData)
+ nbDataPts = len(self._xData)
# interleave coord+error, coord-error.
# xError vertices first if any, then yError vertices if any.
@@ -897,64 +774,61 @@ class _ErrorBars(object):
yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
dtype=numpy.float32)
- if xError is not None: # errors on the X axis
- if len(xError.shape) == 2:
- xErrorMinus, xErrorPlus = xError[0], xError[1]
+ if self._xError is not None: # errors on the X axis
+ if len(self._xError.shape) == 2:
+ xErrorMinus, xErrorPlus = self._xError[0], self._xError[1]
else:
# numpy arrays of len 1 or len(xData)
- xErrorMinus, xErrorPlus = xError, xError
+ xErrorMinus, xErrorPlus = self._xError, self._xError
# Interleave vertices for xError
- endXError = 2 * nbDataPts
- xCoords[0:endXError-1:2] = xData + xErrorPlus
+ endXError = 4 * nbDataPts
+ xCoords[0:endXError-3:4] = self._xData + xErrorPlus
+ xCoords[1:endXError-2:4] = self._xData
+ xCoords[2:endXError-1:4] = self._xData
+ xCoords[3:endXError:4] = self._xData - xErrorMinus
- minValues = xData - xErrorMinus
- if isXLog:
- # Clip min bounds to positive value
- minValues[minValues <= 0] = FLOAT32_MINPOS
- xCoords[1:endXError:2] = minValues
+ yCoords[0:endXError-3:4] = self._yData
+ yCoords[1:endXError-2:4] = self._yData
+ yCoords[2:endXError-1:4] = self._yData
+ yCoords[3:endXError:4] = self._yData
- yCoords[0:endXError-1:2] = yData
- yCoords[1:endXError:2] = yData
else:
endXError = 0
- if yError is not None: # errors on the Y axis
- if len(yError.shape) == 2:
- yErrorMinus, yErrorPlus = yError[0], yError[1]
+ if self._yError is not None: # errors on the Y axis
+ if len(self._yError.shape) == 2:
+ yErrorMinus, yErrorPlus = self._yError[0], self._yError[1]
else:
# numpy arrays of len 1 or len(yData)
- yErrorMinus, yErrorPlus = yError, yError
+ yErrorMinus, yErrorPlus = self._yError, self._yError
# Interleave vertices for yError
- xCoords[endXError::2] = xData
- xCoords[endXError+1::2] = xData
- yCoords[endXError::2] = yData + yErrorPlus
- minValues = yData - yErrorMinus
- if isYLog:
- # Clip min bounds to positive value
- minValues[minValues <= 0] = FLOAT32_MINPOS
- yCoords[endXError+1::2] = minValues
+ xCoords[endXError::4] = self._xData
+ xCoords[endXError+1::4] = self._xData
+ xCoords[endXError+2::4] = self._xData
+ xCoords[endXError+3::4] = self._xData
+
+ yCoords[endXError::4] = self._yData + yErrorPlus
+ yCoords[endXError+1::4] = self._yData
+ yCoords[endXError+2::4] = self._yData
+ yCoords[endXError+3::4] = self._yData - yErrorMinus
return xCoords, yCoords
- def prepare(self, isXLog, isYLog):
+ def prepare(self):
+ """Rendering preparation: build indices and bounding box vertices"""
if self._xData is None:
return
- if self._isXLog != isXLog or self._isYLog != isYLog:
- # Log state has changed
- self._isXLog, self._isYLog = isXLog, isYLog
-
- self.discard() # discard existing VBOs
-
if self._attribs is None:
- xCoords, yCoords = self._buildVertices(isXLog, isYLog)
+ xCoords, yCoords = self._buildVertices()
xAttrib, yAttrib = vertexBuffer((xCoords, yCoords))
self._attribs = xAttrib, yAttrib
- self._lines.xVboData, self._lines.yVboData = xAttrib, yAttrib
+ self._lines.xVboData = xAttrib
+ self._lines.yVboData = yAttrib
# Set xError points using the same VBO as lines
self._xErrPoints.xVboData = xAttrib.copy()
@@ -972,13 +846,20 @@ class _ErrorBars(object):
self._yErrPoints.yVboData.offset += (yAttrib.itemsize *
yAttrib.size // 2)
- def render(self, matrix, isXLog, isYLog):
+ def render(self, matrix):
+ """Perform rendering
+
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ """
+ self.prepare()
+
if self._attribs is not None:
- self._lines.render(matrix, isXLog, isYLog)
- self._xErrPoints.render(matrix, isXLog, isYLog)
- self._yErrPoints.render(matrix, isXLog, isYLog)
+ self._lines.render(matrix)
+ self._xErrPoints.render(matrix)
+ self._yErrPoints.render(matrix)
def discard(self):
+ """Release VBOs"""
if self._attribs is not None:
self._lines.xVboData, self._lines.yVboData = None, None
self._xErrPoints.xVboData, self._xErrPoints.yVboData = None, None
@@ -1014,71 +895,80 @@ def _proxyProperty(*componentsAttributes):
class GLPlotCurve2D(object):
def __init__(self, xData, yData, colorData=None,
xError=None, yError=None,
- lineStyle=None, lineColor=None,
- lineWidth=None, lineDashPeriod=None,
- marker=None, markerColor=None, markerSize=None,
- fillColor=None):
- self._isXLog = False
- self._isYLog = False
- self.xData, self.yData, self.colorData = xData, yData, colorData
-
- if fillColor is not None:
- self.fill = _Fill2D(color=fillColor)
- else:
- self.fill = None
+ lineStyle=SOLID,
+ lineColor=(0., 0., 0., 1.),
+ lineWidth=1,
+ lineDashPeriod=20,
+ marker=SQUARE,
+ markerColor=(0., 0., 0., 1.),
+ markerSize=7,
+ fillColor=None,
+ isYLog=False):
+
+ self.colorData = colorData
# Compute x bounds
if xError is None:
- result = min_max(xData, min_positive=True)
- self.xMin = result.minimum
- self.xMinPos = result.min_positive
- self.xMax = result.maximum
+ self.xMin, self.xMax = min_max(xData, min_positive=False)
else:
# Takes the error into account
if hasattr(xError, 'shape') and len(xError.shape) == 2:
- xErrorPlus, xErrorMinus = xError[0], xError[1]
+ xErrorMinus, xErrorPlus = xError[0], xError[1]
else:
- xErrorPlus, xErrorMinus = xError, xError
- result = min_max(xData - xErrorMinus, min_positive=True)
- self.xMin = result.minimum
- self.xMinPos = result.min_positive
- self.xMax = (xData + xErrorPlus).max()
+ xErrorMinus, xErrorPlus = xError, xError
+ self.xMin = numpy.nanmin(xData - xErrorMinus)
+ self.xMax = numpy.nanmax(xData + xErrorPlus)
# Compute y bounds
if yError is None:
- result = min_max(yData, min_positive=True)
- self.yMin = result.minimum
- self.yMinPos = result.min_positive
- self.yMax = result.maximum
+ self.yMin, self.yMax = min_max(yData, min_positive=False)
else:
# Takes the error into account
if hasattr(yError, 'shape') and len(yError.shape) == 2:
- yErrorPlus, yErrorMinus = yError[0], yError[1]
+ yErrorMinus, yErrorPlus = yError[0], yError[1]
else:
- yErrorPlus, yErrorMinus = yError, yError
- result = min_max(yData - yErrorMinus, min_positive=True)
- self.yMin = result.minimum
- self.yMinPos = result.min_positive
- self.yMax = (yData + yErrorPlus).max()
-
- self._errorBars = _ErrorBars(xData, yData, xError, yError,
- self.xMin, self.yMin)
-
- kwargs = {'style': lineStyle}
- if lineColor is not None:
- kwargs['color'] = lineColor
- if lineWidth is not None:
- kwargs['width'] = lineWidth
- if lineDashPeriod is not None:
- kwargs['dashPeriod'] = lineDashPeriod
- self.lines = _Lines2D(**kwargs)
-
- kwargs = {'marker': marker}
- if markerColor is not None:
- kwargs['color'] = markerColor
- if markerSize is not None:
- kwargs['size'] = markerSize
- self.points = _Points2D(**kwargs)
+ yErrorMinus, yErrorPlus = yError, yError
+ self.yMin = numpy.nanmin(yData - yErrorMinus)
+ self.yMax = numpy.nanmax(yData + yErrorPlus)
+
+ # Handle data offset
+ if xData.itemsize > 4 or yData.itemsize > 4: # Use normalization
+ # offset data, do not offset error as it is relative
+ self.offset = self.xMin, self.yMin
+ self.xData = (xData - self.offset[0]).astype(numpy.float32)
+ self.yData = (yData - self.offset[1]).astype(numpy.float32)
+
+ else: # float32
+ self.offset = 0., 0.
+ self.xData = xData
+ self.yData = yData
+
+ if fillColor is not None:
+ # Use different baseline depending of Y log scale
+ self.fill = _Fill2D(self.xData, self.yData,
+ baseline=-38 if isYLog else 0,
+ color=fillColor,
+ offset=self.offset)
+ else:
+ self.fill = None
+
+ self._errorBars = _ErrorBars(self.xData, self.yData,
+ xError, yError,
+ self.xMin, self.yMin,
+ offset=self.offset)
+
+ self.lines = _Lines2D()
+ self.lines.style = lineStyle
+ self.lines.color = lineColor
+ self.lines.width = lineWidth
+ self.lines.dashPeriod = lineDashPeriod
+ self.lines.offset = self.offset
+
+ self.points = _Points2D()
+ self.points.marker = marker
+ self.points.color = markerColor
+ self.points.size = markerSize
+ self.points.offset = self.offset
xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData'))
@@ -1108,123 +998,53 @@ class GLPlotCurve2D(object):
@classmethod
def init(cls):
+ """OpenGL context initialization"""
_Lines2D.init()
_Points2D.init()
- @staticmethod
- def _logFilterData(x, y, color=None, xLog=False, yLog=False):
- # Copied from Plot.py
- if xLog and yLog:
- idx = numpy.nonzero((x > 0) & (y > 0))[0]
- x = numpy.take(x, idx)
- y = numpy.take(y, idx)
- elif yLog:
- idx = numpy.nonzero(y > 0)[0]
- x = numpy.take(x, idx)
- y = numpy.take(y, idx)
- elif xLog:
- idx = numpy.nonzero(x > 0)[0]
- x = numpy.take(x, idx)
- y = numpy.take(y, idx)
- else:
- idx = None
-
- if idx is not None and isinstance(color, numpy.ndarray):
- colors = numpy.zeros((x.size, 4), color.dtype)
- colors[:, 0] = color[idx, 0]
- colors[:, 1] = color[idx, 1]
- colors[:, 2] = color[idx, 2]
- colors[:, 3] = color[idx, 3]
- else:
- colors = color
- return x, y, colors
-
- def prepare(self, isXLog, isYLog):
- # init only supports updating isXLog, isYLog
- xData, yData, colorData = self.xData, self.yData, self.colorData
-
- if self._isXLog != isXLog or self._isYLog != isYLog:
- # Log state has changed
- self._isXLog, self._isYLog = isXLog, isYLog
-
- # Check if data <= 0. with log scale
- if (isXLog and self.xMin <= 0.) or (isYLog and self.yMin <= 0.):
- # Filtering data is needed
- xData, yData, colorData = self._logFilterData(
- self.xData, self.yData, self.colorData,
- self._isXLog, self._isYLog)
-
- self.discard() # discard existing VBOs
-
+ def prepare(self):
+ """Rendering preparation: build indices and bounding box vertices"""
if self.xVboData is None:
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
- dists = _distancesFromArrays(xData, yData)
+ dists = _distancesFromArrays(self.xData, self.yData)
if self.colorData is None:
xAttrib, yAttrib, dAttrib = vertexBuffer(
- (xData, yData, dists),
- prefix=(1, 1, 0), suffix=(1, 1, 0))
+ (self.xData, self.yData, dists))
else:
xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer(
- (xData, yData, colorData, dists),
- prefix=(1, 1, 0, 0), suffix=(1, 1, 0, 0))
+ (self.xData, self.yData, self.colorData, dists))
elif self.colorData is None:
- xAttrib, yAttrib = vertexBuffer(
- (xData, yData), prefix=(1, 1), suffix=(1, 1))
+ xAttrib, yAttrib = vertexBuffer((self.xData, self.yData))
else:
xAttrib, yAttrib, cAttrib = vertexBuffer(
- (xData, yData, colorData), prefix=(1, 1, 0))
-
- # Shrink VBO
- self.xVboData = xAttrib.copy()
- self.xVboData.size -= 2
- self.xVboData.offset += xAttrib.itemsize
+ (self.xData, self.yData, self.colorData))
- self.yVboData = yAttrib.copy()
- self.yVboData.size -= 2
- self.yVboData.offset += yAttrib.itemsize
+ self.xVboData = xAttrib
+ self.yVboData = yAttrib
+ self.distVboData = dAttrib
- if cAttrib is not None and colorData.dtype.kind == 'u':
+ if cAttrib is not None and self.colorData.dtype.kind == 'u':
cAttrib.normalization = True # Normalize uint to [0, 1]
self.colorVboData = cAttrib
self.useColorVboData = cAttrib is not None
- self.distVboData = dAttrib
-
- if self.fill is not None:
- xData = xData.reshape(xData.size, 1)
- zero = numpy.array((1e-32,), dtype=self.yData.dtype)
-
- # Add one point before data: (x0, 0.)
- xAttrib.vbo.update(xData[0], xAttrib.offset,
- xData[0].itemsize)
- yAttrib.vbo.update(zero, yAttrib.offset, zero.itemsize)
-
- # Add one point after data: (xN, 0.)
- xAttrib.vbo.update(xData[-1],
- xAttrib.offset +
- (xAttrib.size - 1) * xAttrib.itemsize,
- xData[-1].itemsize)
- yAttrib.vbo.update(zero,
- yAttrib.offset +
- (yAttrib.size - 1) * yAttrib.itemsize,
- zero.itemsize)
-
- self.fill.xFillVboData = xAttrib
- self.fill.yFillVboData = yAttrib
- self.fill.xMin, self.fill.yMin = self.xMin, self.yMin
- self.fill.xMax, self.fill.yMax = self.xMax, self.yMax
-
- self._errorBars.prepare(isXLog, isYLog)
def render(self, matrix, isXLog, isYLog):
- self.prepare(isXLog, isYLog)
+ """Perform rendering
+
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param bool isXLog:
+ :param bool isYLog:
+ """
+ self.prepare()
if self.fill is not None:
- self.fill.render(matrix, isXLog, isYLog)
- self._errorBars.render(matrix, isXLog, isYLog)
- self.lines.render(matrix, isXLog, isYLog)
- self.points.render(matrix, isXLog, isYLog)
+ self.fill.render(matrix)
+ self._errorBars.render(matrix)
+ self.lines.render(matrix)
+ self.points.render(matrix)
def discard(self):
+ """Release VBOs"""
if self.xVboData is not None:
self.xVboData.vbo.discard()
@@ -1234,6 +1054,8 @@ class GLPlotCurve2D(object):
self.distVboData = None
self._errorBars.discard()
+ if self.fill is not None:
+ self.fill.discard()
def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
"""Perform picking on the curve according to its rendering.
@@ -1251,19 +1073,29 @@ class GLPlotCurve2D(object):
if (self.marker is None and self.lineStyle is None) or \
self.xMin > xPickMax or xPickMin > self.xMax or \
self.yMin > yPickMax or yPickMin > self.yMax:
- # Note: With log scale the bounding box is too large if
- # some data <= 0.
return None
- elif self.lineStyle is not None:
+ # offset picking bounds
+ xPickMin = xPickMin - self.offset[0]
+ xPickMax = xPickMax - self.offset[0]
+ yPickMin = yPickMin - self.offset[1]
+ yPickMax = yPickMax - self.offset[1]
+
+ if self.lineStyle is not None:
# Using Cohen-Sutherland algorithm for line clipping
- codes = ((self.yData > yPickMax) << 3) | \
+ with warnings.catch_warnings(): # Ignore NaN comparison warnings
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ codes = ((self.yData > yPickMax) << 3) | \
((self.yData < yPickMin) << 2) | \
((self.xData > xPickMax) << 1) | \
(self.xData < xPickMin)
+ notNaN = numpy.logical_not(numpy.logical_or(
+ numpy.isnan(self.xData), numpy.isnan(self.yData)))
+
# Add all points that are inside the picking area
- indices = numpy.nonzero(codes == 0)[0].tolist()
+ indices = numpy.nonzero(
+ numpy.logical_and(codes == 0, notNaN))[0].tolist()
# Segment that might cross the area with no end point inside it
segToTestIdx = numpy.nonzero((codes[:-1] != 0) &
@@ -1309,9 +1141,11 @@ class GLPlotCurve2D(object):
indices.sort()
else:
- indices = numpy.nonzero((self.xData >= xPickMin) &
- (self.xData <= xPickMax) &
- (self.yData >= yPickMin) &
- (self.yData <= yPickMax))[0].tolist()
+ with warnings.catch_warnings(): # Ignore NaN comparison warnings
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ indices = numpy.nonzero((self.xData >= xPickMin) &
+ (self.xData <= xPickMax) &
+ (self.yData >= yPickMin) &
+ (self.yData <= yPickMax))[0].tolist()
return indices
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py
index eb101c4..4ad1547 100644
--- a/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ b/silx/gui/plot/backends/glutils/GLPlotFrame.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -35,6 +35,7 @@ __date__ = "03/04/2017"
# keep aspect ratio managed here?
# smarter dirty flag handling?
+import datetime as dt
import math
import weakref
import logging
@@ -47,7 +48,8 @@ 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__)
@@ -68,6 +70,8 @@ class PlotAxis(object):
self._plot = weakref.ref(plot)
+ self._isDateTime = False
+ self._timeZone = None
self._isLog = False
self._dataRange = 1., 100.
self._displayCoords = (0., 0.), (1., 0.)
@@ -110,6 +114,29 @@ class PlotAxis(object):
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
@@ -235,6 +262,10 @@ class PlotAxis(object):
(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)
@@ -269,19 +300,41 @@ class PlotAxis(object):
# Density of 1.3 label per 92 pixels
# i.e., 1.3 label per inch on a 92 dpi screen
- tickMin, tickMax, step, nbFrac = niceNumbersAdaptative(
- dataMin, dataMax, nbPixels, 1.3 / 92)
-
- 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)
+ 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 #################################################################
@@ -501,7 +554,8 @@ class GLPlotFrame(object):
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matProj)
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ matProj.astype(numpy.float32))
gl.glUniform4f(prog.uniforms['color'], 0., 0., 0., 1.)
gl.glUniform1f(prog.uniforms['tickFactor'], 0.)
@@ -534,7 +588,8 @@ class GLPlotFrame(object):
prog.use()
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matProj)
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ matProj.astype(numpy.float32))
gl.glUniform4f(prog.uniforms['color'], 0.7, 0.7, 0.7, 1.)
gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen
@@ -810,11 +865,11 @@ class GLPlotFrame2D(GLPlotFrame):
# Non-orthogonal axes
if self.baseVectors != self.DEFAULT_BASE_VECTORS:
(xx, xy), (yx, yy) = self.baseVectors
- mat = mat * numpy.matrix((
+ mat = numpy.dot(mat, numpy.array((
(xx, yx, 0., 0.),
(xy, yy, 0., 0.),
(0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64))
self._transformedDataProjMat = mat
@@ -839,11 +894,11 @@ class GLPlotFrame2D(GLPlotFrame):
# Non-orthogonal axes
if self.baseVectors != self.DEFAULT_BASE_VECTORS:
(xx, xy), (yx, yy) = self.baseVectors
- mat = mat * numpy.matrix((
+ mat = numpy.dot(mat, numpy.matrix((
(xx, yx, 0., 0.),
(xy, yy, 0., 0.),
(0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64))
self._transformedDataY2ProjMat = mat
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
index df5b289..6f3c487 100644
--- a/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ b/silx/gui/plot/backends/glutils/GLPlotImage.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -350,8 +350,11 @@ class GLPlotColormap(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
- mat = matrix * mat4Translate(*self.origin) * mat4Scale(*self.scale)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, mat)
+ mat = numpy.dot(numpy.dot(matrix,
+ mat4Translate(*self.origin)),
+ mat4Scale(*self.scale))
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ mat.astype(numpy.float32))
gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
@@ -377,9 +380,11 @@ class GLPlotColormap(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
- mat = mat4Translate(ox, oy) * mat4Scale(*self.scale)
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, mat)
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ matrix.astype(numpy.float32))
+ mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
+ gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
+ mat.astype(numpy.float32))
gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog)
@@ -598,8 +603,10 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
- mat = matrix * mat4Translate(*self.origin) * mat4Scale(*self.scale)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, mat)
+ mat = numpy.dot(numpy.dot(matrix, mat4Translate(*self.origin)),
+ mat4Scale(*self.scale))
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ mat.astype(numpy.float32))
gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
@@ -617,9 +624,11 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
- mat = mat4Translate(ox, oy) * mat4Scale(*self.scale)
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, mat)
+ gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
+ matrix.astype(numpy.float32))
+ mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
+ gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
+ mat.astype(numpy.float32))
gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog)
diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py
index 3f473be..18c5eb7 100644
--- a/silx/gui/plot/backends/glutils/GLSupport.py
+++ b/silx/gui/plot/backends/glutils/GLSupport.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -36,11 +36,20 @@ import numpy
from ...._glutils import gl
-def buildFillMaskIndices(nIndices):
- if nIndices <= numpy.iinfo(numpy.uint16).max + 1:
- dtype = numpy.uint16
- else:
- dtype = numpy.uint32
+def buildFillMaskIndices(nIndices, dtype=None):
+ """Returns triangle strip indices for rendering a filled polygon mask
+
+ :param int nIndices: Number of points
+ :param Union[numpy.dtype,None] dtype:
+ If specified the dtype of the returned indices array
+ :return: 1D array of indices constructing a triangle strip
+ :rtype: numpy.ndarray
+ """
+ if dtype is None:
+ if nIndices <= numpy.iinfo(numpy.uint16).max + 1:
+ dtype = numpy.uint16
+ else:
+ dtype = numpy.uint32
lastIndex = nIndices - 1
splitIndex = lastIndex // 2 + 1
@@ -158,35 +167,35 @@ class Shape2D(object):
def mat4Ortho(left, right, bottom, top, near, far):
"""Orthographic projection matrix (row-major)"""
- return numpy.matrix((
+ return numpy.array((
(2./(right - left), 0., 0., -(right+left)/float(right-left)),
(0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)),
(0., 0., -2./(far-near), -(far+near)/float(far-near)),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64)
def mat4Translate(x=0., y=0., z=0.):
"""Translation matrix (row-major)"""
- return numpy.matrix((
+ return numpy.array((
(1., 0., 0., x),
(0., 1., 0., y),
(0., 0., 1., z),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64)
def mat4Scale(sx=1., sy=1., sz=1.):
"""Scale matrix (row-major)"""
- return numpy.matrix((
+ return numpy.array((
(sx, 0., 0., 0.),
(0., sy, 0., 0.),
(0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64)
def mat4Identity():
"""Identity matrix"""
- return numpy.matrix((
+ return numpy.array((
(1., 0., 0., 0.),
(0., 1., 0., 0.),
(0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ (0., 0., 0., 1.)), dtype=numpy.float64)
diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py
index cef0c5a..1540e26 100644
--- a/silx/gui/plot/backends/glutils/GLText.py
+++ b/silx/gui/plot/backends/glutils/GLText.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -195,8 +195,9 @@ class Text2D(object):
gl.glUniform1i(prog.uniforms['texText'], texUnit)
+ mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y)))
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matrix * mat4Translate(int(self.x), int(self.y)))
+ mat.astype(numpy.float32))
gl.glUniform4f(prog.uniforms['color'], *self.color)
if self.bgColor is not None: