summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2021-01-06 14:10:12 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2021-01-06 14:10:12 +0100
commitb3bea947efa55d2c0f198b6c6795b3177be27f45 (patch)
tree4116758aafe4483bf472c1d54b519e685737fd77 /silx/gui/plot/backends
parent5ad425ff4e62f5e003178813ebd073577679a00e (diff)
New upstream version 0.14.0+dfsg
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-xsilx/gui/plot/backends/BackendBase.py25
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py149
-rwxr-xr-xsilx/gui/plot/backends/BackendOpenGL.py426
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py86
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py159
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py103
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotItem.py94
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py14
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py60
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py5
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py3
11 files changed, 701 insertions, 423 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index bcc93a5..6fc1aa7 100755
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -58,8 +58,8 @@ class BackendBase(object):
self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)}
self.__yAxisInverted = False
self.__keepDataAspectRatio = False
+ self.__xAxisTimeSeries = False
self._xAxisTimeZone = None
- self._axesDisplayed = True
# Store a weakref to get access to the plot state.
self._setPlot(plot)
@@ -457,14 +457,14 @@ class BackendBase(object):
:rtype: bool
"""
- raise NotImplementedError()
+ return self.__xAxisTimeSeries
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()
+ self.__xAxisTimeSeries = bool(isTimeSeries)
def setXAxisLogarithmic(self, flag):
"""Set the X axis scale between linear and log.
@@ -548,20 +548,17 @@ class BackendBase(object):
"""
raise NotImplementedError()
- def setAxesDisplayed(self, displayed):
- """Display or not the axes.
+ def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
+ """Set the size of plot margins as ratios.
- :param bool displayed: If `True` axes are displayed. If `False` axes
- are not anymore visible and the margin used for them is removed.
- """
- self._axesDisplayed = displayed
+ Values are expected in [0., 1.]
- def isAxesDisplayed(self):
- """private because in some case it is possible that one of the two axes
- are displayed and not the other.
- This only check status set to axes from the public API
+ :param float left:
+ :param float top:
+ :param float right:
+ :param float bottom:
"""
- return self._axesDisplayed
+ pass
def setForegroundColors(self, foregroundColor, gridColor):
"""Set foreground and grid colors used to display this widget.
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 036e630..140672f 100755
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -33,6 +33,7 @@ __date__ = "21/12/2018"
import logging
import datetime as dt
+from typing import Tuple
import numpy
from pkg_resources import parse_version as _parse_version
@@ -44,7 +45,7 @@ _logger = logging.getLogger(__name__)
from ... import qt
# First of all init matplotlib and set its backend
-from ..matplotlib import FigureCanvasQTAgg
+from ...utils.matplotlib import FigureCanvasQTAgg
import matplotlib
from matplotlib.container import Container
from matplotlib.figure import Figure
@@ -593,7 +594,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if (len(color) == 4 and
type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float) / 255.
+ color = numpy.array(color, dtype=numpy.float64) / 255.
if yaxis == "right":
axes = self.ax2
@@ -601,7 +602,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
axes = self.ax
- picker = 3
+ pickradius = 3
artists = [] # All the artists composing the curve
@@ -627,7 +628,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if hasattr(color, 'dtype') and len(color) == len(x):
# scatter plot
- if color.dtype not in [numpy.float32, numpy.float]:
+ if color.dtype not in [numpy.float32, numpy.float64]:
actualColor = color / 255.
else:
actualColor = color
@@ -639,7 +640,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
linestyle=linestyle,
color=actualColor[0],
linewidth=linewidth,
- picker=picker,
+ picker=True,
+ pickradius=pickradius,
marker=None)
artists += list(curveList)
@@ -647,7 +649,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
scatter = axes.scatter(x, y,
color=actualColor,
marker=marker,
- picker=picker,
+ picker=True,
+ pickradius=pickradius,
s=symbolsize**2)
artists.append(scatter)
@@ -665,7 +668,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
color=color,
linewidth=linewidth,
marker=symbol,
- picker=picker,
+ picker=True,
+ pickradius=pickradius,
markersize=symbolsize)
artists += list(curveList)
@@ -744,13 +748,13 @@ class BackendMatplotlib(BackendBase.BackendBase):
color = numpy.array(color, copy=False)
assert color.ndim == 2 and len(color) == len(x)
- if color.dtype not in [numpy.float32, numpy.float]:
+ if color.dtype not in [numpy.float32, numpy.float64]:
color = color.astype(numpy.float32) / 255.
collection = TriMesh(
Triangulation(x, y, triangles),
alpha=alpha,
- picker=0) # 0 enables picking on filled triangle
+ pickradius=0) # 0 enables picking on filled triangle
collection.set_color(color)
self.ax.add_collection(collection)
@@ -893,7 +897,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
raise RuntimeError('A marker must at least have one coordinate')
- line.set_picker(5)
+ line.set_picker(True)
+ line.set_pickradius(5)
# All markers are overlays
line.set_animated(True)
@@ -1014,7 +1019,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
lambda item: item.isVisible() and item._backendRenderer is not None)
count = len(items)
for index, item in enumerate(items):
- zorder = 1. + index / count
+ if item.getZValue() < 0.5:
+ # Make sure matplotlib z order is below the grid (with z=0.5)
+ zorder = 0.5 * index / count
+ else: # Make sure matplotlib z order is above the grid (> 0.5)
+ zorder = 1. + index / count
if zorder != item._backendRenderer.get_zorder():
item._backendRenderer.set_zorder(zorder)
@@ -1196,67 +1205,58 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Data <-> Pixel coordinates conversion
- def _mplQtYAxisCoordConversion(self, y, asint=True):
- """Qt origin (top) to/from matplotlib origin (bottom) conversion.
+ def _getDevicePixelRatio(self) -> float:
+ """Compatibility wrapper for devicePixelRatioF"""
+ return 1.
- :param y:
- :param bool asint: True to cast to int, False to keep as float
+ def _mplToQtPosition(self, x: float, y: float) -> Tuple[float, float]:
+ """Convert matplotlib "display" space coord to Qt widget logical pixel
+ """
+ ratio = self._getDevicePixelRatio()
+ # Convert from matplotlib origin (bottom) to Qt origin (top)
+ # and apply device pixel ratio
+ return x / ratio, (self.fig.get_window_extent().height - y) / ratio
- :rtype: float
+ def _qtToMplPosition(self, x: float, y: float) -> Tuple[float, float]:
+ """Convert Qt widget logical pixel to matplotlib "display" space coord
"""
- value = self.fig.get_window_extent().height - y
- return int(value) if asint else value
+ ratio = self._getDevicePixelRatio()
+ # Apply device pixel ration and
+ # convert from Qt origin (top) to matplotlib origin (bottom)
+ return x * ratio, self.fig.get_window_extent().height - (y * ratio)
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, asint=False)
-
- return xPixel, yPixel
+ displayPos = ax.transData.transform_point((x, y)).transpose()
+ return self._mplToQtPosition(*displayPos)
def pixelToData(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
-
- # Convert from Qt origin (top) to matplotlib origin (bottom)
- y = self._mplQtYAxisCoordConversion(y, asint=False)
-
- inv = ax.transData.inverted()
- x, y = inv.transform_point((x, y))
- return x, y
+ displayPos = self._qtToMplPosition(x, y)
+ return tuple(ax.transData.inverted().transform_point(displayPos))
def getPlotBoundsInPixels(self):
bbox = self.ax.get_window_extent()
# Warning this is not returning int...
- return (int(bbox.xmin),
- self._mplQtYAxisCoordConversion(bbox.ymax, asint=True),
- int(bbox.width),
- int(bbox.height))
+ ratio = self._getDevicePixelRatio()
+ return tuple(int(value / ratio) for value in (
+ bbox.xmin,
+ self.fig.get_window_extent().height - bbox.ymax,
+ bbox.width,
+ bbox.height))
- def setAxesDisplayed(self, displayed):
- """Display or not the axes.
+ def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
+ width, height = 1. - left - right, 1. - top - bottom
+ position = left, bottom, width, height
+
+ # Toggle display of axes and viewbox rect
+ isFrameOn = position != (0., 0., 1., 1.)
+ self.ax.set_frame_on(isFrameOn)
+ self.ax2.set_frame_on(isFrameOn)
+
+ self.ax.set_position(position)
+ self.ax2.set_position(position)
- :param bool displayed: If `True` axes are displayed. If `False` axes
- are not anymore visible and the margin used for them is removed.
- """
- BackendBase.BackendBase.setAxesDisplayed(self, displayed)
- if displayed:
- # show axes and viewbox rect
- self.ax.set_frame_on(True)
- self.ax2.set_frame_on(True)
- # set the default margins
- self.ax.set_position([.15, .15, .75, .75])
- self.ax2.set_position([.15, .15, .75, .75])
- else:
- # hide axes and viewbox rect
- self.ax.set_frame_on(False)
- self.ax2.set_frame_on(False)
- # remove external margins
- self.ax.set_position([0, 0, 1, 1])
- self.ax2.set_position([0, 0, 1, 1])
self._synchronizeBackgroundColors()
self._synchronizeForegroundColors()
self._plot._setDirtyPlot()
@@ -1349,6 +1349,15 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def postRedisplay(self):
self._sigPostRedisplay.emit()
+ def _getDevicePixelRatio(self) -> float:
+ """Compatibility wrapper for devicePixelRatioF"""
+ if hasattr(self, 'devicePixelRatioF'):
+ ratio = self.devicePixelRatioF()
+ else: # Qt < 5.6 compatibility
+ ratio = float(self.devicePixelRatio())
+ # Safety net: avoid returning 0
+ return ratio if ratio != 0. else 1.
+
# Mouse event forwarding
_MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
@@ -1356,17 +1365,14 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def _onMousePress(self, event):
button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
if button is not None:
- self._plot.onMousePress(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- button)
+ x, y = self._mplToQtPosition(event.x, event.y)
+ self._plot.onMousePress(int(x), int(y), button)
def _onMouseMove(self, event):
+ x, y = self._mplToQtPosition(event.x, event.y)
if self._graphCursor:
position = self._plot.pixelToData(
- event.x,
- self._mplQtYAxisCoordConversion(event.y),
- axis='left',
- check=True)
+ x, y, axis='left', check=True)
lineh, linev = self._graphCursor
if position is not None:
linev.set_visible(True)
@@ -1380,19 +1386,17 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self._plot._setDirtyPlot(overlayOnly=True)
# onMouseMove must trigger replot if dirty flag is raised
- self._plot.onMouseMove(
- event.x, self._mplQtYAxisCoordConversion(event.y))
+ self._plot.onMouseMove(int(x), int(y))
def _onMouseRelease(self, event):
button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
if button is not None:
- self._plot.onMouseRelease(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- button)
+ x, y = self._mplToQtPosition(event.x, event.y)
+ self._plot.onMouseRelease(int(x), int(y), button)
def _onMouseWheel(self, event):
- self._plot.onMouseWheel(
- event.x, self._mplQtYAxisCoordConversion(event.y), event.step)
+ x, y = self._mplToQtPosition(event.x, event.y)
+ self._plot.onMouseWheel(int(x), int(y), event.step)
def leaveEvent(self, event):
"""QWidget event handler"""
@@ -1406,8 +1410,9 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
# picking
def pickItem(self, x, y, item):
+ xDisplay, yDisplay = self._qtToMplPosition(x, y)
mouseEvent = MouseEvent(
- 'button_press_event', self, x, self._mplQtYAxisCoordConversion(y))
+ 'button_press_event', self, int(xDisplay), int(yDisplay))
# Override axes and data position with the axes
mouseEvent.inaxes = item.axes
mouseEvent.xdata, mouseEvent.ydata = self.pixelToData(
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index cf1da31..909d18a 100755
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -43,12 +43,7 @@ from ... import qt
from ..._glutils import gl
from ... import _glutils as glu
-from .glutils import (
- GLLines2D, GLPlotTriangles,
- GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D,
- mat4Ortho, mat4Identity,
- LEFT, RIGHT, BOTTOM, TOP,
- Text2D, FilledShape2D)
+from . import glutils
from .glutils.PlotImageFile import saveImageToFile
_logger = logging.getLogger(__name__)
@@ -216,7 +211,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._backgroundColor = 1., 1., 1., 1.
self._dataBackgroundColor = 1., 1., 1., 1.
- self.matScreenProj = mat4Identity()
+ self.matScreenProj = glutils.mat4Identity()
self._progBase = glu.Program(
_baseVertShd, _baseFragShd, attrib0='position')
@@ -231,10 +226,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._glGarbageCollector = []
- self._plotFrame = GLPlotFrame2D(
+ self._plotFrame = glutils.GLPlotFrame2D(
foregroundColor=(0., 0., 0., 1.),
gridColor=(.7, .7, .7, 1.),
- margins={'left': 100, 'right': 50, 'top': 50, 'bottom': 50})
+ marginRatios=(.15, .1, .1, .15))
+ self._plotFrame.size = ( # Init size with size int
+ int(self.getDevicePixelRatio() * 640),
+ int(self.getDevicePixelRatio() * 480))
# Make postRedisplay asynchronous using Qt signal
self._sigPostRedisplay.connect(
@@ -254,50 +252,43 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def mousePressEvent(self, event):
if event.button() not in self._MOUSE_BTNS:
return super(BackendOpenGL, self).mousePressEvent(event)
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
- btn = self._MOUSE_BTNS[event.button()]
- self._plot.onMousePress(xPixel, yPixel, btn)
+ self._plot.onMousePress(
+ event.x(), event.y(), self._MOUSE_BTNS[event.button()])
event.accept()
def mouseMoveEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
- # Handle crosshair
- inXPixel, inYPixel = self._mouseInPlotArea(xPixel, yPixel)
- isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
+ qtPos = event.x(), event.y()
previousMousePosInPixels = self._mousePosInPixels
- self._mousePosInPixels = (xPixel, yPixel) if isCursorInPlot else None
+ if qtPos == self._mouseInPlotArea(*qtPos):
+ devicePixelRatio = self.getDevicePixelRatio()
+ devicePos = qtPos[0] * devicePixelRatio, qtPos[1] * devicePixelRatio
+ self._mousePosInPixels = devicePos # Mouse in plot area
+ else:
+ self._mousePosInPixels = None # Mouse outside plot area
+
if (self._crosshairCursor is not None and
previousMousePosInPixels != self._mousePosInPixels):
# Avoid replot when cursor remains outside plot area
self._plot._setDirtyPlot(overlayOnly=True)
- self._plot.onMouseMove(xPixel, yPixel)
+ self._plot.onMouseMove(*qtPos)
event.accept()
def mouseReleaseEvent(self, event):
if event.button() not in self._MOUSE_BTNS:
return super(BackendOpenGL, self).mouseReleaseEvent(event)
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
- btn = self._MOUSE_BTNS[event.button()]
- self._plot.onMouseRelease(xPixel, yPixel, btn)
+ self._plot.onMouseRelease(
+ event.x(), event.y(), self._MOUSE_BTNS[event.button()])
event.accept()
def wheelEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
if hasattr(event, 'angleDelta'): # Qt 5
delta = event.angleDelta().y()
else: # Qt 4 support
delta = event.delta()
angleInDegrees = delta / 8.
- self._plot.onMouseWheel(xPixel, yPixel, angleInDegrees)
+ self._plot.onMouseWheel(event.x(), event.y(), angleInDegrees)
event.accept()
def leaveEvent(self, _):
@@ -371,7 +362,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().astype(numpy.float32))
+ glutils.mat4Identity().astype(numpy.float32))
gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
gl.glVertexAttribPointer(self._progTex.attributes['position'],
@@ -405,10 +396,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
# Check if window is large enough
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- if plotWidth <= 2 or plotHeight <= 2:
+ if self._plotFrame.plotSize <= (2, 2):
return
+ # Sync plot frame with window
+ self._plotFrame.devicePixelRatio = self.getDevicePixelRatio()
# self._paintDirectGL()
self._paintFBOGL()
@@ -422,7 +414,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
True to render items that are overlays.
"""
# Values that are often used
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ plotWidth, plotHeight = self._plotFrame.plotSize
isXLog = self._plotFrame.xAxis.isLog
isYLog = self._plotFrame.yAxis.isLog
isYInverted = self._plotFrame.isYAxisInverted
@@ -431,6 +423,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
labels = []
pixelOffset = 3
+ context = glutils.RenderContext(
+ isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch())
+
for plotItem in self.getItemsFromBackToFront(
condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
if plotItem._backendRenderer is None:
@@ -438,20 +433,16 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
item = plotItem._backendRenderer
- if isinstance(item, (GLPlotCurve2D,
- GLPlotColormap,
- GLPlotRGBAImage,
- GLPlotTriangles)): # Render data items
+ if isinstance(item, glutils.GLPlotItem): # Render data items
gl.glViewport(self._plotFrame.margins.left,
self._plotFrame.margins.bottom,
plotWidth, plotHeight)
-
- if isinstance(item, GLPlotCurve2D) and item.info.get('yAxis') == 'right':
- item.render(self._plotFrame.transformedDataY2ProjMat,
- isXLog, isYLog)
+ # Set matrix
+ if item.yaxis == 'right':
+ context.matrix = self._plotFrame.transformedDataY2ProjMat
else:
- item.render(self._plotFrame.transformedDataProjMat,
- isXLog, isYLog)
+ context.matrix = self._plotFrame.transformedDataProjMat
+ item.render(context)
elif isinstance(item, _ShapeItem): # Render shape items
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
@@ -463,53 +454,67 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if item['shape'] == 'hline':
width = self._plotFrame.size[0]
- _, yPixel = self._plot.dataToPixel(
- None, item['y'], axis='left', check=False)
- points = numpy.array(((0., yPixel), (width, yPixel)),
- dtype=numpy.float32)
+ _, yPixel = self._plotFrame.dataToPixel(
+ 0.5 * sum(self._plotFrame.dataRanges[0]),
+ item['y'],
+ axis='left')
+ subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
+ dtype=numpy.float32)]
elif item['shape'] == 'vline':
- xPixel, _ = self._plot.dataToPixel(
- item['x'], None, axis='left', check=False)
+ xPixel, _ = self._plotFrame.dataToPixel(
+ item['x'],
+ 0.5 * sum(self._plotFrame.dataRanges[1]),
+ axis='left')
height = self._plotFrame.size[1]
- points = numpy.array(((xPixel, 0), (xPixel, height)),
- dtype=numpy.float32)
+ subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
+ dtype=numpy.float32)]
else:
- points = numpy.array([
- self._plot.dataToPixel(x, y, axis='left', check=False)
- for (x, y) in zip(item['x'], item['y'])])
-
- # Draw the fill
- if (item['fill'] is not None and
- item['shape'] not in ('hline', 'vline')):
- self._progBase.use()
- gl.glUniformMatrix4fv(
- self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
-
- shape2D = FilledShape2D(
- points, style=item['fill'], color=item['color'])
- shape2D.render(
- posAttrib=self._progBase.attributes['position'],
- colorUnif=self._progBase.uniforms['color'],
- hatchStepUnif=self._progBase.uniforms['hatchStep'])
-
- # Draw the stroke
- if item['linestyle'] not in ('', ' ', None):
- if item['shape'] != 'polylines':
- # close the polyline
- points = numpy.append(points,
- numpy.atleast_2d(points[0]), axis=0)
-
- lines = GLLines2D(points[:, 0], points[:, 1],
- style=item['linestyle'],
- color=item['color'],
- dash2ndColor=item['linebgcolor'],
- width=item['linewidth'])
- lines.render(self.matScreenProj)
+ # Split sub-shapes at not finite values
+ splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
+ numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
+ splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
+ subShapes = []
+ for begin, end in zip(splits[:-1] + 1, splits[1:]):
+ if end > begin:
+ subShapes.append(numpy.array([
+ self._plotFrame.dataToPixel(x, y, axis='left')
+ for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))
+
+ for points in subShapes: # Draw each sub-shape
+ # Draw the fill
+ if (item['fill'] is not None and
+ item['shape'] not in ('hline', 'vline')):
+ self._progBase.use()
+ gl.glUniformMatrix4fv(
+ self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
+ self.matScreenProj.astype(numpy.float32))
+ gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
+ gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
+
+ shape2D = glutils.FilledShape2D(
+ points, style=item['fill'], color=item['color'])
+ shape2D.render(
+ posAttrib=self._progBase.attributes['position'],
+ colorUnif=self._progBase.uniforms['color'],
+ hatchStepUnif=self._progBase.uniforms['hatchStep'])
+
+ # Draw the stroke
+ if item['linestyle'] not in ('', ' ', None):
+ if item['shape'] != 'polylines':
+ # close the polyline
+ points = numpy.append(points,
+ numpy.atleast_2d(points[0]), axis=0)
+
+ lines = glutils.GLLines2D(
+ points[:, 0], points[:, 1],
+ style=item['linestyle'],
+ color=item['color'],
+ dash2ndColor=item['linebgcolor'],
+ width=item['linewidth'])
+ context.matrix = self.matScreenProj
+ lines.render(context)
elif isinstance(item, _MarkerItem):
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
@@ -522,76 +527,103 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
continue
if xCoord is None or yCoord is None:
- pixelPos = self._plot.dataToPixel(
- xCoord, yCoord, axis=yAxis, check=False)
-
if xCoord is None: # Horizontal line in data space
+ pixelPos = self._plotFrame.dataToPixel(
+ 0.5 * sum(self._plotFrame.dataRanges[0]),
+ yCoord,
+ axis=yAxis)
+
if item['text'] is not None:
x = self._plotFrame.size[0] - \
self._plotFrame.margins.right - pixelOffset
y = pixelPos[1] - pixelOffset
- label = Text2D(item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=RIGHT, valign=BOTTOM)
+ label = glutils.Text2D(
+ item['text'], x, y,
+ color=item['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=glutils.RIGHT,
+ valign=glutils.BOTTOM,
+ devicePixelRatio=self.getDevicePixelRatio())
labels.append(label)
width = self._plotFrame.size[0]
- lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
- lines.render(self.matScreenProj)
+ lines = glutils.GLLines2D(
+ (0, width), (pixelPos[1], pixelPos[1]),
+ style=item['linestyle'],
+ color=item['color'],
+ width=item['linewidth'])
+ context.matrix = self.matScreenProj
+ lines.render(context)
else: # yCoord is None: vertical line in data space
+ yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2]
+ pixelPos = self._plotFrame.dataToPixel(
+ xCoord, 0.5 * sum(yRange), axis=yAxis)
+
if item['text'] is not None:
x = pixelPos[0] + pixelOffset
y = self._plotFrame.margins.top + pixelOffset
- label = Text2D(item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
+ label = glutils.Text2D(
+ item['text'], x, y,
+ color=item['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=glutils.LEFT,
+ valign=glutils.TOP,
+ devicePixelRatio=self.getDevicePixelRatio())
labels.append(label)
height = self._plotFrame.size[1]
- lines = GLLines2D((pixelPos[0], pixelPos[0]), (0, height),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
- lines.render(self.matScreenProj)
+ lines = glutils.GLLines2D(
+ (pixelPos[0], pixelPos[0]), (0, height),
+ style=item['linestyle'],
+ color=item['color'],
+ width=item['linewidth'])
+ context.matrix = self.matScreenProj
+ lines.render(context)
else:
- pixelPos = self._plot.dataToPixel(
- xCoord, yCoord, axis=yAxis, check=True)
- if pixelPos is None:
+ xmin, xmax = self._plot.getXAxis().getLimits()
+ ymin, ymax = self._plot.getYAxis(axis=yAxis).getLimits()
+ if not xmin < xCoord < xmax or not ymin < yCoord < ymax:
# Do not render markers outside visible plot area
continue
+ pixelPos = self._plotFrame.dataToPixel(
+ xCoord, yCoord, axis=yAxis)
if isYInverted:
- valign = BOTTOM
+ valign = glutils.BOTTOM
vPixelOffset = -pixelOffset
else:
- valign = TOP
+ valign = glutils.TOP
vPixelOffset = pixelOffset
if item['text'] is not None:
x = pixelPos[0] + pixelOffset
y = pixelPos[1] + vPixelOffset
- label = Text2D(item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=valign)
+ label = glutils.Text2D(
+ item['text'], x, y,
+ color=item['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=glutils.LEFT,
+ valign=valign,
+ devicePixelRatio=self.getDevicePixelRatio())
labels.append(label)
# For now simple implementation: using a curve for each marker
# Should pack all markers to a single set of points
- markerCurve = GLPlotCurve2D(
+ markerCurve = glutils.GLPlotCurve2D(
numpy.array((pixelPos[0],), dtype=numpy.float64),
numpy.array((pixelPos[1],), dtype=numpy.float64),
marker=item['symbol'],
markerColor=item['color'],
markerSize=11)
- markerCurve.render(self.matScreenProj, False, False)
+
+ context = glutils.RenderContext(
+ matrix=self.matScreenProj,
+ isXLog=False,
+ isYLog=False,
+ dpi=self.getDotsPerInch())
+ markerCurve.render(context)
markerCurve.discard()
else:
@@ -605,7 +637,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def _renderOverlayGL(self):
"""Render overlay layer: overlay items and crosshair."""
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ plotWidth, plotHeight = self._plotFrame.plotSize
# Scissor to plot area
gl.glScissor(self._plotFrame.margins.left,
@@ -658,7 +690,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
It renders the background, grid and items except overlays
"""
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ plotWidth, plotHeight = self._plotFrame.plotSize
gl.glScissor(self._plotFrame.margins.left,
self._plotFrame.margins.bottom,
@@ -687,9 +719,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
int(self.getDevicePixelRatio() * width),
int(self.getDevicePixelRatio() * height))
- self.matScreenProj = mat4Ortho(0, self._plotFrame.size[0],
- self._plotFrame.size[1], 0,
- 1, -1)
+ self.matScreenProj = glutils.mat4Ortho(
+ 0, self._plotFrame.size[0],
+ self._plotFrame.size[1], 0,
+ 1, -1)
# Store current ranges
previousXRange = self.getGraphXLimits()
@@ -824,21 +857,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
fillColor = None
if fill is True:
fillColor = color
- curve = GLPlotCurve2D(x, y, colorArray,
- xError=xerror,
- yError=yerror,
- lineStyle=linestyle,
- lineColor=color,
- lineWidth=linewidth,
- marker=symbol,
- markerColor=color,
- markerSize=symbolsize,
- fillColor=fillColor,
- baseline=baseline,
- isYLog=isYLog)
- curve.info = {
- 'yAxis': 'left' if yaxis is None else yaxis,
- }
+ curve = glutils.GLPlotCurve2D(
+ x, y, colorArray,
+ xError=xerror,
+ yError=yerror,
+ lineStyle=linestyle,
+ lineColor=color,
+ lineWidth=linewidth,
+ marker=symbol,
+ markerColor=color,
+ markerSize=symbolsize,
+ fillColor=fillColor,
+ baseline=baseline,
+ isYLog=isYLog)
+ curve.yaxis = 'left' if yaxis is None else yaxis
if yaxis == "right":
self._plotFrame.isY2Axis = True
@@ -853,7 +885,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if data.ndim == 2:
# Ensure array is contiguous and eventually convert its type
- if data.dtype in (numpy.float32, numpy.uint8, numpy.uint16):
+ dtypes = [dtype for dtype in (
+ numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
+ if glu.isSupportedGLType(dtype)]
+ if data.dtype in dtypes:
data = numpy.array(data, copy=False, order='C')
else:
_logger.info(
@@ -861,24 +896,27 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
data = numpy.array(data, dtype=numpy.float32, order='C')
normalization = colormap.getNormalization()
- if normalization in GLPlotColormap.SUPPORTED_NORMALIZATIONS:
+ if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS:
# Fast path applying colormap on the GPU
cmapRange = colormap.getColormapRange(data=data)
colormapLut = colormap.getNColors(nbColors=256)
gamma = colormap.getGammaNormalizationParameter()
-
- image = GLPlotColormap(data,
- origin,
- scale,
- colormapLut,
- normalization,
- gamma,
- cmapRange,
- alpha)
+ nanColor = colors.rgba(colormap.getNaNColor())
+
+ image = glutils.GLPlotColormap(
+ data,
+ origin,
+ scale,
+ colormapLut,
+ normalization,
+ gamma,
+ cmapRange,
+ alpha,
+ nanColor)
else: # Fallback applying colormap on CPU
rgba = colormap.applyToData(data)
- image = GLPlotRGBAImage(rgba, origin, scale, alpha)
+ image = glutils.GLPlotRGBAImage(rgba, origin, scale, alpha)
elif len(data.shape) == 3:
# For RGB, RGBA data
@@ -893,7 +931,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
else:
raise ValueError('Unsupported data type')
- image = GLPlotRGBAImage(data, origin, scale, alpha)
+ image = glutils.GLPlotRGBAImage(data, origin, scale, alpha)
else:
raise RuntimeError("Unsupported data shape {0}".format(data.shape))
@@ -916,7 +954,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if self._plotFrame.yAxis.isLog:
y = numpy.log10(y)
- triangles = GLPlotTriangles(x, y, color, triangles, alpha)
+ triangles = glutils.GLPlotTriangles(x, y, color, triangles, alpha)
return triangles
@@ -944,11 +982,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Remove methods
def remove(self, item):
- if isinstance(item, (GLPlotCurve2D,
- GLPlotColormap,
- GLPlotRGBAImage,
- GLPlotTriangles)):
- if isinstance(item, GLPlotCurve2D):
+ if isinstance(item, glutils.GLPlotItem):
+ if item.yaxis == 'right':
# Check if some curves remains on the right Y axis
y2AxisItems = (item for item in self._plot.getItems()
if isinstance(item, items.YAxisMixIn) and
@@ -997,13 +1032,18 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
_PICK_OFFSET = 3 # Offset in pixel used for picking
def _mouseInPlotArea(self, x, y):
- xPlot = numpy.clip(
- x, self._plotFrame.margins.left,
- self._plotFrame.size[0] - self._plotFrame.margins.right - 1)
- yPlot = numpy.clip(
- y, self._plotFrame.margins.top,
- self._plotFrame.size[1] - self._plotFrame.margins.bottom - 1)
- return xPlot, yPlot
+ """Returns closest visible position in the plot.
+
+ This is performed in Qt widget pixel, not device pixel.
+
+ :param float x: X coordinate in Qt widget pixel
+ :param float y: Y coordinate in Qt widget pixel
+ :return: (x, y) closest point in the plot.
+ :rtype: List[float]
+ """
+ left, top, width, height = self.getPlotBoundsInPixels()
+ return (numpy.clip(x, left, left + width - 1), # TODO -1?
+ numpy.clip(y, top, top + height - 1))
def __pickCurves(self, item, x, y):
"""Perform picking on a curve item.
@@ -1016,22 +1056,26 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
"""
offset = self._PICK_OFFSET
if item.marker is not None:
- offset = max(item.markerSize / 2., offset)
+ # Convert markerSize from points to qt pixels
+ qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
+ size = item.markerSize / 72. * qtDpi
+ offset = max(size / 2., offset)
if item.lineStyle is not None:
- offset = max(item.lineWidth / 2., offset)
-
- yAxis = item.info['yAxis']
+ # Convert line width from points to qt pixels
+ qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
+ lineWidth = item.lineWidth / 72. * qtDpi
+ offset = max(lineWidth / 2., offset)
inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
+ axis=item.yaxis, check=True)
if dataPos is None:
return None
xPick0, yPick0 = dataPos
inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
+ axis=item.yaxis, check=True)
if dataPos is None:
return None
xPick1, yPick1 = dataPos
@@ -1051,8 +1095,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPickMin = numpy.log10(xPickMin)
xPickMax = numpy.log10(xPickMax)
- if (yAxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yAxis == 'right' and self._plotFrame.y2Axis.isLog):
+ if (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
+ item.yaxis == 'right' and self._plotFrame.y2Axis.isLog):
yPickMin = numpy.log10(yPickMin)
yPickMax = numpy.log10(yPickMax)
@@ -1060,6 +1104,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPickMax, yPickMax)
def pickItem(self, x, y, item):
+ # Picking is performed in Qt widget pixels not device pixels
dataPos = self._plot.pixelToData(x, y, axis='left', check=True)
if dataPos is None:
return None # Outside plot area
@@ -1100,17 +1145,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return (0,) if isPicked else None
# Pick image, curve, triangles
- elif isinstance(item, (GLPlotCurve2D,
- GLPlotColormap,
- GLPlotRGBAImage,
- GLPlotTriangles)):
- if isinstance(item, (GLPlotColormap, GLPlotRGBAImage, GLPlotTriangles)):
- return item.pick(*dataPos) # Might be None
-
- elif isinstance(item, GLPlotCurve2D):
+ elif isinstance(item, glutils.GLPlotItem):
+ if isinstance(item, glutils.GLPlotCurve2D):
return self.__pickCurves(item, x, y)
else:
- return None
+ return item.pick(*dataPos) # Might be None
# Update curve
@@ -1184,8 +1223,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if axis == 'left':
self._plotFrame.yAxis.title = label
else: # right axis
- if label:
- _logger.warning('Right axis label not implemented')
+ self._plotFrame.y2Axis.title = label
# Graph limits
@@ -1209,7 +1247,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
:param str keepDim: The dimension to maintain: 'x', 'y' or None.
If None (the default), the dimension with the largest range.
"""
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ plotWidth, plotHeight = self._plotFrame.plotSize
if plotWidth <= 2 or plotHeight <= 2:
return
@@ -1352,17 +1390,25 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Data <-> Pixel coordinates conversion
def dataToPixel(self, x, y, axis):
- return self._plotFrame.dataToPixel(x, y, axis)
+ result = self._plotFrame.dataToPixel(x, y, axis)
+ if result is None:
+ return None
+ else:
+ devicePixelRatio = self.getDevicePixelRatio()
+ return tuple(value/devicePixelRatio for value in result)
def pixelToData(self, x, y, axis):
- return self._plotFrame.pixelToData(x, y, axis)
+ devicePixelRatio = self.getDevicePixelRatio()
+ return self._plotFrame.pixelToData(
+ x * devicePixelRatio, y * devicePixelRatio, axis)
def getPlotBoundsInPixels(self):
- return self._plotFrame.plotOrigin + self._plotFrame.plotSize
+ devicePixelRatio = self.getDevicePixelRatio()
+ return tuple(int(value / devicePixelRatio)
+ for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize)
- def setAxesDisplayed(self, displayed):
- BackendBase.BackendBase.setAxesDisplayed(self, displayed)
- self._plotFrame.displayed = displayed
+ def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
+ self._plotFrame.marginRatios = left, top, right, bottom
def setForegroundColors(self, foregroundColor, gridColor):
self._plotFrame.foregroundColor = foregroundColor
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 9ab85fd..c4e2c1e 100644
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -43,6 +43,7 @@ from silx.math.combo import min_max
from ...._glutils import gl
from ...._glutils import Program, vertexBuffer, VertexBufferAttrib
from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate
+from .GLPlotImage import GLPlotItem
_logger = logging.getLogger(__name__)
@@ -172,10 +173,10 @@ class _Fill2D(object):
self._xFillVboData, self._yFillVboData = vertexBuffer(points.T)
- def render(self, matrix):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param RenderContext context:
"""
self.prepare()
@@ -186,7 +187,7 @@ class _Fill2D(object):
gl.glUniformMatrix4fv(
self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- numpy.dot(matrix,
+ numpy.dot(context.matrix,
mat4Translate(*self.offset)).astype(numpy.float32))
gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color)
@@ -404,11 +405,13 @@ class GLLines2D(object):
"""OpenGL context initialization"""
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
- def render(self, matrix):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param RenderContext context:
"""
+ width = self.width / 72. * context.dpi
+
style = self.style
if style is None:
return
@@ -425,7 +428,7 @@ class GLLines2D(object):
gl.glUniform2f(program.uniforms['halfViewportSize'],
0.5 * viewWidth, 0.5 * viewHeight)
- dashPeriod = self.dashPeriod * self.width
+ dashPeriod = self.dashPeriod * width
if self.style == DOTTED:
dash = (0.2 * dashPeriod,
0.5 * dashPeriod,
@@ -463,10 +466,10 @@ class GLLines2D(object):
0,
self.distVboData)
- if self.width != 1:
+ if width != 1:
gl.glEnable(gl.GL_LINE_SMOOTH)
- matrix = numpy.dot(matrix,
+ matrix = numpy.dot(context.matrix,
mat4Translate(*self.offset)).astype(numpy.float32)
gl.glUniformMatrix4fv(program.uniforms['matrix'],
1, gl.GL_TRUE, matrix)
@@ -503,7 +506,7 @@ class GLLines2D(object):
0,
self.yVboData)
- gl.glLineWidth(self.width)
+ gl.glLineWidth(width)
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
gl.glDisable(gl.GL_LINE_SMOOTH)
@@ -516,10 +519,26 @@ def distancesFromArrays(xData, yData):
: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]
- return numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1)))
+ # Split array into sub-shapes at not finite points
+ splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
+ numpy.isfinite(xData), numpy.isfinite(yData))))[0]
+ splits = numpy.concatenate(([-1], splits, [len(xData) - 1]))
+
+ # Compute distance independently for each sub-shapes,
+ # putting not finite points as last points of sub-shapes
+ distances = []
+ for begin, end in zip(splits[:-1] + 1, splits[1:] + 1):
+ if begin == end: # Empty shape
+ continue
+ elif end - begin == 1: # Single element
+ distances.append([0])
+ else:
+ deltas = numpy.dstack((
+ numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)),
+ numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0]
+ distances.append(
+ numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))))
+ return numpy.concatenate(distances)
# points ######################################################################
@@ -833,10 +852,10 @@ class _Points2D(object):
if majorVersion >= 3: # OpenGL 3
gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- def render(self, matrix):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param RenderContext context:
"""
if self.marker is None:
return
@@ -844,7 +863,7 @@ class _Points2D(object):
program = self._getProgram(self.marker)
program.use()
- matrix = numpy.dot(matrix,
+ matrix = numpy.dot(context.matrix,
mat4Translate(*self.offset)).astype(numpy.float32)
gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
@@ -854,6 +873,13 @@ class _Points2D(object):
size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point
else:
size = self.size
+ size = size / 72. * context.dpi
+
+ if self.marker in (PLUS, H_LINE, V_LINE,
+ TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN):
+ # Convert to nearest odd number
+ size = size // 2 * 2 + 1.
+
gl.glUniform1f(program.uniforms['size'], size)
# gl.glPointSize(self.size)
@@ -1021,17 +1047,17 @@ class _ErrorBars(object):
self._yErrPoints.yVboData.offset += (yAttrib.itemsize *
yAttrib.size // 2)
- def render(self, matrix):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param RenderContext context:
"""
self.prepare()
if self._attribs is not None:
- self._lines.render(matrix)
- self._xErrPoints.render(matrix)
- self._yErrPoints.render(matrix)
+ self._lines.render(context)
+ self._xErrPoints.render(context)
+ self._yErrPoints.render(context)
def discard(self):
"""Release VBOs"""
@@ -1067,7 +1093,7 @@ def _proxyProperty(*componentsAttributes):
return property(getter, setter)
-class GLPlotCurve2D(object):
+class GLPlotCurve2D(GLPlotItem):
def __init__(self, xData, yData, colorData=None,
xError=None, yError=None,
lineStyle=SOLID,
@@ -1080,7 +1106,7 @@ class GLPlotCurve2D(object):
fillColor=None,
baseline=None,
isYLog=False):
-
+ super().__init__()
self.colorData = colorData
# Compute x bounds
@@ -1220,19 +1246,17 @@ class GLPlotCurve2D(object):
self.colorVboData = cAttrib
self.useColorVboData = cAttrib is not None
- def render(self, matrix, isXLog, isYLog):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- :param bool isXLog:
- :param bool isYLog:
+ :param RenderContext context: Rendering information
"""
self.prepare()
if self.fill is not None:
- self.fill.render(matrix)
- self._errorBars.render(matrix)
- self.lines.render(matrix)
- self.points.render(matrix)
+ self.fill.render(context)
+ self._errorBars.render(context)
+ self.lines.render(context)
+ self.points.render(context)
def discard(self):
"""Release VBOs"""
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py
index 43f6e10..c5ee75b 100644
--- a/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ b/silx/gui/plot/backends/glutils/GLPlotFrame.py
@@ -61,7 +61,7 @@ class PlotAxis(object):
This class is intended to be used with :class:`GLPlotFrame`.
"""
- def __init__(self, plot,
+ def __init__(self, plotFrame,
tickLength=(0., 0.),
foregroundColor=(0., 0., 0., 1.0),
labelAlign=CENTER, labelVAlign=CENTER,
@@ -69,7 +69,7 @@ class PlotAxis(object):
titleRotate=0, titleOffset=(0., 0.)):
self._ticks = None
- self._plot = weakref.ref(plot)
+ self._plotFrameRef = weakref.ref(plotFrame)
self._isDateTime = False
self._timeZone = None
@@ -157,6 +157,12 @@ class PlotAxis(object):
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
@@ -165,10 +171,18 @@ class PlotAxis(object):
def title(self, title):
if title != self._title:
self._title = title
+ self._dirtyPlotFrame()
- plot = self._plot()
- if plot is not None:
- plot._dirty()
+ @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):
@@ -201,6 +215,8 @@ class PlotAxis(object):
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
@@ -212,7 +228,8 @@ class PlotAxis(object):
x=xPixel - xTickLength,
y=yPixel - yTickLength,
align=self._labelAlign,
- valign=self._labelVAlign)
+ valign=self._labelVAlign,
+ devicePixelRatio=self.devicePixelRatio)
width, height = label.size
if width > tickLabelsSize[0]:
@@ -230,7 +247,7 @@ class PlotAxis(object):
xAxisCenter = 0.5 * (x0 + x1)
yAxisCenter = 0.5 * (y0 + y1)
- xOffset, yOffset = self._titleOffset
+ xOffset, yOffset = self.titleOffset
# Adaptative title positioning:
# tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2)
@@ -245,17 +262,22 @@ class PlotAxis(object):
y=yAxisCenter + yOffset,
align=self._titleAlign,
valign=self._titleVAlign,
- rotate=self._titleRotate)
+ 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
- plot = self._plot()
- if plot is not None:
- plot._dirty()
+ self._dirtyPlotFrame()
@staticmethod
def _frange(start, stop, step):
@@ -314,7 +336,7 @@ class PlotAxis(object):
xScale = (x1 - x0) / (dataMax - dataMin)
yScale = (y1 - y0) / (dataMax - dataMin)
- nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2))
+ 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
@@ -391,11 +413,11 @@ class GLPlotFrame(object):
# Margins used when plot frame is not displayed
_NoDisplayMargins = _Margins(0, 0, 0, 0)
- def __init__(self, margins, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor):
"""
- :param margins: The margins around plot area for axis and labels.
- :type margins: dict with 'left', 'right', 'top', 'bottom' keys and
- values as ints.
+ :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.
@@ -403,7 +425,9 @@ class GLPlotFrame(object):
"""
self._renderResources = None
- self._margins = self._Margins(**margins)
+ self.__marginRatios = marginRatios
+ self.__marginsCache = None
+
self._foregroundColor = foregroundColor
self._gridColor = gridColor
@@ -412,7 +436,8 @@ class GLPlotFrame(object):
self._grid = False
self._size = 0., 0.
self._title = ''
- self._displayed = True
+
+ self._devicePixelRatio = 1.
@property
def isDirty(self):
@@ -453,26 +478,49 @@ class GLPlotFrame(object):
if self._gridColor != color:
self._gridColor = color
self._dirty()
-
+
@property
- def displayed(self):
- """Whether axes and their labels are displayed or not (bool)"""
- return self._displayed
-
- @displayed.setter
- def displayed(self, displayed):
- displayed = bool(displayed)
- if displayed != self._displayed:
- self._displayed = displayed
+ 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 not self.displayed:
- return self._NoDisplayMargins
- else:
- return self._margins
+ 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):
@@ -493,7 +541,7 @@ class GLPlotFrame(object):
@property
def size(self):
- """Size in pixels of the plot area including margins."""
+ """Size in device pixels of the plot area including margins."""
return self._size
@size.setter
@@ -502,6 +550,7 @@ class GLPlotFrame(object):
size = tuple(size)
if size != self._size:
self._size = size
+ self.__marginsCache = None # Clear cached margins
self._dirty()
@property
@@ -580,7 +629,8 @@ class GLPlotFrame(object):
x=xTitle,
y=yTitle,
align=CENTER,
- valign=BOTTOM))
+ valign=BOTTOM,
+ devicePixelRatio=self.devicePixelRatio))
# grid
gridVertices = numpy.array(self._buildGridVertices(),
@@ -592,7 +642,7 @@ class GLPlotFrame(object):
_SHADERS['vertex'], _SHADERS['fragment'], attrib0='position')
def render(self):
- if not self.displayed:
+ if self.margins == self._NoDisplayMargins:
return
if self._renderResources is None:
@@ -661,25 +711,24 @@ class GLPlotFrame(object):
# GLPlotFrame2D ###############################################################
class GLPlotFrame2D(GLPlotFrame):
- def __init__(self, margins, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor):
"""
- :param margins: The margins around plot area for axis and labels.
- :type margins: dict with 'left', 'right', 'top', 'bottom' keys and
- values as ints.
+ :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__(margins, foregroundColor, gridColor)
+ 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,
- titleOffset=(0, self.margins.bottom // 2)))
+ titleRotate=0))
self._x2AxisCoords = ()
@@ -688,18 +737,14 @@ class GLPlotFrame2D(GLPlotFrame):
foregroundColor=self._foregroundColor,
labelAlign=RIGHT, labelVAlign=CENTER,
titleAlign=CENTER, titleVAlign=BOTTOM,
- titleRotate=ROTATE_270,
- titleOffset=(-3 * self.margins.left // 4,
- 0)))
+ titleRotate=ROTATE_270))
self._y2Axis = PlotAxis(self,
tickLength=(-5., 0.),
foregroundColor=self._foregroundColor,
labelAlign=LEFT, labelVAlign=CENTER,
titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=ROTATE_270,
- titleOffset=(3 * self.margins.right // 4,
- 0))
+ titleRotate=ROTATE_270)
self._isYAxisInverted = False
@@ -794,6 +839,24 @@ class GLPlotFrame2D(GLPlotFrame):
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.
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
index e985a3d..f60a159 100644
--- a/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ b/silx/gui/plot/backends/glutils/GLPlotImage.py
@@ -40,10 +40,12 @@ from ...._glutils import gl, Program, Texture
from ..._utils import FLOAT32_MINPOS
from .GLSupport import mat4Translate, mat4Scale
from .GLTexture import Image
+from .GLPlotItem import GLPlotItem
-class _GLPlotData2D(object):
+class _GLPlotData2D(GLPlotItem):
def __init__(self, data, origin, scale):
+ super().__init__()
self.data = data
assert len(origin) == 2
self.origin = tuple(origin)
@@ -80,15 +82,6 @@ class _GLPlotData2D(object):
oy, sy = self.origin[1], self.scale[1]
return oy + sy * self.data.shape[0] if sy >= 0. else oy
- def discard(self):
- pass
-
- def prepare(self):
- pass
-
- def render(self, matrix, isXLog, isYLog):
- pass
-
class GLPlotColormap(_GLPlotData2D):
@@ -160,6 +153,11 @@ class GLPlotColormap(_GLPlotData2D):
'fragment': """
#version 120
+ /* isnan declaration for compatibility with GLSL 1.20 */
+ bool isnan(float value) {
+ return (value != value);
+ }
+
uniform sampler2D data;
uniform sampler2D cmap_texture;
uniform int cmap_normalization;
@@ -167,6 +165,7 @@ class GLPlotColormap(_GLPlotData2D):
uniform float cmap_min;
uniform float cmap_oneOverRange;
uniform float alpha;
+ uniform vec4 nancolor;
varying vec2 coords;
@@ -175,7 +174,8 @@ class GLPlotColormap(_GLPlotData2D):
const float oneOverLog10 = 0.43429448190325176;
void main(void) {
- float value = texture2D(data, textureCoords()).r;
+ float data = texture2D(data, textureCoords()).r;
+ float value = data;
if (cmap_normalization == 1) { /*Logarithm mapping*/
if (value > 0.) {
value = clamp(cmap_oneOverRange *
@@ -202,7 +202,11 @@ class GLPlotColormap(_GLPlotData2D):
value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.);
}
- gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
+ if (isnan(data)) {
+ gl_FragColor = nancolor;
+ } else {
+ gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
+ }
gl_FragColor.a *= alpha;
}
"""
@@ -213,6 +217,7 @@ class GLPlotColormap(_GLPlotData2D):
_INTERNAL_FORMATS = {
numpy.dtype(numpy.float32): gl.GL_R32F,
+ numpy.dtype(numpy.float16): gl.GL_R16F,
# Use normalized integer for unsigned int formats
numpy.dtype(numpy.uint16): gl.GL_R16,
numpy.dtype(numpy.uint8): gl.GL_R8,
@@ -232,7 +237,7 @@ class GLPlotColormap(_GLPlotData2D):
def __init__(self, data, origin, scale,
colormap, normalization='linear', gamma=0., cmapRange=None,
- alpha=1.0):
+ alpha=1.0, nancolor=(1., 1., 1., 0.)):
"""Create a 2D colormap
:param data: The 2D scalar data array to display
@@ -252,6 +257,8 @@ class GLPlotColormap(_GLPlotData2D):
TODO: check consistency with matplotlib
:type cmapRange: (float, float) or None
:param float alpha: Opacity from 0 (transparent) to 1 (opaque)
+ :param nancolor: RGBA color for Not-A-Number values
+ :type nancolor: 4-tuple of float in [0., 1.]
"""
assert data.dtype in self._INTERNAL_FORMATS
assert normalization in self.SUPPORTED_NORMALIZATIONS
@@ -263,6 +270,7 @@ class GLPlotColormap(_GLPlotData2D):
self._cmapRange = (1., 10.) # Colormap range
self.cmapRange = cmapRange # Update _cmapRange
self._alpha = numpy.clip(alpha, 0., 1.)
+ self._nancolor = numpy.clip(nancolor, 0., 1.)
self._cmap_texture = None
self._texture = None
@@ -283,7 +291,7 @@ class GLPlotColormap(_GLPlotData2D):
if self.normalization == 'log':
assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0.
elif self.normalization == 'sqrt':
- assert self._cmapRange[0] >= 0. and self._cmapRange[1] > 0.
+ assert self._cmapRange[0] >= 0. and self._cmapRange[1] >= 0.
return self._cmapRange
@cmapRange.setter
@@ -324,6 +332,7 @@ class GLPlotColormap(_GLPlotData2D):
magFilter=gl.GL_NEAREST,
wrap=(gl.GL_CLAMP_TO_EDGE,
gl.GL_CLAMP_TO_EDGE))
+ self._cmap_texture.prepare()
if self._texture is None:
internalFormat = self._INTERNAL_FORMATS[self.data.dtype]
@@ -376,9 +385,15 @@ class GLPlotColormap(_GLPlotData2D):
oneOverRange = 0. # Fall-back
gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)
+ gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor)
+
self._cmap_texture.bind()
- def _renderLinear(self, matrix):
+ def _renderLinear(self, context):
+ """Perform rendering when both axes have linear scales
+
+ :param RenderContext context: Rendering information
+ """
self.prepare()
prog = self._linearProgram
@@ -386,7 +401,7 @@ class GLPlotColormap(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(matrix,
+ mat = numpy.dot(numpy.dot(context.matrix,
mat4Translate(*self.origin)),
mat4Scale(*self.scale))
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
@@ -400,10 +415,14 @@ class GLPlotColormap(_GLPlotData2D):
prog.attributes['texCoords'],
self._DATA_TEX_UNIT)
- def _renderLog10(self, matrix, isXLog, isYLog):
+ def _renderLog10(self, context):
+ """Perform rendering when one axis has log scale
+
+ :param RenderContext context: Rendering information
+ """
xMin, yMin = self.xMin, self.yMin
- if ((isXLog and xMin < FLOAT32_MINPOS) or
- (isYLog and yMin < FLOAT32_MINPOS)):
+ if ((context.isXLog and xMin < FLOAT32_MINPOS) or
+ (context.isYLog and yMin < FLOAT32_MINPOS)):
# Do not render images that are partly or totally <= 0
return
@@ -417,12 +436,12 @@ class GLPlotColormap(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matrix.astype(numpy.float32))
+ context.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)
+ gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
ex = ox + self.scale[0] * self.data.shape[1]
ey = oy + self.scale[1] * self.data.shape[0]
@@ -461,11 +480,15 @@ class GLPlotColormap(_GLPlotData2D):
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
- def render(self, matrix, isXLog, isYLog):
- if any((isXLog, isYLog)):
- self._renderLog10(matrix, isXLog, isYLog)
+ def render(self, context):
+ """Perform rendering
+
+ :param RenderContext context: Rendering information
+ """
+ if any((context.isXLog, context.isYLog)):
+ self._renderLog10(context)
else:
- self._renderLinear(matrix)
+ self._renderLinear(context)
# Unbind colormap texture
gl.glActiveTexture(gl.GL_TEXTURE0 + self._cmap_texture.texUnit)
@@ -635,7 +658,11 @@ class GLPlotRGBAImage(_GLPlotData2D):
format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB
self._texture.updateAll(format_=format_, data=self.data)
- def _renderLinear(self, matrix):
+ def _renderLinear(self, context):
+ """Perform rendering with both axes having linear scales
+
+ :param RenderContext context: Rendering information
+ """
self.prepare()
prog = self._linearProgram
@@ -643,7 +670,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(matrix, mat4Translate(*self.origin)),
+ mat = numpy.dot(numpy.dot(context.matrix, mat4Translate(*self.origin)),
mat4Scale(*self.scale))
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
mat.astype(numpy.float32))
@@ -654,7 +681,11 @@ class GLPlotRGBAImage(_GLPlotData2D):
prog.attributes['texCoords'],
self._DATA_TEX_UNIT)
- def _renderLog(self, matrix, isXLog, isYLog):
+ def _renderLog(self, context):
+ """Perform rendering with axes having log scale
+
+ :param RenderContext context: Rendering information
+ """
self.prepare()
prog = self._logProgram
@@ -665,12 +696,12 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matrix.astype(numpy.float32))
+ context.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)
+ gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
@@ -707,8 +738,12 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
- def render(self, matrix, isXLog, isYLog):
- if any((isXLog, isYLog)):
- self._renderLog(matrix, isXLog, isYLog)
+ def render(self, context):
+ """Perform rendering
+
+ :param RenderContext context: Rendering information
+ """
+ if any((context.isXLog, context.isYLog)):
+ self._renderLog(context)
else:
- self._renderLinear(matrix)
+ self._renderLinear(context)
diff --git a/silx/gui/plot/backends/glutils/GLPlotItem.py b/silx/gui/plot/backends/glutils/GLPlotItem.py
new file mode 100644
index 0000000..899f38e
--- /dev/null
+++ b/silx/gui/plot/backends/glutils/GLPlotItem.py
@@ -0,0 +1,94 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2020 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 module provides a base class for PlotWidget OpenGL backend primitives
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/07/2020"
+
+
+class RenderContext:
+ """Context with which to perform OpenGL rendering.
+
+ :param numpy.ndarray matrix: 4x4 transform matrix to use for rendering
+ :param bool isXLog: Whether X axis is log scale or not
+ :param bool isYLog: Whether Y axis is log scale or not
+ :param float dpi: Number of device pixels per inch
+ """
+
+ def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96.):
+ self.matrix = matrix
+ """Current transformation matrix"""
+
+ self.__isXLog = isXLog
+ self.__isYLog = isYLog
+ self.__dpi = dpi
+
+ @property
+ def isXLog(self):
+ """True if X axis is using log scale"""
+ return self.__isXLog
+
+ @property
+ def isYLog(self):
+ """True if Y axis is using log scale"""
+ return self.__isYLog
+
+ @property
+ def dpi(self):
+ """Number of device pixels per inch"""
+ return self.__dpi
+
+
+class GLPlotItem:
+ """Base class for primitives used in the PlotWidget OpenGL backend"""
+
+ def __init__(self):
+ self.yaxis = 'left'
+ "YAxis this item is attached to (either 'left' or 'right')"
+
+ def pick(self, x, y):
+ """Perform picking at given position.
+
+ :param float x: X coordinate in plot data frame of reference
+ :param float y: Y coordinate in plot data frame of reference
+ :returns:
+ Result of picking as a list of indices or None if nothing picked
+ :rtype: Union[List[int],None]
+ """
+ return None
+
+ def render(self, context):
+ """Performs OpenGL rendering of the item.
+
+ :param RenderContext context: Rendering context information
+ """
+ pass
+
+ def discard(self):
+ """Discards OpenGL resources this item has created."""
+ pass
diff --git a/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
index 7aeb5ab..d5ba1a6 100644
--- a/silx/gui/plot/backends/glutils/GLPlotTriangles.py
+++ b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2019 European Synchrotron Radiation Facility
+# Copyright (c) 2019-2020 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
@@ -38,9 +38,10 @@ import numpy
from .....math.combo import min_max
from .... import _glutils as glutils
from ...._glutils import gl
+from .GLPlotItem import GLPlotItem
-class GLPlotTriangles(object):
+class GLPlotTriangles(GLPlotItem):
"""Handle rendering of a set of colored triangles"""
_PROGRAM = glutils.Program(
@@ -81,6 +82,7 @@ class GLPlotTriangles(object):
:param numpy.ndarray triangles: (N, 3) array of indices of triangles
:param float alpha: Opacity in [0, 1]
"""
+ super().__init__()
# Check and convert input data
x = numpy.ravel(numpy.array(x, dtype=numpy.float32))
y = numpy.ravel(numpy.array(y, dtype=numpy.float32))
@@ -161,12 +163,10 @@ class GLPlotTriangles(object):
usage=gl.GL_STATIC_DRAW,
target=gl.GL_ELEMENT_ARRAY_BUFFER)
- def render(self, matrix, isXLog, isYLog):
+ def render(self, context):
"""Perform rendering
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- :param bool isXLog:
- :param bool isYLog:
+ :param RenderContext context: Rendering information
"""
self.prepare()
@@ -178,7 +178,7 @@ class GLPlotTriangles(object):
gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'],
1,
gl.GL_TRUE,
- matrix.astype(numpy.float32))
+ context.matrix.astype(numpy.float32))
gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha)
diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py
index 725c12c..d6ae6fa 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-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2020 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
@@ -140,7 +140,9 @@ class Text2D(object):
color=(0., 0., 0., 1.),
bgColor=None,
align=LEFT, valign=BASELINE,
- rotate=0):
+ rotate=0,
+ devicePixelRatio= 1.):
+ self.devicePixelRatio = devicePixelRatio
self._vertices = None
self._text = text
self.x = x
@@ -160,30 +162,35 @@ class Text2D(object):
self._rotate = numpy.radians(rotate)
- def _getTexture(self, text):
+ def _getTexture(self, text, devicePixelRatio):
# Retrieve/initialize texture cache for current context
+ textureKey = text, devicePixelRatio
+
context = Context.getCurrent()
if context not in self._textures:
self._textures[context] = _Cache(
callback=lambda key, value: value[0].discard())
textures = self._textures[context]
- if text not in textures:
- image, offset = font.rasterText(text,
- font.getDefaultFontFamily())
- if text not in self._sizes:
- self._sizes[text] = image.shape[1], image.shape[0]
-
- textures[text] = (
- Texture(gl.GL_RED,
- data=image,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE)),
- offset)
-
- return textures[text]
+ if textureKey not in textures:
+ image, offset = font.rasterText(
+ text,
+ font.getDefaultFontFamily(),
+ devicePixelRatio=self.devicePixelRatio)
+ if textureKey not in self._sizes:
+ self._sizes[textureKey] = image.shape[1], image.shape[0]
+
+ texture = Texture(
+ gl.GL_RED,
+ data=image,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=(gl.GL_CLAMP_TO_EDGE,
+ gl.GL_CLAMP_TO_EDGE))
+ texture.prepare()
+ textures[textureKey] = texture, offset
+
+ return textures[textureKey]
@property
def text(self):
@@ -191,11 +198,14 @@ class Text2D(object):
@property
def size(self):
- if self.text not in self._sizes:
- image, offset = font.rasterText(self.text,
- font.getDefaultFontFamily())
- self._sizes[self.text] = image.shape[1], image.shape[0]
- return self._sizes[self.text]
+ textureKey = self.text, self.devicePixelRatio
+ if textureKey not in self._sizes:
+ image, offset = font.rasterText(
+ self.text,
+ font.getDefaultFontFamily(),
+ devicePixelRatio=self.devicePixelRatio)
+ self._sizes[textureKey] = image.shape[1], image.shape[0]
+ return self._sizes[textureKey]
def getVertices(self, offset, shape):
height, width = shape
@@ -238,7 +248,7 @@ class Text2D(object):
prog.use()
texUnit = 0
- texture, offset = self._getTexture(self.text)
+ texture, offset = self._getTexture(self.text, self.devicePixelRatio)
gl.glUniform1i(prog.uniforms['texText'], texUnit)
diff --git a/silx/gui/plot/backends/glutils/GLTexture.py b/silx/gui/plot/backends/glutils/GLTexture.py
index 118a36f..37fbdd0 100644
--- a/silx/gui/plot/backends/glutils/GLTexture.py
+++ b/silx/gui/plot/backends/glutils/GLTexture.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2020 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
@@ -98,6 +98,7 @@ class Image(object):
minFilter=self._MIN_FILTER,
magFilter=self._MAG_FILTER,
wrap=self._WRAP)
+ texture.prepare()
vertices = numpy.array((
(0., 0., 0., 0.),
(self.width, 0., 1., 0.),
@@ -177,6 +178,7 @@ class Image(object):
(xOrig, yOrig + hData, 0., vMax),
(xOrig + wData, yOrig + hData, uMax, vMax)),
dtype=numpy.float32)
+ texture.prepare()
tiles.append((texture, vertices,
{'xOrigData': xOrig, 'yOrigData': yOrig,
'wData': wData, 'hData': hData}))
@@ -203,6 +205,7 @@ class Image(object):
texture.update(format_,
data[yOrig:yOrig+height, xOrig:xOrig+width],
texUnit=texUnit)
+ texture.prepare()
# TODO check
# width=info['wData'], height=info['hData'],
# texUnit=texUnit, unpackAlign=unpackAlign,
diff --git a/silx/gui/plot/backends/glutils/__init__.py b/silx/gui/plot/backends/glutils/__init__.py
index d58c084..f87d7c1 100644
--- a/silx/gui/plot/backends/glutils/__init__.py
+++ b/silx/gui/plot/backends/glutils/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2020 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
@@ -39,6 +39,7 @@ _logger = logging.getLogger(__name__)
from .GLPlotCurve import * # noqa
from .GLPlotFrame import * # noqa
from .GLPlotImage import * # noqa
+from .GLPlotItem import GLPlotItem, RenderContext # noqa
from .GLPlotTriangles import GLPlotTriangles # noqa
from .GLSupport import * # noqa
from .GLText import * # noqa