summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
authorAlexandre Marie <alexandre.marie@synchrotron-soleil.fr>2020-07-21 14:45:14 +0200
committerAlexandre Marie <alexandre.marie@synchrotron-soleil.fr>2020-07-21 14:45:14 +0200
commit328032e2317e3ac4859196bbf12bdb71795302fe (patch)
tree8cd13462beab109e3cb53410c42335b6d1e00ee6 /silx/gui/plot/backends
parent33ed2a64c92b0311ae35456c016eb284e426afc2 (diff)
New upstream version 0.13.0+dfsg
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-xsilx/gui/plot/backends/BackendBase.py34
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py252
-rwxr-xr-xsilx/gui/plot/backends/BackendOpenGL.py78
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py15
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py122
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py6
6 files changed, 321 insertions, 186 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index 75d999b..bcc93a5 100755
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -63,8 +63,6 @@ class BackendBase(object):
# Store a weakref to get access to the plot state.
self._setPlot(plot)
- self.__zoomBackAction = None
-
@property
def _plot(self):
"""The plot this backend is attached to."""
@@ -83,24 +81,12 @@ class BackendBase(object):
"""
self._plotRef = weakref.ref(plot)
- # Default Qt context menu
-
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- if self.__zoomBackAction is None:
- from ..actions.control import ZoomBackAction # Avoid cyclic import
- self.__zoomBackAction = ZoomBackAction(plot=self._plot,
- parent=self._plot)
- menu = qt.QMenu(self)
- menu.addAction(self.__zoomBackAction)
- menu.exec_(event.globalPos())
-
# Add methods
def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z,
+ xerror, yerror,
fill, alpha, symbolsize, baseline):
"""Add a 1D curve given by x an y to the graph.
@@ -134,7 +120,6 @@ class BackendBase(object):
:type xerror: numpy.ndarray or None
:param yerror: Values with the uncertainties on the y values
:type yerror: numpy.ndarray or None
- :param int z: Layer on which to draw the cuve
:param bool fill: True to fill the curve, False otherwise
:param float alpha: Curve opacity, as a float in [0., 1.]
:param float symbolsize: Size of the symbol (if any) drawn
@@ -144,7 +129,7 @@ class BackendBase(object):
return object()
def addImage(self, data,
- origin, scale, z,
+ origin, scale,
colormap, alpha):
"""Add an image to the plot.
@@ -156,7 +141,6 @@ class BackendBase(object):
:param scale: (scale X, scale Y) of the data.
Default: (1., 1.)
:type scale: 2-tuple of float
- :param int z: Layer on which to draw the image
:param ~silx.gui.colors.Colormap colormap: Colormap object to use.
Ignored if data is RGB(A).
:param float alpha: Opacity of the image, as a float in range [0, 1].
@@ -165,7 +149,7 @@ class BackendBase(object):
return object()
def addTriangles(self, x, y, triangles,
- color, z, alpha):
+ color, alpha):
"""Add a set of triangles.
:param numpy.ndarray x: The data corresponding to the x axis
@@ -173,14 +157,13 @@ class BackendBase(object):
:param numpy.ndarray triangles: The indices to make triangles
as a (Ntriangle, 3) array
:param numpy.ndarray color: color(s) as (npoints, 4) array
- :param int z: Layer on which to draw the cuve
:param float alpha: Opacity as a float in [0., 1.]
:returns: The triangles' unique identifier used by the backend
"""
return object()
- def addItem(self, x, y, shape, color, fill, overlay, z,
- linestyle, linewidth, linebgcolor):
+ def addShape(self, x, y, shape, color, fill, overlay,
+ linestyle, linewidth, linebgcolor):
"""Add an item (i.e. a shape) to the plot.
:param numpy.ndarray x: The X coords of the points of the shape
@@ -190,7 +173,6 @@ class BackendBase(object):
:param str color: Color of the item
:param bool fill: True to fill the shape
:param bool overlay: True if item is an overlay, False otherwise
- :param int z: Layer on which to draw the item
:param str linestyle: Style of the line.
Only relevant for line markers where X or Y is None.
Value in:
@@ -545,7 +527,7 @@ class BackendBase(object):
"""
raise NotImplementedError()
- def pixelToData(self, x, y, axis, check):
+ def pixelToData(self, x, y, axis):
"""Convert a position in pixels in the widget to a position in
the data space.
@@ -553,8 +535,6 @@ class BackendBase(object):
:param float y: The Y coordinate in pixels.
:param str axis: The Y axis to use for the conversion
('left' or 'right').
- :param bool check: True to check if the coordinates are in the
- plot area.
:returns: The corresponding position in data space or
None if the pixel position is not in the plot area.
:rtype: A tuple of 2 floats: (xData, yData) or None.
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 2336494..036e630 100755
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -52,6 +52,7 @@ from matplotlib.patches import Rectangle, Polygon
from matplotlib.image import AxesImage
from matplotlib.backend_bases import MouseEvent
from matplotlib.lines import Line2D
+from matplotlib.text import Text
from matplotlib.collections import PathCollection, LineCollection
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
from matplotlib.tri import Triangulation
@@ -252,6 +253,60 @@ class _PickableContainer(Container):
return False, {}
+class _TextWithOffset(Text):
+ """Text object which can be displayed at a specific position
+ of the plot, but with a pixel offset"""
+
+ def __init__(self, *args, **kwargs):
+ Text.__init__(self, *args, **kwargs)
+ self.pixel_offset = (0, 0)
+ self.__cache = None
+
+ def draw(self, renderer):
+ self.__cache = None
+ return Text.draw(self, renderer)
+
+ def __get_xy(self):
+ if self.__cache is not None:
+ return self.__cache
+
+ align = self.get_horizontalalignment()
+ if align == "left":
+ xoffset = self.pixel_offset[0]
+ elif align == "right":
+ xoffset = -self.pixel_offset[0]
+ else:
+ xoffset = 0
+
+ align = self.get_verticalalignment()
+ if align == "top":
+ yoffset = -self.pixel_offset[1]
+ elif align == "bottom":
+ yoffset = self.pixel_offset[1]
+ else:
+ yoffset = 0
+
+ trans = self.get_transform()
+ invtrans = self.get_transform().inverted()
+
+ x = super(_TextWithOffset, self).convert_xunits(self._x)
+ y = super(_TextWithOffset, self).convert_xunits(self._y)
+ pos = x, y
+ proj = trans.transform_point(pos)
+ proj = proj + numpy.array((xoffset, yoffset))
+ pos = invtrans.transform_point(proj)
+ self.__cache = pos
+ return pos
+
+ def convert_xunits(self, x):
+ """Return the pixel position of the annotated point."""
+ return self.__get_xy()[0]
+
+ def convert_yunits(self, y):
+ """Return the pixel position of the annotated point."""
+ return self.__get_xy()[1]
+
+
class _MarkerContainer(_PickableContainer):
"""Marker artists container supporting draw/remove and text position update
@@ -263,9 +318,10 @@ class _MarkerContainer(_PickableContainer):
:param y: Y coordinate of the marker (None for vertical lines)
"""
- def __init__(self, artists, x, y, yAxis):
+ def __init__(self, artists, symbol, x, y, yAxis):
self.line = artists[0]
self.text = artists[1] if len(artists) > 1 else None
+ self.symbol = symbol
self.x = x
self.y = y
self.yAxis = yAxis
@@ -278,27 +334,39 @@ class _MarkerContainer(_PickableContainer):
if self.text is not None:
self.text.draw(*args, **kwargs)
- def updateMarkerText(self, xmin, xmax, ymin, ymax):
+ def updateMarkerText(self, xmin, xmax, ymin, ymax, yinverted):
"""Update marker text position and visibility according to plot limits
:param xmin: X axis lower limit
:param xmax: X axis upper limit
:param ymin: Y axis lower limit
- :param ymax: Y axis upprt limit
+ :param ymax: Y axis upper limit
+ :param yinverted: True if the y axis is inverted
"""
if self.text is not None:
visible = ((self.x is None or xmin <= self.x <= xmax) and
(self.y is None or ymin <= self.y <= ymax))
self.text.set_visible(visible)
- if self.x is not None and self.y is None: # vertical line
- delta = abs(ymax - ymin)
- if ymin > ymax:
- ymax = ymin
- ymax -= 0.005 * delta
- self.text.set_y(ymax)
-
- if self.x is None and self.y is not None: # Horizontal line
+ if self.x is not None and self.y is not None:
+ if self.symbol is None:
+ valign = 'baseline'
+ else:
+ if yinverted:
+ valign = 'bottom'
+ else:
+ valign = 'top'
+ self.text.set_verticalalignment(valign)
+
+ elif self.y is None: # vertical line
+ # Always display it on top
+ center = (ymax + ymin) * 0.5
+ pos = (ymax - ymin) * 0.5 * 0.99
+ if yinverted:
+ pos = -pos
+ self.text.set_y(center + pos)
+
+ elif self.x is None: # Horizontal line
delta = abs(xmax - xmin)
if xmin > xmax:
xmax = xmin
@@ -354,9 +422,35 @@ class _DoubleColoredLinePatch(matplotlib.patches.Patch):
class Image(AxesImage):
- """An AxesImage with a fast path for uint8 RGBA images"""
+ """An AxesImage with a fast path for uint8 RGBA images.
+
+ :param List[float] silx_origin: (ox, oy) Offset of the image.
+ :param List[float] silx_scale: (sx, sy) Scale of the image.
+ """
+
+ def __init__(self, *args,
+ silx_origin=(0., 0.),
+ silx_scale=(1., 1.),
+ **kwargs):
+ super().__init__(*args, **kwargs)
+ self.__silx_origin = silx_origin
+ self.__silx_scale = silx_scale
+
+ def contains(self, mouseevent):
+ """Overridden to fill 'ind' with row and column"""
+ inside, info = super().contains(mouseevent)
+ if inside:
+ x, y = mouseevent.xdata, mouseevent.ydata
+ ox, oy = self.__silx_origin
+ sx, sy = self.__silx_scale
+ height, width = self.get_size()
+ column = numpy.clip(int((x - ox) / sx), 0, width - 1)
+ row = numpy.clip(int((y - oy) / sy), 0, height - 1)
+ info['ind'] = (row,), (column,)
+ return inside, info
def set_data(self, A):
+ """Overridden to add a fast path for RGBA unit8 images"""
A = numpy.array(A, copy=False)
if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8:
super(Image, self).set_data(A)
@@ -402,10 +496,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
# disable the use of offsets
try:
- self.ax.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax.get_xaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_xaxis().get_major_formatter().set_useOffset(False)
+ axes = [
+ self.ax.get_yaxis().get_major_formatter(),
+ self.ax.get_xaxis().get_major_formatter(),
+ self.ax2.get_yaxis().get_major_formatter(),
+ self.ax2.get_xaxis().get_major_formatter(),
+ ]
+ for axis in axes:
+ axis.set_useOffset(False)
+ axis.set_scientific(False)
except:
_logger.warning('Cannot disabled axes offsets in %s '
% matplotlib.__version__)
@@ -485,10 +584,10 @@ class BackendMatplotlib(BackendBase.BackendBase):
def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z,
+ xerror, yerror,
fill, alpha, symbolsize, baseline):
for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, z, fill, alpha, symbolsize):
+ yaxis, fill, alpha, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -584,12 +683,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
return _PickableContainer(artists)
- def addImage(self, data, origin, scale, z, colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
# Non-uniform image
# http://wiki.scipy.org/Cookbook/Histograms
# Non-linear axes
# http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib
- for parameter in (data, origin, scale, z):
+ for parameter in (data, origin, scale):
assert parameter is not None
origin = float(origin[0]), float(origin[1])
@@ -600,7 +699,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
image = Image(self.ax,
interpolation='nearest',
picker=True,
- origin='lower')
+ origin='lower',
+ silx_origin=origin,
+ silx_scale=scale)
if alpha < 1:
image.set_alpha(alpha)
@@ -627,13 +728,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
if data.ndim == 2: # Data image, convert to RGBA image
data = colormap.applyToData(data)
-
+ elif data.dtype == numpy.uint16:
+ # Normalize uint16 data to have a similar behavior as opengl backend
+ data = data.astype(numpy.float32)
+ data /= 65535
+
image.set_data(data)
self.ax.add_artist(image)
return image
- def addTriangles(self, x, y, triangles, color, z, alpha):
- for parameter in (x, y, triangles, color, z, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
+ for parameter in (x, y, triangles, color, alpha):
assert parameter is not None
color = numpy.array(color, copy=False)
@@ -651,8 +756,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
return collection
- def addItem(self, x, y, shape, color, fill, overlay, z,
- linestyle, linewidth, linebgcolor):
+ def addShape(self, x, y, shape, color, fill, overlay,
+ linestyle, linewidth, linebgcolor):
if (linebgcolor is not None and
shape not in ('rectangle', 'polygon', 'polylines')):
_logger.warning(
@@ -755,17 +860,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
markersize=10.)[-1]
if text is not None:
- if symbol is None:
- valign = 'baseline'
- else:
- valign = 'top'
- text = " " + text
-
- textArtist = ax.text(x, y, text,
- color=color,
- horizontalalignment='left',
- verticalalignment=valign)
-
+ textArtist = _TextWithOffset(x, y, text,
+ color=color,
+ horizontalalignment='left')
+ if symbol is not None:
+ textArtist.pixel_offset = 10, 3
elif x is not None:
line = ax.axvline(x,
color=color,
@@ -773,11 +872,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
linestyle=linestyle)
if text is not None:
# Y position will be updated in updateMarkerText call
- textArtist = ax.text(x, 1., " " + text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
-
+ textArtist = _TextWithOffset(x, 1., text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment='top')
+ textArtist.pixel_offset = 5, 3
elif y is not None:
line = ax.axhline(y,
color=color,
@@ -786,11 +885,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
if text is not None:
# X position will be updated in updateMarkerText call
- textArtist = ax.text(1., y, " " + text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
-
+ textArtist = _TextWithOffset(1., y, text,
+ color=color,
+ horizontalalignment='right',
+ verticalalignment='top')
+ textArtist.pixel_offset = 5, 3
else:
raise RuntimeError('A marker must at least have one coordinate')
@@ -799,11 +898,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
# All markers are overlays
line.set_animated(True)
if textArtist is not None:
+ ax.add_artist(textArtist)
textArtist.set_animated(True)
artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, x, y, yaxis)
- container.updateMarkerText(xmin, xmax, ymin, ymax)
+ container = _MarkerContainer(artists, symbol, x, y, yaxis)
+ container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted())
return container
@@ -811,12 +911,13 @@ class BackendMatplotlib(BackendBase.BackendBase):
xmin, xmax = self.ax.get_xbound()
ymin1, ymax1 = self.ax.get_ybound()
ymin2, ymax2 = self.ax2.get_ybound()
+ yinverted = self.isYAxisInverted()
for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
if item.yAxis == 'left':
- item.updateMarkerText(xmin, xmax, ymin1, ymax1)
+ item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted)
else:
- item.updateMarkerText(xmin, xmax, ymin2, ymax2)
+ item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted)
# Remove methods
@@ -1076,6 +1177,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setYAxisInverted(self, flag):
if self.ax.yaxis_inverted() != bool(flag):
self.ax.invert_yaxis()
+ self._updateMarkers()
def isYAxisInverted(self):
return self.ax.yaxis_inverted()
@@ -1143,15 +1245,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
BackendBase.BackendBase.setAxesDisplayed(self, displayed)
if displayed:
# show axes and viewbox rect
- self.ax.set_axis_on()
- self.ax2.set_axis_on()
+ 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_axis_off()
- self.ax2.set_axis_off()
+ 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])
@@ -1168,7 +1270,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
dataBackgroundColor = backgroundColor
- if self.ax.axison:
+ if self.ax.get_frame_on():
self.fig.patch.set_facecolor(backgroundColor)
if self._matplotlibVersion < _parse_version('2'):
self.ax.set_axis_bgcolor(dataBackgroundColor)
@@ -1187,7 +1289,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
gridColor = foregroundColor
for axes in (self.ax, self.ax2):
- if axes.axison:
+ if axes.get_frame_on():
axes.spines['bottom'].set_color(foregroundColor)
axes.spines['top'].set_color(foregroundColor)
axes.spines['right'].set_color(foregroundColor)
@@ -1244,11 +1346,6 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self.mpl_connect('motion_notify_event', self._onMouseMove)
self.mpl_connect('scroll_event', self._onMouseWheel)
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- # Makes sure it is overridden (issue with PySide)
- BackendBase.BackendBase.contextMenuEvent(self, event)
-
def postRedisplay(self):
self._sigPostRedisplay.emit()
@@ -1265,17 +1362,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def _onMouseMove(self, event):
if self._graphCursor:
+ position = self._plot.pixelToData(
+ event.x,
+ self._mplQtYAxisCoordConversion(event.y),
+ axis='left',
+ check=True)
lineh, linev = self._graphCursor
- if event.inaxes not in (self.ax, self.ax2) and lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
- else:
+ if position is not None:
linev.set_visible(True)
- linev.set_xdata((event.xdata, event.xdata))
+ linev.set_xdata((position[0], position[0]))
lineh.set_visible(True)
- lineh.set_ydata((event.ydata, event.ydata))
+ lineh.set_ydata((position[1], position[1]))
self._plot._setDirtyPlot(overlayOnly=True)
+ elif lineh.get_visible():
+ lineh.set_visible(False)
+ linev.set_visible(False)
+ self._plot._setDirtyPlot(overlayOnly=True)
# onMouseMove must trigger replot if dirty flag is raised
self._plot.onMouseMove(
@@ -1294,14 +1396,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def leaveEvent(self, event):
"""QWidget event handler"""
- self._plot.onMouseLeaveWidget()
+ try:
+ plot = self._plot
+ except RuntimeError:
+ pass
+ else:
+ plot.onMouseLeaveWidget()
# picking
def pickItem(self, x, y, item):
mouseEvent = MouseEvent(
'button_press_event', self, x, self._mplQtYAxisCoordConversion(y))
+ # Override axes and data position with the axes
mouseEvent.inaxes = item.axes
+ mouseEvent.xdata, mouseEvent.ydata = self.pixelToData(
+ x, y, axis='left' if item.axes is self.ax else 'right')
picked, info = item.contains(mouseEvent)
if not picked:
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index 27f3894..cf1da31 100755
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.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
@@ -31,7 +31,6 @@ __license__ = "MIT"
__date__ = "21/12/2018"
import logging
-import warnings
import weakref
import numpy
@@ -62,7 +61,7 @@ _logger = logging.getLogger(__name__)
# Content #####################################################################
class _ShapeItem(dict):
- def __init__(self, x, y, shape, color, fill, overlay, z,
+ def __init__(self, x, y, shape, color, fill, overlay,
linestyle, linewidth, linebgcolor):
super(_ShapeItem, self).__init__()
@@ -249,11 +248,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
_MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- # Makes sure it is overridden (issue with PySide)
- BackendBase.BackendBase.contextMenuEvent(self, event)
-
def sizeHint(self):
return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend
@@ -431,6 +425,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
isXLog = self._plotFrame.xAxis.isLog
isYLog = self._plotFrame.yAxis.isLog
+ isYInverted = self._plotFrame.isYAxisInverted
# Used by marker rendering
labels = []
@@ -572,13 +567,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Do not render markers outside visible plot area
continue
+ if isYInverted:
+ valign = BOTTOM
+ vPixelOffset = -pixelOffset
+ else:
+ valign = TOP
+ vPixelOffset = pixelOffset
+
if item['text'] is not None:
x = pixelPos[0] + pixelOffset
- y = pixelPos[1] + pixelOffset
+ y = pixelPos[1] + vPixelOffset
label = Text2D(item['text'], x, y,
color=item['color'],
bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
+ align=LEFT, valign=valign)
labels.append(label)
# For now simple implementation: using a curve for each marker
@@ -726,10 +728,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z,
+ xerror, yerror,
fill, alpha, symbolsize, baseline):
for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, z, fill, symbolsize):
+ yaxis, fill, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -767,8 +769,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xErrorMinus, xErrorPlus = xerror[0], xerror[1]
else:
xErrorMinus, xErrorPlus = xerror, xerror
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ with numpy.errstate(divide='ignore', invalid='ignore'):
# Ignore divide by zero, invalid value encountered in log10
xErrorMinus = logX - numpy.log10(x - xErrorMinus)
xErrorPlus = numpy.log10(x + xErrorPlus) - logX
@@ -790,8 +791,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
yErrorMinus, yErrorPlus = yerror[0], yerror[1]
else:
yErrorMinus, yErrorPlus = yerror, yerror
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ with numpy.errstate(divide='ignore', invalid='ignore'):
# Ignore divide by zero, invalid value encountered in log10
yErrorMinus = logY - numpy.log10(y - yErrorMinus)
yErrorPlus = numpy.log10(y + yErrorPlus) - logY
@@ -846,9 +846,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return curve
def addImage(self, data,
- origin, scale, z,
+ origin, scale,
colormap, alpha):
- for parameter in (data, origin, scale, z):
+ for parameter in (data, origin, scale):
assert parameter is not None
if data.ndim == 2:
@@ -860,17 +860,25 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
'addImage: Convert %s data to float32', str(data.dtype))
data = numpy.array(data, dtype=numpy.float32, order='C')
- colormapIsLog = colormap.getNormalization() == 'log'
- cmapRange = colormap.getColormapRange(data=data)
- colormapLut = colormap.getNColors(nbColors=256)
-
- image = GLPlotColormap(data,
- origin,
- scale,
- colormapLut,
- colormapIsLog,
- cmapRange,
- alpha)
+ normalization = colormap.getNormalization()
+ if normalization in 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)
+
+ else: # Fallback applying colormap on CPU
+ rgba = colormap.applyToData(data)
+ image = GLPlotRGBAImage(rgba, origin, scale, alpha)
elif len(data.shape) == 3:
# For RGB, RGBA data
@@ -878,6 +886,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if numpy.issubdtype(data.dtype, numpy.floating):
data = numpy.array(data, dtype=numpy.float32, copy=False)
+ elif data.dtype in [numpy.uint8, numpy.uint16]:
+ pass
elif numpy.issubdtype(data.dtype, numpy.integer):
data = numpy.array(data, dtype=numpy.uint8, copy=False)
else:
@@ -899,7 +909,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return image
def addTriangles(self, x, y, triangles,
- color, z, alpha):
+ color, alpha):
# Handle axes log scale: convert data
if self._plotFrame.xAxis.isLog:
x = numpy.log10(x)
@@ -910,8 +920,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return triangles
- def addItem(self, x, y, shape, color, fill, overlay, z,
- linestyle, linewidth, linebgcolor):
+ def addShape(self, x, y, shape, color, fill, overlay,
+ linestyle, linewidth, linebgcolor):
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
@@ -923,7 +933,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
raise RuntimeError(
'Cannot add item with Y <= 0 with Y axis log scale')
- return _ShapeItem(x, y, shape, color, fill, overlay, z,
+ return _ShapeItem(x, y, shape, color, fill, overlay,
linestyle, linewidth, linebgcolor)
def addMarker(self, x, y, text, color,
@@ -971,7 +981,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
super(BackendOpenGL, self).setCursor(qt.QCursor(cursor))
def setGraphCursor(self, flag, color, linewidth, linestyle):
- if linestyle is not '-':
+ if linestyle != '-':
_logger.warning(
"BackendOpenGL.setGraphCursor linestyle parameter ignored")
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 3a0ebac..9ab85fd 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-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
@@ -35,7 +35,6 @@ __date__ = "03/04/2017"
import math
import logging
-import warnings
import numpy
@@ -1129,11 +1128,9 @@ class GLPlotCurve2D(object):
_baseline = numpy.repeat(_baseline,
len(self.xData))
if isYLog is True:
- with warnings.catch_warnings(): # Ignore NaN comparison warnings
- warnings.simplefilter('ignore',
- category=RuntimeWarning)
+ with numpy.errstate(divide='ignore', invalid='ignore'):
log_val = numpy.log10(_baseline)
- _baseline = numpy.where(_baseline>0.0, log_val, -38)
+ _baseline = numpy.where(_baseline>0.0, log_val, -38)
return _baseline
_baseline = deduce_baseline(baseline)
@@ -1277,8 +1274,7 @@ class GLPlotCurve2D(object):
if self.lineStyle is not None:
# Using Cohen-Sutherland algorithm for line clipping
- with warnings.catch_warnings(): # Ignore NaN comparison warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
codes = ((self.yData > yPickMax) << 3) | \
((self.yData < yPickMin) << 2) | \
((self.xData > xPickMax) << 1) | \
@@ -1335,8 +1331,7 @@ class GLPlotCurve2D(object):
indices.sort()
else:
- with warnings.catch_warnings(): # Ignore NaN comparison warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
indices = numpy.nonzero((self.xData >= xPickMin) &
(self.xData <= xPickMax) &
(self.yData >= yPickMin) &
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
index 5d79023..e985a3d 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-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
@@ -56,7 +56,7 @@ class _GLPlotData2D(object):
sx, sy = self.scale
col = int((x - ox) / sx)
row = int((y - oy) / sy)
- return ((row, col),)
+ return (row,), (col,)
else:
return None
@@ -141,10 +141,8 @@ class GLPlotColormap(_GLPlotData2D):
""",
'fragTransform': """
uniform bvec2 isLog;
- uniform struct {
- vec2 oneOverRange;
- vec2 originOverRange;
- } bounds;
+ uniform vec2 bounds_oneOverRange;
+ uniform vec2 bounds_originOverRange;
vec2 textureCoords(void) {
vec2 pos = coords;
@@ -154,7 +152,7 @@ class GLPlotColormap(_GLPlotData2D):
if (isLog.y) {
pos.y = pow(10., coords.y);
}
- return pos * bounds.oneOverRange - bounds.originOverRange;
+ return pos * bounds_oneOverRange - bounds_originOverRange;
// TODO texture coords in range different from [0, 1]
}
"""},
@@ -163,12 +161,11 @@ class GLPlotColormap(_GLPlotData2D):
#version 120
uniform sampler2D data;
- uniform struct {
- sampler2D texture;
- bool isLog;
- float min;
- float oneOverRange;
- } cmap;
+ uniform sampler2D cmap_texture;
+ uniform int cmap_normalization;
+ uniform float cmap_parameter;
+ uniform float cmap_min;
+ uniform float cmap_oneOverRange;
uniform float alpha;
varying vec2 coords;
@@ -179,19 +176,33 @@ class GLPlotColormap(_GLPlotData2D):
void main(void) {
float value = texture2D(data, textureCoords()).r;
- if (cmap.isLog) {
+ if (cmap_normalization == 1) { /*Logarithm mapping*/
if (value > 0.) {
- value = clamp(cmap.oneOverRange *
- (oneOverLog10 * log(value) - cmap.min),
+ value = clamp(cmap_oneOverRange *
+ (oneOverLog10 * log(value) - cmap_min),
0., 1.);
} else {
value = 0.;
}
- } else { /*Linear mapping*/
- value = clamp(cmap.oneOverRange * (value - cmap.min), 0., 1.);
+ } else if (cmap_normalization == 2) { /*Square root mapping*/
+ if (value >= 0.) {
+ value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min),
+ 0., 1.);
+ } else {
+ value = 0.;
+ }
+ } else if (cmap_normalization == 3) { /*Gamma correction mapping*/
+ value = pow(
+ clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.),
+ cmap_parameter);
+ } else if (cmap_normalization == 4) { /* arcsinh mapping */
+ /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */
+ value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.);
+ } else { /*Linear mapping and fallback*/
+ value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.);
}
- gl_FragColor = texture2D(cmap.texture, vec2(value, 0.5));
+ gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
gl_FragColor.a *= alpha;
}
"""
@@ -217,8 +228,10 @@ class GLPlotColormap(_GLPlotData2D):
_SHADERS['log']['fragTransform'],
attrib0='position')
+ SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
+
def __init__(self, data, origin, scale,
- colormap, cmapIsLog=False, cmapRange=None,
+ colormap, normalization='linear', gamma=0., cmapRange=None,
alpha=1.0):
"""Create a 2D colormap
@@ -231,7 +244,9 @@ class GLPlotColormap(_GLPlotData2D):
:type scale: 2-tuple of floats.
:param str colormap: Name of the colormap to use
TODO: Accept a 1D scalar array as the colormap
- :param bool cmapIsLog: If True, uses log10 of the data value
+ :param str normalization: The colormap normalization.
+ One of: 'linear', 'log', 'sqrt', 'gamma'
+ ;param float gamma: The gamma parameter (for 'gamma' normalization)
:param cmapRange: The range of colormap or None for autoscale colormap
For logarithmic colormap, the range is in the untransformed data
TODO: check consistency with matplotlib
@@ -239,10 +254,12 @@ class GLPlotColormap(_GLPlotData2D):
:param float alpha: Opacity from 0 (transparent) to 1 (opaque)
"""
assert data.dtype in self._INTERNAL_FORMATS
+ assert normalization in self.SUPPORTED_NORMALIZATIONS
super(GLPlotColormap, self).__init__(data, origin, scale)
self.colormap = numpy.array(colormap, copy=False)
- self.cmapIsLog = cmapIsLog
+ self.normalization = normalization
+ self.gamma = gamma
self._cmapRange = (1., 10.) # Colormap range
self.cmapRange = cmapRange # Update _cmapRange
self._alpha = numpy.clip(alpha, 0., 1.)
@@ -263,8 +280,10 @@ class GLPlotColormap(_GLPlotData2D):
@property
def cmapRange(self):
- if self.cmapIsLog:
+ 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.
return self._cmapRange
@cmapRange.setter
@@ -319,6 +338,7 @@ class GLPlotColormap(_GLPlotData2D):
def _setCMap(self, prog):
dataMin, dataMax = self.cmapRange # If log, it is stricly positive
+ param = 0.
if self.data.dtype in (numpy.uint16, numpy.uint8):
# Using unsigned int as normalized integer in OpenGL
@@ -326,19 +346,35 @@ class GLPlotColormap(_GLPlotData2D):
maxInt = float(numpy.iinfo(self.data.dtype).max)
dataMin, dataMax = dataMin / maxInt, dataMax / maxInt
- if self.cmapIsLog:
+ if self.normalization == 'log':
dataMin = math.log10(dataMin)
dataMax = math.log10(dataMax)
-
- gl.glUniform1i(prog.uniforms['cmap.texture'],
+ normID = 1
+ elif self.normalization == 'sqrt':
+ dataMin = math.sqrt(dataMin)
+ dataMax = math.sqrt(dataMax)
+ normID = 2
+ elif self.normalization == 'gamma':
+ # Keep dataMin, dataMax as is
+ param = self.gamma
+ normID = 3
+ elif self.normalization == 'arcsinh':
+ dataMin = numpy.arcsinh(dataMin)
+ dataMax = numpy.arcsinh(dataMax)
+ normID = 4
+ else: # Linear and fallback
+ normID = 0
+
+ gl.glUniform1i(prog.uniforms['cmap_texture'],
self._cmap_texture.texUnit)
- gl.glUniform1i(prog.uniforms['cmap.isLog'], self.cmapIsLog)
- gl.glUniform1f(prog.uniforms['cmap.min'], dataMin)
+ gl.glUniform1i(prog.uniforms['cmap_normalization'], normID)
+ gl.glUniform1f(prog.uniforms['cmap_parameter'], param)
+ gl.glUniform1f(prog.uniforms['cmap_min'], dataMin)
if dataMax > dataMin:
oneOverRange = 1. / (dataMax - dataMin)
else:
oneOverRange = 0. # Fall-back
- gl.glUniform1f(prog.uniforms['cmap.oneOverRange'], oneOverRange)
+ gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)
self._cmap_texture.bind()
@@ -393,9 +429,9 @@ class GLPlotColormap(_GLPlotData2D):
xOneOverRange = 1. / (ex - ox)
yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds.originOverRange'],
+ gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds.oneOverRange'],
+ gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
xOneOverRange, yOneOverRange)
gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
@@ -500,10 +536,8 @@ class GLPlotRGBAImage(_GLPlotData2D):
uniform sampler2D tex;
uniform bvec2 isLog;
- uniform struct {
- vec2 oneOverRange;
- vec2 originOverRange;
- } bounds;
+ uniform vec2 bounds_oneOverRange;
+ uniform vec2 bounds_originOverRange;
uniform float alpha;
varying vec2 coords;
@@ -516,7 +550,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
if (isLog.y) {
pos.y = pow(10., coords.y);
}
- return pos * bounds.oneOverRange - bounds.originOverRange;
+ return pos * bounds_oneOverRange - bounds_originOverRange;
// TODO texture coords in range different from [0, 1]
}
@@ -530,7 +564,8 @@ class GLPlotRGBAImage(_GLPlotData2D):
_DATA_TEX_UNIT = 0
_SUPPORTED_DTYPES = (numpy.dtype(numpy.float32),
- numpy.dtype(numpy.uint8))
+ numpy.dtype(numpy.uint8),
+ numpy.dtype(numpy.uint16))
_linearProgram = Program(_SHADERS['linear']['vertex'],
_SHADERS['linear']['fragment'],
@@ -582,9 +617,14 @@ class GLPlotRGBAImage(_GLPlotData2D):
def prepare(self):
if self._texture is None:
- format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB
+ formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB'
+ format_ = getattr(gl, formatName)
- self._texture = Image(format_,
+ if self.data.dtype == numpy.uint16:
+ formatName += '16' # Use sized internal format for uint16
+ internalFormat = getattr(gl, formatName)
+
+ self._texture = Image(internalFormat,
self.data,
format_=format_,
texUnit=self._DATA_TEX_UNIT)
@@ -639,9 +679,9 @@ class GLPlotRGBAImage(_GLPlotData2D):
xOneOverRange = 1. / (ex - ox)
yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds.originOverRange'],
+ gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds.oneOverRange'],
+ gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
xOneOverRange, yOneOverRange)
try:
diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py
index 83c7ae0..5fb6853 100644
--- a/silx/gui/plot/backends/glutils/PlotImageFile.py
+++ b/silx/gui/plot/backends/glutils/PlotImageFile.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 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
@@ -59,7 +59,7 @@ def convertRGBDataToPNG(data):
0, 0, interlace)
# Add filter 'None' before each scanline
- preparedData = b'\x00' + b'\x00'.join(line.tostring() for line in data)
+ preparedData = b'\x00' + b'\x00'.join(line.tobytes() for line in data)
compressedData = zlib.compress(preparedData, 8)
IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T')
@@ -134,7 +134,7 @@ def saveImageToFile(data, fileNameOrObj, fileFormat):
fileObj.write(b'P6\n')
fileObj.write(b'%d %d\n' % (width, height))
fileObj.write(b'255\n')
- fileObj.write(data.tostring())
+ fileObj.write(data.tobytes())
elif fileFormat == 'png':
fileObj.write(convertRGBDataToPNG(data))