diff options
Diffstat (limited to 'silx/gui/plot/backends')
-rw-r--r-- | silx/gui/plot/backends/BackendBase.py | 548 | ||||
-rw-r--r-- | silx/gui/plot/backends/BackendMatplotlib.py | 1139 | ||||
-rw-r--r-- | silx/gui/plot/backends/BackendOpenGL.py | 1725 | ||||
-rw-r--r-- | silx/gui/plot/backends/__init__.py | 29 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotCurve.py | 1151 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotFrame.py | 1116 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotImage.py | 674 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLSupport.py | 201 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLText.py | 270 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLTexture.py | 239 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/PlotImageFile.py | 153 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/__init__.py | 44 |
12 files changed, 0 insertions, 7289 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py deleted file mode 100644 index 7fb8be0..0000000 --- a/silx/gui/plot/backends/BackendBase.py +++ /dev/null @@ -1,548 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""Base class for Plot backends. - -It documents the Plot backend API. - -This API is a simplified version of PyMca PlotBackend API. -""" - -__authors__ = ["V.A. Sole", "T. Vincent"] -__license__ = "MIT" -__date__ = "24/04/2018" - -import weakref -from ... import qt - - -# Names for setCursor -CURSOR_DEFAULT = 'default' -CURSOR_POINTING = 'pointing' -CURSOR_SIZE_HOR = 'size horizontal' -CURSOR_SIZE_VER = 'size vertical' -CURSOR_SIZE_ALL = 'size all' - - -class BackendBase(object): - """Class defining the API a backend of the Plot should provide.""" - - def __init__(self, plot, parent=None): - """Init. - - :param Plot plot: The Plot this backend is attached to - :param parent: The parent widget of the plot widget. - """ - self.__xLimits = 1., 100. - self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)} - self.__yAxisInverted = False - self.__keepDataAspectRatio = False - self._xAxisTimeZone = None - self._axesDisplayed = True - # Store a weakref to get access to the plot state. - self._setPlot(plot) - - self.__zoomBackAction = None - - @property - def _plot(self): - """The plot this backend is attached to.""" - if self._plotRef is None: - raise RuntimeError('This backend is not attached to a Plot') - - plot = self._plotRef() - if plot is None: - raise RuntimeError('This backend is no more attached to a Plot') - return plot - - def _setPlot(self, plot): - """Allow to set plot after init. - - Use with caution, basically **immediately** after init. - """ - 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, legend, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, z, selectable, - fill, alpha, symbolsize): - """Add a 1D curve given by x an y to the graph. - - :param numpy.ndarray x: The data corresponding to the x axis - :param numpy.ndarray y: The data corresponding to the y axis - :param str legend: The legend to be associated to the curve - :param color: color(s) to be used - :type color: string ("#RRGGBB") or (npoints, 4) unsigned byte array or - one of the predefined color names defined in colors.py - :param str symbol: Symbol to be drawn at each (x, y) position:: - - - ' ' or '' no symbol - - 'o' circle - - '.' point - - ',' pixel - - '+' cross - - 'x' x-cross - - 'd' diamond - - 's' square - - :param float linewidth: The width of the curve in pixels - :param str linestyle: Type of line:: - - - ' ' or '' no line - - '-' solid line - - '--' dashed line - - '-.' dash-dot line - - ':' dotted line - - :param str yaxis: The Y axis this curve belongs to in: 'left', 'right' - :param xerror: Values with the uncertainties on the x values - :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 selectable: indicate if the curve can be selected - :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 - at each (x, y) position. - :returns: The handle used by the backend to univocally access the curve - """ - return legend - - def addImage(self, data, legend, - origin, scale, z, - selectable, draggable, - colormap, alpha): - """Add an image to the plot. - - :param numpy.ndarray data: (nrows, ncolumns) data or - (nrows, ncolumns, RGBA) ubyte array - :param str legend: The legend to be associated to the image - :param origin: (origin X, origin Y) of the data. - Default: (0., 0.) - :type origin: 2-tuple of float - :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 bool selectable: indicate if the image can be selected - :param bool draggable: indicate if the image can be moved - :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]. - :returns: The handle used by the backend to univocally access the image - """ - return legend - - def addItem(self, x, y, legend, shape, color, fill, overlay, z): - """Add an item (i.e. a shape) to the plot. - - :param numpy.ndarray x: The X coords of the points of the shape - :param numpy.ndarray y: The Y coords of the points of the shape - :param str legend: The legend to be associated to the item - :param str shape: Type of item to be drawn in - hline, polygon, rectangle, vline, polylines - :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 - :returns: The handle used by the backend to univocally access the item - """ - return legend - - def addMarker(self, x, y, legend, text, color, - selectable, draggable, - symbol, linestyle, linewidth, constraint): - """Add a point, vertical line or horizontal line marker to the plot. - - :param float x: Horizontal position of the marker in graph coordinates. - If None, the marker is a horizontal line. - :param float y: Vertical position of the marker in graph coordinates. - If None, the marker is a vertical line. - :param str legend: Legend associated to the marker - :param str text: Text associated to the marker (or None for no text) - :param str color: Color to be used for instance 'blue', 'b', '#FF0000' - :param bool selectable: indicate if the marker can be selected - :param bool draggable: indicate if the marker can be moved - :param str symbol: Symbol representing the marker. - Only relevant for point markers where X and Y are not None. - Value in: - - - 'o' circle - - '.' point - - ',' pixel - - '+' cross - - 'x' x-cross - - 'd' diamond - - 's' square - :param str linestyle: Style of the line. - Only relevant for line markers where X or Y is None. - Value in: - - - ' ' no line - - '-' solid line - - '--' dashed line - - '-.' dash-dot line - - ':' dotted line - :param float linewidth: Width of the line. - Only relevant for line markers where X or Y is None. - :param constraint: A function filtering marker displacement by - dragging operations or None for no filter. - This function is called each time a marker is - moved. - This parameter is only used if draggable is True. - :type constraint: None or a callable that takes the coordinates of - the current cursor position in the plot as input - and that returns the filtered coordinates. - :return: Handle used by the backend to univocally access the marker - """ - return legend - - # Remove methods - - def remove(self, item): - """Remove an existing item from the plot. - - :param item: A backend specific item handle returned by a add* method - """ - pass - - # Interaction methods - - def setGraphCursorShape(self, cursor): - """Set the cursor shape. - - To override in interactive backends. - - :param str cursor: Name of the cursor shape or None - """ - pass - - def setGraphCursor(self, flag, color, linewidth, linestyle): - """Toggle the display of a crosshair cursor and set its attributes. - - To override in interactive backends. - - :param bool flag: Toggle the display of a crosshair cursor. - :param color: The color to use for the crosshair. - :type color: A string (either a predefined color name in colors.py - or "#RRGGBB")) or a 4 columns unsigned byte array. - :param int linewidth: The width of the lines of the crosshair. - :param linestyle: Type of line:: - - - ' ' no line - - '-' solid line - - '--' dashed line - - '-.' dash-dot line - - ':' dotted line - - :type linestyle: None or one of the predefined styles. - """ - pass - - def pickItems(self, x, y, kinds): - """Get a list of items at a pixel position. - - :param float x: The x pixel coord where to pick. - :param float y: The y pixel coord where to pick. - :param List[str] kind: List of item kinds to pick. - Supported kinds: 'marker', 'curve', 'image'. - :return: All picked items from back to front. - One dict per item, - with 'kind' key in 'curve', 'marker', 'image'; - 'legend' key, the item legend. - and for curves, 'xdata' and 'ydata' keys storing picked - position on the curve. - :rtype: list of dict - """ - return [] - - # Update curve - - def setCurveColor(self, curve, color): - """Set the color of a curve. - - :param curve: The curve handle - :param str color: The color to use. - """ - pass - - # Misc. - - def getWidgetHandle(self): - """Return the widget this backend is drawing to.""" - return None - - def postRedisplay(self): - """Trigger a :meth:`Plot.replot`. - - Default implementation triggers a synchronous replot if plot is dirty. - This method should be overridden by the embedding widget in order to - provide an asynchronous call to replot in order to optimize the number - replot operations. - """ - # This method can be deferred and it might happen that plot has been - # destroyed in between, especially with unittests - - plot = self._plotRef() - if plot is not None and plot._getDirtyPlot(): - plot.replot() - - def replot(self): - """Redraw the plot.""" - pass - - def saveGraph(self, fileName, fileFormat, dpi): - """Save the graph to a file (or a StringIO) - - At least "png", "svg" are supported. - - :param fileName: Destination - :type fileName: String or StringIO or BytesIO - :param str fileFormat: String specifying the format - :param int dpi: The resolution to use or None. - """ - pass - - # Graph labels - - def setGraphTitle(self, title): - """Set the main title of the plot. - - :param str title: Title associated to the plot - """ - pass - - def setGraphXLabel(self, label): - """Set the X axis label. - - :param str label: label associated to the plot bottom X axis - """ - pass - - def setGraphYLabel(self, label, axis): - """Set the left Y axis label. - - :param str label: label associated to the plot left Y axis - :param str axis: The axis for which to get the limits: left or right - """ - pass - - # Graph limits - - def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): - """Set the limits of the X and Y axes at once. - - :param float xmin: minimum bottom axis value - :param float xmax: maximum bottom axis value - :param float ymin: minimum left axis value - :param float ymax: maximum left axis value - :param float y2min: minimum right axis value - :param float y2max: maximum right axis value - """ - self.__xLimits = xmin, xmax - self.__yLimits['left'] = ymin, ymax - if y2min is not None and y2max is not None: - self.__yLimits['right'] = y2min, y2max - - def getGraphXLimits(self): - """Get the graph X (bottom) limits. - - :return: Minimum and maximum values of the X axis - """ - return self.__xLimits - - def setGraphXLimits(self, xmin, xmax): - """Set the limits of X axis. - - :param float xmin: minimum bottom axis value - :param float xmax: maximum bottom axis value - """ - self.__xLimits = xmin, xmax - - def getGraphYLimits(self, axis): - """Get the graph Y (left) limits. - - :param str axis: The axis for which to get the limits: left or right - :return: Minimum and maximum values of the Y axis - """ - return self.__yLimits[axis] - - def setGraphYLimits(self, ymin, ymax, axis): - """Set the limits of the Y axis. - - :param float ymin: minimum left axis value - :param float ymax: maximum left axis value - :param str axis: The axis for which to get the limits: left or right - """ - self.__yLimits[axis] = ymin, ymax - - # Graph axes - - - def getXAxisTimeZone(self): - """Returns tzinfo that is used if the X-Axis plots date-times. - - None means the datetimes are interpreted as local time. - - :rtype: datetime.tzinfo of None. - """ - return self._xAxisTimeZone - - def setXAxisTimeZone(self, tz): - """Sets tzinfo that is used if the X-Axis plots date-times. - - Use None to let the datetimes be interpreted as local time. - - :rtype: datetime.tzinfo of None. - """ - self._xAxisTimeZone = tz - - def isXAxisTimeSeries(self): - """Return True if the X-axis scale shows datetime objects. - - :rtype: bool - """ - raise NotImplementedError() - - def setXAxisTimeSeries(self, isTimeSeries): - """Set whether the X-axis is a time series - - :param bool flag: True to switch to time series, False for regular axis. - """ - raise NotImplementedError() - - def setXAxisLogarithmic(self, flag): - """Set the X axis scale between linear and log. - - :param bool flag: If True, the bottom axis will use a log scale - """ - pass - - def setYAxisLogarithmic(self, flag): - """Set the Y axis scale between linear and log. - - :param bool flag: If True, the left axis will use a log scale - """ - pass - - def setYAxisInverted(self, flag): - """Invert the Y axis. - - :param bool flag: If True, put the vertical axis origin on the top - """ - self.__yAxisInverted = bool(flag) - - def isYAxisInverted(self): - """Return True if left Y axis is inverted, False otherwise.""" - return self.__yAxisInverted - - def isKeepDataAspectRatio(self): - """Returns whether the plot is keeping data aspect ratio or not.""" - return self.__keepDataAspectRatio - - def setKeepDataAspectRatio(self, flag): - """Set whether to keep data aspect ratio or not. - - :param flag: True to respect data aspect ratio - :type flag: Boolean, default True - """ - self.__keepDataAspectRatio = bool(flag) - - def setGraphGrid(self, which): - """Set grid. - - :param which: None to disable grid, 'major' for major grid, - 'both' for major and minor grid - """ - pass - - # Data <-> Pixel coordinates conversion - - def dataToPixel(self, x, y, axis): - """Convert a position in data space to a position in pixels - in the widget. - - :param float x: The X coordinate in data space. - :param float y: The Y coordinate in data space. - :param str axis: The Y axis to use for the conversion - ('left' or 'right'). - :returns: The corresponding position in pixels or - None if the data position is not in the displayed area. - :rtype: A tuple of 2 floats: (xPixel, yPixel) or None. - """ - raise NotImplementedError() - - def pixelToData(self, x, y, axis, check): - """Convert a position in pixels in the widget to a position in - the data space. - - :param float x: The X coordinate in pixels. - :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. - """ - raise NotImplementedError() - - def getPlotBoundsInPixels(self): - """Plot area bounds in widget coordinates in pixels. - - :return: bounds as a 4-tuple of int: (left, top, width, height) - """ - raise NotImplementedError() - - def setAxesDisplayed(self, displayed): - """Display or not the axes. - - :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 - - 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 - """ - return self._axesDisplayed diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py deleted file mode 100644 index 3b1d6dd..0000000 --- a/silx/gui/plot/backends/BackendMatplotlib.py +++ /dev/null @@ -1,1139 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Matplotlib Plot backend.""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"] -__license__ = "MIT" -__date__ = "01/08/2018" - - -import logging -import datetime as dt -import numpy - -from pkg_resources import parse_version as _parse_version - - -_logger = logging.getLogger(__name__) - - -from ... import qt - -# First of all init matplotlib and set its backend -from ..matplotlib import FigureCanvasQTAgg -import matplotlib -from matplotlib.container import Container -from matplotlib.figure import Figure -from matplotlib.patches import Rectangle, Polygon -from matplotlib.image import AxesImage -from matplotlib.backend_bases import MouseEvent -from matplotlib.lines import Line2D -from matplotlib.collections import PathCollection, LineCollection -from matplotlib.ticker import Formatter, ScalarFormatter, Locator - - -from ....third_party.modest_image import ModestImage -from . import BackendBase -from .._utils import FLOAT32_MINPOS -from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp - - - -class NiceDateLocator(Locator): - """ - Matplotlib Locator that uses Nice Numbers algorithm (adapted to dates) - to find the tick locations. This results in the same number behaviour - as when using the silx Open GL backend. - - Expects the data to be posix timestampes (i.e. seconds since 1970) - """ - def __init__(self, numTicks=5, tz=None): - """ - :param numTicks: target number of ticks - :param datetime.tzinfo tz: optional time zone. None is local time. - """ - super(NiceDateLocator, self).__init__() - self.numTicks = numTicks - - self._spacing = None - self._unit = None - self.tz = tz - - @property - def spacing(self): - """ The current spacing. Will be updated when new tick value are made""" - return self._spacing - - @property - def unit(self): - """ The current DtUnit. Will be updated when new tick value are made""" - return self._unit - - def __call__(self): - """Return the locations of the ticks""" - vmin, vmax = self.axis.get_view_interval() - return self.tick_values(vmin, vmax) - - def tick_values(self, vmin, vmax): - """ Calculates tick values - """ - if vmax < vmin: - vmin, vmax = vmax, vmin - - # vmin and vmax should be timestamps (i.e. seconds since 1 Jan 1970) - dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz) - dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz) - dtTicks, self._spacing, self._unit = \ - calcTicks(dtMin, dtMax, self.numTicks) - - # Convert datetime back to time stamps. - ticks = [timestamp(dtTick) for dtTick in dtTicks] - return ticks - - - -class NiceAutoDateFormatter(Formatter): - """ - Matplotlib FuncFormatter that is linked to a NiceDateLocator and gives the - best possible formats given the locators current spacing an date unit. - """ - - def __init__(self, locator, tz=None): - """ - :param niceDateLocator: a NiceDateLocator object - :param datetime.tzinfo tz: optional time zone. None is local time. - """ - super(NiceAutoDateFormatter, self).__init__() - self.locator = locator - self.tz = tz - - @property - def formatString(self): - if self.locator.spacing is None or self.locator.unit is None: - # Locator has no spacing or units yet. Return elaborate fmtString - return "Y-%m-%d %H:%M:%S" - else: - return bestFormatString(self.locator.spacing, self.locator.unit) - - - def __call__(self, x, pos=None): - """Return the format for tick val *x* at position *pos* - Expects x to be a POSIX timestamp (seconds since 1 Jan 1970) - """ - dateTime = dt.datetime.fromtimestamp(x, tz=self.tz) - tickStr = dateTime.strftime(self.formatString) - return tickStr - - - - -class _MarkerContainer(Container): - """Marker artists container supporting draw/remove and text position update - - :param artists: - Iterable with either one Line2D or a Line2D and a Text. - The use of an iterable if enforced by Container being - a subclass of tuple that defines a specific __new__. - :param x: X coordinate of the marker (None for horizontal lines) - :param y: Y coordinate of the marker (None for vertical lines) - """ - - def __init__(self, artists, x, y): - self.line = artists[0] - self.text = artists[1] if len(artists) > 1 else None - self.x = x - self.y = y - - Container.__init__(self, artists) - - def draw(self, *args, **kwargs): - """artist-like draw to broadcast draw to line and text""" - self.line.draw(*args, **kwargs) - if self.text is not None: - self.text.draw(*args, **kwargs) - - def updateMarkerText(self, xmin, xmax, ymin, ymax): - """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 - """ - 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 - delta = abs(xmax - xmin) - if xmin > xmax: - xmax = xmin - xmax -= 0.005 * delta - self.text.set_x(xmax) - - -class BackendMatplotlib(BackendBase.BackendBase): - """Base class for Matplotlib backend without a FigureCanvas. - - For interactive on screen plot, see :class:`BackendMatplotlibQt`. - - See :class:`BackendBase.BackendBase` for public API documentation. - """ - - def __init__(self, plot, parent=None): - super(BackendMatplotlib, self).__init__(plot, parent) - - # matplotlib is handling keep aspect ratio at draw time - # When keep aspect ratio is on, and one changes the limits and - # ask them *before* next draw has been performed he will get the - # limits without applying keep aspect ratio. - # This attribute is used to ensure consistent values returned - # when getting the limits at the expense of a replot - self._dirtyLimits = True - self._axesDisplayed = True - self._matplotlibVersion = _parse_version(matplotlib.__version__) - - self.fig = Figure() - self.fig.set_facecolor("w") - - self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left") - self.ax2 = self.ax.twinx() - self.ax2.set_label("right") - - # 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) - except: - _logger.warning('Cannot disabled axes offsets in %s ' \ - % matplotlib.__version__) - - # critical for picking!!!! - self.ax2.set_zorder(0) - self.ax2.set_autoscaley_on(True) - self.ax.set_zorder(1) - # this works but the figure color is left - if self._matplotlibVersion < _parse_version('2'): - self.ax.set_axis_bgcolor('none') - else: - self.ax.set_facecolor('none') - self.fig.sca(self.ax) - - self._overlays = set() - self._background = None - - self._colormaps = {} - - self._graphCursor = tuple() - - self._enableAxis('right', False) - self._isXAxisTimeSeries = False - - # Add methods - - def addCurve(self, x, y, legend, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, z, selectable, - fill, alpha, symbolsize): - for parameter in (x, y, legend, color, symbol, linewidth, linestyle, - yaxis, z, selectable, fill, alpha, symbolsize): - assert parameter is not None - assert yaxis in ('left', 'right') - - if (len(color) == 4 and - type(color[3]) in [type(1), numpy.uint8, numpy.int8]): - color = numpy.array(color, dtype=numpy.float) / 255. - - if yaxis == "right": - axes = self.ax2 - self._enableAxis("right", True) - else: - axes = self.ax - - picker = 3 if selectable else None - - artists = [] # All the artists composing the curve - - # First add errorbars if any so they are behind the curve - if xerror is not None or yerror is not None: - if hasattr(color, 'dtype') and len(color) == len(x): - errorbarColor = 'k' - else: - errorbarColor = color - - # On Debian 7 at least, Nx1 array yerr does not seems supported - if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and - yerror.shape[1] == 1 and len(x) != 1): - yerror = numpy.ravel(yerror) - - errorbars = axes.errorbar(x, y, label=legend, - xerr=xerror, yerr=yerror, - linestyle=' ', color=errorbarColor) - artists += list(errorbars.get_children()) - - if hasattr(color, 'dtype') and len(color) == len(x): - # scatter plot - if color.dtype not in [numpy.float32, numpy.float]: - actualColor = color / 255. - else: - actualColor = color - - if linestyle not in ["", " ", None]: - # scatter plot with an actual line ... - # we need to assign a color ... - curveList = axes.plot(x, y, label=legend, - linestyle=linestyle, - color=actualColor[0], - linewidth=linewidth, - picker=picker, - marker=None) - artists += list(curveList) - - scatter = axes.scatter(x, y, - label=legend, - color=actualColor, - marker=symbol, - picker=picker, - s=symbolsize**2) - artists.append(scatter) - - if fill: - artists.append(axes.fill_between( - x, FLOAT32_MINPOS, y, facecolor=actualColor[0], linestyle='')) - - else: # Curve - curveList = axes.plot(x, y, - label=legend, - linestyle=linestyle, - color=color, - linewidth=linewidth, - marker=symbol, - picker=picker, - markersize=symbolsize) - artists += list(curveList) - - if fill: - artists.append( - axes.fill_between(x, FLOAT32_MINPOS, y, facecolor=color)) - - for artist in artists: - artist.set_zorder(z) - if alpha < 1: - artist.set_alpha(alpha) - - return Container(artists) - - def addImage(self, data, legend, - origin, scale, z, - selectable, draggable, - 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, legend, origin, scale, z, - selectable, draggable): - assert parameter is not None - - origin = float(origin[0]), float(origin[1]) - scale = float(scale[0]), float(scale[1]) - height, width = data.shape[0:2] - - picker = (selectable or draggable) - - # Debian 7 specific support - # No transparent colormap with matplotlib < 1.2.0 - # Add support for transparent colormap for uint8 data with - # colormap with 256 colors, linear norm, [0, 255] range - if self._matplotlibVersion < _parse_version('1.2.0'): - if (len(data.shape) == 2 and colormap.getName() is None and - colormap.getColormapLUT() is not None): - colors = colormap.getColormapLUT() - if (colors.shape[-1] == 4 and - not numpy.all(numpy.equal(colors[3], 255))): - # This is a transparent colormap - if (colors.shape == (256, 4) and - colormap.getNormalization() == 'linear' and - not colormap.isAutoscale() and - colormap.getVMin() == 0 and - colormap.getVMax() == 255 and - data.dtype == numpy.uint8): - # Supported case, convert data to RGBA - data = colors[data.reshape(-1)].reshape( - data.shape + (4,)) - else: - _logger.warning( - 'matplotlib %s does not support transparent ' - 'colormap.', matplotlib.__version__) - - if ((height * width) > 5.0e5 and - origin == (0., 0.) and scale == (1., 1.)): - imageClass = ModestImage - else: - imageClass = AxesImage - - # All image are shown as RGBA image - image = imageClass(self.ax, - label="__IMAGE__" + legend, - interpolation='nearest', - picker=picker, - zorder=z, - origin='lower') - - if alpha < 1: - image.set_alpha(alpha) - - # Set image extent - xmin = origin[0] - xmax = xmin + scale[0] * width - if scale[0] < 0.: - xmin, xmax = xmax, xmin - - ymin = origin[1] - ymax = ymin + scale[1] * height - if scale[1] < 0.: - ymin, ymax = ymax, ymin - - image.set_extent((xmin, xmax, ymin, ymax)) - - # Set image data - if scale[0] < 0. or scale[1] < 0.: - # For negative scale, step by -1 - xstep = 1 if scale[0] >= 0. else -1 - ystep = 1 if scale[1] >= 0. else -1 - data = data[::ystep, ::xstep] - - if self._matplotlibVersion < _parse_version('2.1'): - # matplotlib 1.4.2 do not support float128 - dtype = data.dtype - if dtype.kind == "f" and dtype.itemsize >= 16: - _logger.warning("Your matplotlib version do not support " - "float128. Data converted to float64.") - data = data.astype(numpy.float64) - - if data.ndim == 2: # Data image, convert to RGBA image - data = colormap.applyToData(data) - - image.set_data(data) - - self.ax.add_artist(image) - - return image - - def addItem(self, x, y, legend, shape, color, fill, overlay, z): - xView = numpy.array(x, copy=False) - yView = numpy.array(y, copy=False) - - if shape == "line": - item = self.ax.plot(x, y, label=legend, color=color, - linestyle='-', marker=None)[0] - - elif shape == "hline": - if hasattr(y, "__len__"): - y = y[-1] - item = self.ax.axhline(y, label=legend, color=color) - - elif shape == "vline": - if hasattr(x, "__len__"): - x = x[-1] - item = self.ax.axvline(x, label=legend, color=color) - - elif shape == 'rectangle': - xMin = numpy.nanmin(xView) - xMax = numpy.nanmax(xView) - yMin = numpy.nanmin(yView) - yMax = numpy.nanmax(yView) - w = xMax - xMin - h = yMax - yMin - item = Rectangle(xy=(xMin, yMin), - width=w, - height=h, - fill=False, - color=color) - if fill: - item.set_hatch('.') - - self.ax.add_patch(item) - - elif shape in ('polygon', 'polylines'): - points = numpy.array((xView, yView)).T - if shape == 'polygon': - closed = True - else: # shape == 'polylines' - closed = numpy.all(numpy.equal(points[0], points[-1])) - item = Polygon(points, - closed=closed, - fill=False, - label=legend, - color=color) - if fill and shape == 'polygon': - item.set_hatch('/') - - self.ax.add_patch(item) - - else: - raise NotImplementedError("Unsupported item shape %s" % shape) - - item.set_zorder(z) - - if overlay: - item.set_animated(True) - self._overlays.add(item) - - return item - - def addMarker(self, x, y, legend, text, color, - selectable, draggable, - symbol, linestyle, linewidth, constraint): - legend = "__MARKER__" + legend - - textArtist = None - - xmin, xmax = self.getGraphXLimits() - ymin, ymax = self.getGraphYLimits(axis='left') - - if x is not None and y is not None: - line = self.ax.plot(x, y, label=legend, - linestyle=" ", - color=color, - marker=symbol, - markersize=10.)[-1] - - if text is not None: - if symbol is None: - valign = 'baseline' - else: - valign = 'top' - text = " " + text - - textArtist = self.ax.text(x, y, text, - color=color, - horizontalalignment='left', - verticalalignment=valign) - - elif x is not None: - line = self.ax.axvline(x, - label=legend, - color=color, - linewidth=linewidth, - linestyle=linestyle) - if text is not None: - # Y position will be updated in updateMarkerText call - textArtist = self.ax.text(x, 1., " " + text, - color=color, - horizontalalignment='left', - verticalalignment='top') - - elif y is not None: - line = self.ax.axhline(y, - label=legend, - color=color, - linewidth=linewidth, - linestyle=linestyle) - - if text is not None: - # X position will be updated in updateMarkerText call - textArtist = self.ax.text(1., y, " " + text, - color=color, - horizontalalignment='right', - verticalalignment='top') - - else: - raise RuntimeError('A marker must at least have one coordinate') - - if selectable or draggable: - line.set_picker(5) - - # All markers are overlays - line.set_animated(True) - if textArtist is not None: - textArtist.set_animated(True) - - artists = [line] if textArtist is None else [line, textArtist] - container = _MarkerContainer(artists, x, y) - container.updateMarkerText(xmin, xmax, ymin, ymax) - self._overlays.add(container) - - return container - - def _updateMarkers(self): - xmin, xmax = self.ax.get_xbound() - ymin, ymax = self.ax.get_ybound() - for item in self._overlays: - if isinstance(item, _MarkerContainer): - item.updateMarkerText(xmin, xmax, ymin, ymax) - - # Remove methods - - def remove(self, item): - # Warning: It also needs to remove extra stuff if added as for markers - self._overlays.discard(item) - try: - item.remove() - except ValueError: - pass # Already removed e.g., in set[X|Y]AxisLogarithmic - - # Interaction methods - - def setGraphCursor(self, flag, color, linewidth, linestyle): - if flag: - lineh = self.ax.axhline( - self.ax.get_ybound()[0], visible=False, color=color, - linewidth=linewidth, linestyle=linestyle) - lineh.set_animated(True) - - linev = self.ax.axvline( - self.ax.get_xbound()[0], visible=False, color=color, - linewidth=linewidth, linestyle=linestyle) - linev.set_animated(True) - - self._graphCursor = lineh, linev - else: - if self._graphCursor is not None: - lineh, linev = self._graphCursor - lineh.remove() - linev.remove() - self._graphCursor = tuple() - - # Active curve - - def setCurveColor(self, curve, color): - # Store Line2D and PathCollection - for artist in curve.get_children(): - if isinstance(artist, (Line2D, LineCollection)): - artist.set_color(color) - elif isinstance(artist, PathCollection): - artist.set_facecolors(color) - artist.set_edgecolors(color) - else: - _logger.warning( - 'setActiveCurve ignoring artist %s', str(artist)) - - # Misc. - - def getWidgetHandle(self): - return self.fig.canvas - - def _enableAxis(self, axis, flag=True): - """Show/hide Y axis - - :param str axis: Axis name: 'left' or 'right' - :param bool flag: Default, True - """ - assert axis in ('right', 'left') - axes = self.ax2 if axis == 'right' else self.ax - axes.get_yaxis().set_visible(flag) - - def replot(self): - """Do not perform rendering. - - Override in subclass to actually draw something. - """ - # TODO images, markers? scatter plot? move in remove? - # Right Y axis only support curve for now - # Hide right Y axis if no line is present - self._dirtyLimits = False - if not self.ax2.lines: - self._enableAxis('right', False) - - def saveGraph(self, fileName, fileFormat, dpi): - # fileName can be also a StringIO or file instance - if dpi is not None: - self.fig.savefig(fileName, format=fileFormat, dpi=dpi) - else: - self.fig.savefig(fileName, format=fileFormat) - self._plot._setDirtyPlot() - - # Graph labels - - def setGraphTitle(self, title): - self.ax.set_title(title) - - def setGraphXLabel(self, label): - self.ax.set_xlabel(label) - - def setGraphYLabel(self, label, axis): - axes = self.ax if axis == 'left' else self.ax2 - axes.set_ylabel(label) - - # Graph limits - - def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): - # Let matplotlib taking care of keep aspect ratio if any - self._dirtyLimits = True - self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax)) - - if y2min is not None and y2max is not None: - if not self.isYAxisInverted(): - self.ax2.set_ylim(min(y2min, y2max), max(y2min, y2max)) - else: - self.ax2.set_ylim(max(y2min, y2max), min(y2min, y2max)) - - if not self.isYAxisInverted(): - self.ax.set_ylim(min(ymin, ymax), max(ymin, ymax)) - else: - self.ax.set_ylim(max(ymin, ymax), min(ymin, ymax)) - - self._updateMarkers() - - def getGraphXLimits(self): - if self._dirtyLimits and self.isKeepDataAspectRatio(): - self.replot() # makes sure we get the right limits - return self.ax.get_xbound() - - def setGraphXLimits(self, xmin, xmax): - self._dirtyLimits = True - self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax)) - self._updateMarkers() - - def getGraphYLimits(self, axis): - assert axis in ('left', 'right') - ax = self.ax2 if axis == 'right' else self.ax - - if not ax.get_visible(): - return None - - if self._dirtyLimits and self.isKeepDataAspectRatio(): - self.replot() # makes sure we get the right limits - - return ax.get_ybound() - - def setGraphYLimits(self, ymin, ymax, axis): - ax = self.ax2 if axis == 'right' else self.ax - if ymax < ymin: - ymin, ymax = ymax, ymin - self._dirtyLimits = True - - if self.isKeepDataAspectRatio(): - # matplotlib keeps limits of shared axis when keeping aspect ratio - # So x limits are kept when changing y limits.... - # Change x limits first by taking into account aspect ratio - # and then change y limits.. so matplotlib does not need - # to make change (to y) to keep aspect ratio - xmin, xmax = ax.get_xbound() - curYMin, curYMax = ax.get_ybound() - - newXRange = (xmax - xmin) * (ymax - ymin) / (curYMax - curYMin) - xcenter = 0.5 * (xmin + xmax) - ax.set_xlim(xcenter - 0.5 * newXRange, xcenter + 0.5 * newXRange) - - if not self.isYAxisInverted(): - ax.set_ylim(ymin, ymax) - else: - ax.set_ylim(ymax, ymin) - - self._updateMarkers() - - # Graph axes - - def setXAxisTimeZone(self, tz): - super(BackendMatplotlib, self).setXAxisTimeZone(tz) - - # Make new formatter and locator with the time zone. - self.setXAxisTimeSeries(self.isXAxisTimeSeries()) - - def isXAxisTimeSeries(self): - return self._isXAxisTimeSeries - - def setXAxisTimeSeries(self, isTimeSeries): - self._isXAxisTimeSeries = isTimeSeries - if self._isXAxisTimeSeries: - # We can't use a matplotlib.dates.DateFormatter because it expects - # the data to be in datetimes. Silx works internally with - # timestamps (floats). - locator = NiceDateLocator(tz=self.getXAxisTimeZone()) - self.ax.xaxis.set_major_locator(locator) - self.ax.xaxis.set_major_formatter( - NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone())) - else: - try: - scalarFormatter = ScalarFormatter(useOffset=False) - except: - _logger.warning('Cannot disabled axes offsets in %s ' % - matplotlib.__version__) - scalarFormatter = ScalarFormatter() - self.ax.xaxis.set_major_formatter(scalarFormatter) - - def setXAxisLogarithmic(self, flag): - # Workaround for matplotlib 2.1.0 when one tries to set an axis - # to log scale with both limits <= 0 - # In this case a draw with positive limits is needed first - if flag and self._matplotlibVersion >= _parse_version('2.1.0'): - xlim = self.ax.get_xlim() - if xlim[0] <= 0 and xlim[1] <= 0: - self.ax.set_xlim(1, 10) - self.draw() - - self.ax2.set_xscale('log' if flag else 'linear') - self.ax.set_xscale('log' if flag else 'linear') - - def setYAxisLogarithmic(self, flag): - # Workaround for matplotlib 2.0 issue with negative bounds - # before switching to log scale - if flag and self._matplotlibVersion >= _parse_version('2.0.0'): - redraw = False - for axis, dataRangeIndex in ((self.ax, 1), (self.ax2, 2)): - ylim = axis.get_ylim() - if ylim[0] <= 0 or ylim[1] <= 0: - dataRange = self._plot.getDataRange()[dataRangeIndex] - if dataRange is None: - dataRange = 1, 100 # Fallback - axis.set_ylim(*dataRange) - redraw = True - if redraw: - self.draw() - - self.ax2.set_yscale('log' if flag else 'linear') - self.ax.set_yscale('log' if flag else 'linear') - - def setYAxisInverted(self, flag): - if self.ax.yaxis_inverted() != bool(flag): - self.ax.invert_yaxis() - - def isYAxisInverted(self): - return self.ax.yaxis_inverted() - - def isKeepDataAspectRatio(self): - return self.ax.get_aspect() in (1.0, 'equal') - - def setKeepDataAspectRatio(self, flag): - self.ax.set_aspect(1.0 if flag else 'auto') - self.ax2.set_aspect(1.0 if flag else 'auto') - - def setGraphGrid(self, which): - self.ax.grid(False, which='both') # Disable all grid first - if which is not None: - self.ax.grid(True, which=which) - - # Data <-> Pixel coordinates conversion - - def _mplQtYAxisCoordConversion(self, y): - """Qt origin (top) to/from matplotlib origin (bottom) conversion. - - :rtype: float - """ - height = self.fig.get_window_extent().height - return height - y - - def dataToPixel(self, x, y, axis): - ax = self.ax2 if axis == "right" else self.ax - - pixels = ax.transData.transform_point((x, y)) - xPixel, yPixel = pixels.T - - # Convert from matplotlib origin (bottom) to Qt origin (top) - yPixel = self._mplQtYAxisCoordConversion(yPixel) - - return xPixel, yPixel - - def pixelToData(self, x, y, axis, check): - ax = self.ax2 if axis == "right" else self.ax - - # Convert from Qt origin (top) to matplotlib origin (bottom) - y = self._mplQtYAxisCoordConversion(y) - - inv = ax.transData.inverted() - x, y = inv.transform_point((x, y)) - - if check: - xmin, xmax = self.getGraphXLimits() - ymin, ymax = self.getGraphYLimits(axis=axis) - - if x > xmax or x < xmin or y > ymax or y < ymin: - return None # (x, y) is out of plot area - - return x, y - - def getPlotBoundsInPixels(self): - bbox = self.ax.get_window_extent() - # Warning this is not returning int... - return (bbox.xmin, - self._mplQtYAxisCoordConversion(bbox.ymax), - bbox.width, - bbox.height) - - def setAxesDisplayed(self, displayed): - """Display or not the axes. - - :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_axis_on() - self.ax2.set_axis_on() - # 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() - # remove external margins - self.ax.set_position([0, 0, 1, 1]) - self.ax2.set_position([0, 0, 1, 1]) - self._plot._setDirtyPlot() - - -class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): - """QWidget matplotlib backend using a QtAgg canvas. - - It adds fast overlay drawing and mouse event management. - """ - - _sigPostRedisplay = qt.Signal() - """Signal handling automatic asynchronous replot""" - - def __init__(self, plot, parent=None): - BackendMatplotlib.__init__(self, plot, parent) - FigureCanvasQTAgg.__init__(self, self.fig) - self.setParent(parent) - - self._limitsBeforeResize = None - - FigureCanvasQTAgg.setSizePolicy( - self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - FigureCanvasQTAgg.updateGeometry(self) - - # Make postRedisplay asynchronous using Qt signal - self._sigPostRedisplay.connect( - super(BackendMatplotlibQt, self).postRedisplay, - qt.Qt.QueuedConnection) - - self._picked = None - - self.mpl_connect('button_press_event', self._onMousePress) - self.mpl_connect('button_release_event', self._onMouseRelease) - 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() - - # Mouse event forwarding - - _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'} - - def _onMousePress(self, event): - self._plot.onMousePress( - event.x, self._mplQtYAxisCoordConversion(event.y), - self._MPL_TO_PLOT_BUTTONS[event.button]) - - def _onMouseMove(self, event): - if self._graphCursor: - lineh, linev = self._graphCursor - if event.inaxes != self.ax and lineh.get_visible(): - lineh.set_visible(False) - linev.set_visible(False) - self._plot._setDirtyPlot(overlayOnly=True) - else: - linev.set_visible(True) - linev.set_xdata((event.xdata, event.xdata)) - lineh.set_visible(True) - lineh.set_ydata((event.ydata, event.ydata)) - self._plot._setDirtyPlot(overlayOnly=True) - # onMouseMove must trigger replot if dirty flag is raised - - self._plot.onMouseMove( - event.x, self._mplQtYAxisCoordConversion(event.y)) - - def _onMouseRelease(self, event): - self._plot.onMouseRelease( - event.x, self._mplQtYAxisCoordConversion(event.y), - self._MPL_TO_PLOT_BUTTONS[event.button]) - - def _onMouseWheel(self, event): - self._plot.onMouseWheel( - event.x, self._mplQtYAxisCoordConversion(event.y), event.step) - - def leaveEvent(self, event): - """QWidget event handler""" - self._plot.onMouseLeaveWidget() - - # picking - - def _onPick(self, event): - # TODO not very nice and fragile, find a better way? - # Make a selection according to kind - if self._picked is None: - _logger.error('Internal picking error') - return - - label = event.artist.get_label() - if label.startswith('__MARKER__'): - self._picked.append({'kind': 'marker', 'legend': label[10:]}) - - elif label.startswith('__IMAGE__'): - self._picked.append({'kind': 'image', 'legend': label[9:]}) - - else: # it's a curve, item have no picker for now - if not isinstance(event.artist, (PathCollection, Line2D)): - _logger.info('Unsupported artist, ignored') - return - - self._picked.append({'kind': 'curve', 'legend': label, - 'indices': event.ind}) - - def pickItems(self, x, y, kinds): - self._picked = [] - - # Weird way to do an explicit picking: Simulate a button press event - mouseEvent = MouseEvent('button_press_event', - self, x, self._mplQtYAxisCoordConversion(y)) - cid = self.mpl_connect('pick_event', self._onPick) - self.fig.pick(mouseEvent) - self.mpl_disconnect(cid) - - picked = [p for p in self._picked if p['kind'] in kinds] - self._picked = None - - return picked - - # replot control - - def resizeEvent(self, event): - # Store current limits - self._limitsBeforeResize = ( - self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound()) - - FigureCanvasQTAgg.resizeEvent(self, event) - if self.isKeepDataAspectRatio() or self._overlays or self._graphCursor: - # This is needed with matplotlib 1.5.x and 2.0.x - self._plot._setDirtyPlot() - - def _drawOverlays(self): - """Draw overlays if any.""" - if self._overlays or self._graphCursor: - # There is some overlays or crosshair - - # This assume that items are only on left/bottom Axes - for item in self._overlays: - self.ax.draw_artist(item) - - for item in self._graphCursor: - self.ax.draw_artist(item) - - def draw(self): - """Overload draw - - It performs a full redraw (including overlays) of the plot. - It also resets background and emit limits changed signal. - - This is directly called by matplotlib for widget resize. - """ - # Starting with mpl 2.1.0, toggling autoscale raises a ValueError - # in some situations. See #1081, #1136, #1163, - if self._matplotlibVersion >= _parse_version("2.0.0"): - try: - FigureCanvasQTAgg.draw(self) - except ValueError as err: - _logger.debug( - "ValueError caught while calling FigureCanvasQTAgg.draw: " - "'%s'", err) - else: - FigureCanvasQTAgg.draw(self) - - if self._overlays or self._graphCursor: - # Save background - self._background = self.copy_from_bbox(self.fig.bbox) - else: - self._background = None # Reset background - - # Check if limits changed due to a resize of the widget - if self._limitsBeforeResize is not None: - xLimits, yLimits, yRightLimits = self._limitsBeforeResize - self._limitsBeforeResize = None - - if (xLimits != self.ax.get_xbound() or - yLimits != self.ax.get_ybound()): - self._updateMarkers() - - if xLimits != self.ax.get_xbound(): - self._plot.getXAxis()._emitLimitsChanged() - if yLimits != self.ax.get_ybound(): - self._plot.getYAxis(axis='left')._emitLimitsChanged() - if yRightLimits != self.ax2.get_ybound(): - self._plot.getYAxis(axis='right')._emitLimitsChanged() - - self._drawOverlays() - - def replot(self): - BackendMatplotlib.replot(self) - - dirtyFlag = self._plot._getDirtyPlot() - - if dirtyFlag == 'overlay': - # Only redraw overlays using fast rendering path - if self._background is None: - self._background = self.copy_from_bbox(self.fig.bbox) - self.restore_region(self._background) - self._drawOverlays() - self.blit(self.fig.bbox) - - elif dirtyFlag: # Need full redraw - self.draw() - - # Workaround issue of rendering overlays with some matplotlib versions - if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and - not hasattr(self, '_firstReplot')): - self._firstReplot = False - if self._overlays or self._graphCursor: - qt.QTimer.singleShot(0, self.draw) # Request async draw - - # cursor - - _QT_CURSORS = { - BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor, - BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor, - BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor, - BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor, - BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor, - } - - def setGraphCursorShape(self, cursor): - if cursor is None: - FigureCanvasQTAgg.unsetCursor(self) - else: - cursor = self._QT_CURSORS[cursor] - FigureCanvasQTAgg.setCursor(self, qt.QCursor(cursor)) diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py deleted file mode 100644 index 9e2cb73..0000000 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ /dev/null @@ -1,1725 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""OpenGL Plot backend.""" - -from __future__ import division - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "01/08/2018" - -from collections import OrderedDict, namedtuple -from ctypes import c_void_p -import logging - -import numpy - -from .._utils import FLOAT32_MINPOS -from . import BackendBase -from ... import colors -from ... import qt - -from ..._glutils import gl -from ... import _glutils as glu -from .glutils import ( - GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D, - mat4Ortho, mat4Identity, - LEFT, RIGHT, BOTTOM, TOP, - Text2D, Shape2D) -from .glutils.PlotImageFile import saveImageToFile - -_logger = logging.getLogger(__name__) - - -# TODO idea: BackendQtMixIn class to share code between mpl and gl -# TODO check if OpenGL is available -# TODO make an off-screen mesa backend - -# Bounds ###################################################################### - -class Range(namedtuple('Range', ('min_', 'max_'))): - """Describes a 1D range""" - - @property - def range_(self): - return self.max_ - self.min_ - - @property - def center(self): - return 0.5 * (self.min_ + self.max_) - - -class Bounds(object): - """Describes plot bounds with 2 y axis""" - - def __init__(self, xMin, xMax, yMin, yMax, y2Min, y2Max): - self._xAxis = Range(xMin, xMax) - self._yAxis = Range(yMin, yMax) - self._y2Axis = Range(y2Min, y2Max) - - def __repr__(self): - return "x: %s, y: %s, y2: %s" % (repr(self._xAxis), - repr(self._yAxis), - repr(self._y2Axis)) - - @property - def xAxis(self): - return self._xAxis - - @property - def yAxis(self): - return self._yAxis - - @property - def y2Axis(self): - return self._y2Axis - - -# Content ##################################################################### - -class PlotDataContent(object): - """Manage plot data content: images and curves. - - This class is only meant to work with _OpenGLPlotCanvas. - """ - - _PRIMITIVE_TYPES = 'curve', 'image' - - def __init__(self): - self._primitives = OrderedDict() # For images and curves - - def add(self, primitive): - """Add a curve or image to the content dictionary. - - This function generates the key in the dict from the primitive. - - :param primitive: The primitive to add. - :type primitive: Instance of GLPlotCurve2D, GLPlotColormap, - GLPlotRGBAImage. - """ - if isinstance(primitive, GLPlotCurve2D): - primitiveType = 'curve' - elif isinstance(primitive, (GLPlotColormap, GLPlotRGBAImage)): - primitiveType = 'image' - else: - raise RuntimeError('Unsupported object type: %s', primitive) - - key = primitiveType, primitive.info['legend'] - self._primitives[key] = primitive - - def get(self, primitiveType, legend): - """Get the corresponding primitive of given type with given legend. - - :param str primitiveType: Type of primitive ('curve' or 'image'). - :param str legend: The legend of the primitive to retrieve. - :return: The corresponding curve or None if no such curve. - """ - assert primitiveType in self._PRIMITIVE_TYPES - return self._primitives.get((primitiveType, legend)) - - def pop(self, primitiveType, key): - """Pop the corresponding curve or return None if no such curve. - - :param str primitiveType: - :param str key: - :return: - """ - assert primitiveType in self._PRIMITIVE_TYPES - return self._primitives.pop((primitiveType, key), None) - - def zOrderedPrimitives(self, reverse=False): - """List of primitives sorted according to their z order. - - It is a stable sort (as sorted): - Original order is preserved when key is the same. - - :param bool reverse: Ascending (True, default) or descending (False). - """ - return sorted(self._primitives.values(), - key=lambda primitive: primitive.info['zOrder'], - reverse=reverse) - - def primitives(self): - """Iterator over all primitives.""" - return self._primitives.values() - - def primitiveKeys(self, primitiveType): - """Iterator over primitives of a specific type.""" - assert primitiveType in self._PRIMITIVE_TYPES - for type_, key in self._primitives.keys(): - if type_ == primitiveType: - yield key - - def getBounds(self, xPositive=False, yPositive=False): - """Bounds of the data. - - Can return strictly positive bounds (for log scale). - In this case, curves are clipped to their smaller positive value - and images with negative min are ignored. - - :param bool xPositive: True to get strictly positive range. - :param bool yPositive: True to get strictly positive range. - :return: The range of data for x, y and y2, or default (1., 100.) - if no range found for one dimension. - :rtype: Bounds - """ - xMin, yMin, y2Min = float('inf'), float('inf'), float('inf') - xMax = 0. if xPositive else -float('inf') - if yPositive: - yMax, y2Max = 0., 0. - else: - yMax, y2Max = -float('inf'), -float('inf') - - for item in self._primitives.values(): - # To support curve <= 0. and log and bypass images: - # If positive only, uses x|yMinPos if available - # and bypass other data with negative min bounds - if xPositive: - itemXMin = getattr(item, 'xMinPos', item.xMin) - if itemXMin is None or itemXMin < FLOAT32_MINPOS: - continue - else: - itemXMin = item.xMin - - if yPositive: - itemYMin = getattr(item, 'yMinPos', item.yMin) - if itemYMin is None or itemYMin < FLOAT32_MINPOS: - continue - else: - itemYMin = item.yMin - - if itemXMin < xMin: - xMin = itemXMin - if item.xMax > xMax: - xMax = item.xMax - - if item.info.get('yAxis') == 'right': - if itemYMin < y2Min: - y2Min = itemYMin - if item.yMax > y2Max: - y2Max = item.yMax - else: - if itemYMin < yMin: - yMin = itemYMin - if item.yMax > yMax: - yMax = item.yMax - - # One of the limit has not been updated, return default range - if xMin >= xMax: - xMin, xMax = 1., 100. - if yMin >= yMax: - yMin, yMax = 1., 100. - if y2Min >= y2Max: - y2Min, y2Max = 1., 100. - - return Bounds(xMin, xMax, yMin, yMax, y2Min, y2Max) - - -# shaders ##################################################################### - -_baseVertShd = """ - attribute vec2 position; - uniform mat4 matrix; - uniform bvec2 isLog; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec2 posTransformed = position; - if (isLog.x) { - posTransformed.x = oneOverLog10 * log(position.x); - } - if (isLog.y) { - posTransformed.y = oneOverLog10 * log(position.y); - } - gl_Position = matrix * vec4(posTransformed, 0.0, 1.0); - } - """ - -_baseFragShd = """ - uniform vec4 color; - uniform int hatchStep; - uniform float tickLen; - - void main(void) { - if (tickLen != 0.) { - if (mod((gl_FragCoord.x + gl_FragCoord.y) / tickLen, 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } else if (hatchStep == 0 || - mod(gl_FragCoord.x - gl_FragCoord.y, float(hatchStep)) == 0.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - -_texVertShd = """ - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 coords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - coords = texCoords; - } - """ - -_texFragShd = """ - uniform sampler2D tex; - - varying vec2 coords; - - void main(void) { - gl_FragColor = texture2D(tex, coords); - gl_FragColor.a = 1.0; - } - """ - - -# BackendOpenGL ############################################################### - -_current_context = None - - -def _getContext(): - assert _current_context is not None - return _current_context - - -class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): - """OpenGL-based Plot backend. - - WARNINGS: - Unless stated otherwise, this API is NOT thread-safe and MUST be - called from the main thread. - When numpy arrays are passed as arguments to the API (through - :func:`addCurve` and :func:`addImage`), they are copied only if - required. - So, the caller should not modify these arrays afterwards. - """ - - _sigPostRedisplay = qt.Signal() - """Signal handling automatic asynchronous replot""" - - def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()): - glu.OpenGLWidget.__init__(self, parent, - alphaBufferSize=8, - depthBufferSize=0, - stencilBufferSize=0, - version=(2, 1), - f=f) - BackendBase.BackendBase.__init__(self, plot, parent) - - self.matScreenProj = mat4Identity() - - self._progBase = glu.Program( - _baseVertShd, _baseFragShd, attrib0='position') - self._progTex = glu.Program( - _texVertShd, _texFragShd, attrib0='position') - self._plotFBOs = {} - - self._keepDataAspectRatio = False - - self._crosshairCursor = None - self._mousePosInPixels = None - - self._markers = OrderedDict() - self._items = OrderedDict() - self._plotContent = PlotDataContent() # For images and curves - self._glGarbageCollector = [] - - self._plotFrame = GLPlotFrame2D( - margins={'left': 100, 'right': 50, 'top': 50, 'bottom': 50}) - - # Make postRedisplay asynchronous using Qt signal - self._sigPostRedisplay.connect( - super(BackendOpenGL, self).postRedisplay, - qt.Qt.QueuedConnection) - - self.setAutoFillBackground(False) - self.setMouseTracking(True) - - # QWidget - - _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 - - def mousePressEvent(self, event): - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - btn = self._MOUSE_BTNS[event.button()] - self._plot.onMousePress(xPixel, yPixel, btn) - 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 - - previousMousePosInPixels = self._mousePosInPixels - self._mousePosInPixels = (xPixel, yPixel) if isCursorInPlot else None - 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) - event.accept() - - def mouseReleaseEvent(self, event): - xPixel = event.x() * self.getDevicePixelRatio() - yPixel = event.y() * self.getDevicePixelRatio() - - btn = self._MOUSE_BTNS[event.button()] - self._plot.onMouseRelease(xPixel, yPixel, btn) - 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) - event.accept() - - def leaveEvent(self, _): - self._plot.onMouseLeaveWidget() - - # OpenGLWidget API - - def initializeGL(self): - gl.testGL() - - gl.glClearColor(1., 1., 1., 1.) - gl.glClearStencil(0) - - gl.glEnable(gl.GL_BLEND) - # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA, - gl.GL_ONE_MINUS_SRC_ALPHA, - gl.GL_ONE, - gl.GL_ONE) - - # For lines - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - - # For points - gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2 - gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2 - # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE) - - def _paintDirectGL(self): - self._renderPlotAreaGL() - self._plotFrame.render() - self._renderMarkersGL() - self._renderOverlayGL() - - def _paintFBOGL(self): - context = glu.getGLContext() - plotFBOTex = self._plotFBOs.get(context) - if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or - plotFBOTex is None): - self._plotVertices = numpy.array(((-1., -1., 0., 0.), - (1., -1., 1., 0.), - (-1., 1., 0., 1.), - (1., 1., 1., 1.)), - dtype=numpy.float32) - if plotFBOTex is None or \ - plotFBOTex.shape[1] != self._plotFrame.size[0] or \ - plotFBOTex.shape[0] != self._plotFrame.size[1]: - if plotFBOTex is not None: - plotFBOTex.discard() - plotFBOTex = glu.FramebufferTexture( - gl.GL_RGBA, - shape=(self._plotFrame.size[1], - self._plotFrame.size[0]), - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)) - self._plotFBOs[context] = plotFBOTex - - with plotFBOTex: - gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT) - self._renderPlotAreaGL() - self._plotFrame.render() - - # Render plot in screen coords - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - self._progTex.use() - texUnit = 0 - - gl.glUniform1i(self._progTex.uniforms['tex'], texUnit) - gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE, - mat4Identity().astype(numpy.float32)) - - stride = self._plotVertices.shape[-1] * self._plotVertices.itemsize - gl.glEnableVertexAttribArray(self._progTex.attributes['position']) - gl.glVertexAttribPointer(self._progTex.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, self._plotVertices) - - texCoordsPtr = c_void_p(self._plotVertices.ctypes.data + - 2 * self._plotVertices.itemsize) # Better way? - gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords']) - gl.glVertexAttribPointer(self._progTex.attributes['texCoords'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, texCoordsPtr) - - with plotFBOTex.texture: - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices)) - - self._renderMarkersGL() - self._renderOverlayGL() - - def paintGL(self): - global _current_context - _current_context = self.context() - - glu.setGLContextGetter(_getContext) - - # Release OpenGL resources - for item in self._glGarbageCollector: - item.discard() - self._glGarbageCollector = [] - - 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: - return - - # self._paintDirectGL() - self._paintFBOGL() - - glu.setGLContextGetter() - _current_context = None - - def _nonOrthoAxesLineMarkerPrimitives(self, marker, pixelOffset): - """Generates the vertices and label for a line marker. - - :param dict marker: Description of a line marker - :param int pixelOffset: Offset of text from borders in pixels - :return: Line vertices and Text label or None - :rtype: 2-tuple (2x2 numpy.array of float, Text2D) - """ - label, vertices = None, None - - xCoord, yCoord = marker['x'], marker['y'] - assert xCoord is None or yCoord is None # Specific to line markers - - # Get plot corners in data coords - plotLeft, plotTop, plotWidth, plotHeight = self.getPlotBoundsInPixels() - - corners = [(plotLeft, plotTop), - (plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop)] - corners = numpy.array([self.pixelToData(x, y, axis='left', check=False) - for (x, y) in corners]) - - borders = { - 'right': (corners[3], corners[2]), - 'top': (corners[0], corners[3]), - 'bottom': (corners[2], corners[1]), - 'left': (corners[1], corners[0]) - } - - textLayouts = { # align, valign, offsets - 'right': (RIGHT, BOTTOM, (-1., -1.)), - 'top': (LEFT, TOP, (1., 1.)), - 'bottom': (LEFT, BOTTOM, (1., -1.)), - 'left': (LEFT, BOTTOM, (1., -1.)) - } - - if xCoord is None: # Horizontal line in data space - if marker['text'] is not None: - # Find intersection of hline with borders in data - # Order is important as it stops at first intersection - for border_name in ('right', 'top', 'bottom', 'left'): - (x0, y0), (x1, y1) = borders[border_name] - - if min(y0, y1) <= yCoord < max(y0, y1): - xIntersect = (yCoord - y0) * (x1 - x0) / (y1 - y0) + x0 - - # Add text label - pixelPos = self.dataToPixel( - xIntersect, yCoord, axis='left', check=False) - - align, valign, offsets = textLayouts[border_name] - - x = pixelPos[0] + offsets[0] * pixelOffset - y = pixelPos[1] + offsets[1] * pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=align, valign=valign) - break # Stop at first intersection - - xMin, xMax = corners[:, 0].min(), corners[:, 0].max() - vertices = numpy.array( - ((xMin, yCoord), (xMax, yCoord)), dtype=numpy.float32) - - else: # yCoord is None: vertical line in data space - if marker['text'] is not None: - # Find intersection of hline with borders in data - # Order is important as it stops at first intersection - for border_name in ('top', 'bottom', 'right', 'left'): - (x0, y0), (x1, y1) = borders[border_name] - if min(x0, x1) <= xCoord < max(x0, x1): - yIntersect = (xCoord - x0) * (y1 - y0) / (x1 - x0) + y0 - - # Add text label - pixelPos = self.dataToPixel( - xCoord, yIntersect, axis='left', check=False) - - align, valign, offsets = textLayouts[border_name] - - x = pixelPos[0] + offsets[0] * pixelOffset - y = pixelPos[1] + offsets[1] * pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=align, valign=valign) - break # Stop at first intersection - - yMin, yMax = corners[:, 1].min(), corners[:, 1].max() - vertices = numpy.array( - ((xCoord, yMin), (xCoord, yMax)), dtype=numpy.float32) - - return vertices, label - - def _renderMarkersGL(self): - if len(self._markers) == 0: - return - - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - - # Render in plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - # Prepare vertical and horizontal markers rendering - self._progBase.use() - gl.glUniformMatrix4fv( - self._progBase.uniforms['matrix'], 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1i(self._progBase.uniforms['hatchStep'], 0) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - posAttrib = self._progBase.attributes['position'] - - labels = [] - pixelOffset = 3 - - for marker in self._markers.values(): - xCoord, yCoord = marker['x'], marker['y'] - - if ((self._plotFrame.xAxis.isLog and - xCoord is not None and - xCoord <= 0) or - (self._plotFrame.yAxis.isLog and - yCoord is not None and - yCoord <= 0)): - # Do not render markers with negative coords on log axis - continue - - if xCoord is None or yCoord is None: - if not self.isDefaultBaseVectors(): # Non-orthogonal axes - vertices, label = self._nonOrthoAxesLineMarkerPrimitives( - marker, pixelOffset) - if label is not None: - labels.append(label) - - else: # Orthogonal axes - pixelPos = self.dataToPixel( - xCoord, yCoord, axis='left', check=False) - - if xCoord is None: # Horizontal line in data space - if marker['text'] is not None: - x = self._plotFrame.size[0] - \ - self._plotFrame.margins.right - pixelOffset - y = pixelPos[1] - pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=RIGHT, valign=BOTTOM) - labels.append(label) - - width = self._plotFrame.size[0] - vertices = numpy.array(((0, pixelPos[1]), - (width, pixelPos[1])), - dtype=numpy.float32) - - else: # yCoord is None: vertical line in data space - if marker['text'] is not None: - x = pixelPos[0] + pixelOffset - y = self._plotFrame.margins.top + pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=LEFT, valign=TOP) - labels.append(label) - - height = self._plotFrame.size[1] - vertices = numpy.array(((pixelPos[0], 0), - (pixelPos[0], height)), - dtype=numpy.float32) - - self._progBase.use() - gl.glUniform4f(self._progBase.uniforms['color'], - *marker['color']) - - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - gl.glLineWidth(1) - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - else: - pixelPos = self.dataToPixel( - xCoord, yCoord, axis='left', check=True) - if pixelPos is None: - # Do not render markers outside visible plot area - continue - - if marker['text'] is not None: - x = pixelPos[0] + pixelOffset - y = pixelPos[1] + pixelOffset - label = Text2D(marker['text'], x, y, - color=marker['color'], - bgColor=(1., 1., 1., 0.5), - align=LEFT, valign=TOP) - 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( - numpy.array((pixelPos[0],), dtype=numpy.float64), - numpy.array((pixelPos[1],), dtype=numpy.float64), - marker=marker['symbol'], - markerColor=marker['color'], - markerSize=11) - markerCurve.render(self.matScreenProj, False, False) - markerCurve.discard() - - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - # Render marker labels - for label in labels: - label.render(self.matScreenProj) - - gl.glDisable(gl.GL_SCISSOR_TEST) - - def _renderOverlayGL(self): - # Render crosshair cursor - if self._crosshairCursor is not None: - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - - # Scissor to plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - self._progBase.use() - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - posAttrib = self._progBase.attributes['position'] - matrixUnif = self._progBase.uniforms['matrix'] - colorUnif = self._progBase.uniforms['color'] - hatchStepUnif = self._progBase.uniforms['hatchStep'] - - # Render crosshair cursor in screen frame but with scissor - if (self._crosshairCursor is not None and - self._mousePosInPixels is not None): - gl.glViewport( - 0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - - color, lineWidth = self._crosshairCursor - gl.glUniform4f(colorUnif, *color) - gl.glUniform1i(hatchStepUnif, 0) - - xPixel, yPixel = self._mousePosInPixels - xPixel, yPixel = xPixel + 0.5, yPixel + 0.5 - vertices = numpy.array(((0., yPixel), - (self._plotFrame.size[0], yPixel), - (xPixel, 0.), - (xPixel, self._plotFrame.size[1])), - dtype=numpy.float32) - - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - gl.glLineWidth(lineWidth) - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - gl.glDisable(gl.GL_SCISSOR_TEST) - - def _renderPlotAreaGL(self): - plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] - - self._plotFrame.renderGrid() - - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - # Matrix - trBounds = self._plotFrame.transformedDataRanges - if trBounds.x[0] == trBounds.x[1] or \ - trBounds.y[0] == trBounds.y[1]: - return - - isXLog = self._plotFrame.xAxis.isLog - isYLog = self._plotFrame.yAxis.isLog - - gl.glViewport(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - - # Render images and curves - # sorted is stable: original order is preserved when key is the same - for item in self._plotContent.zOrderedPrimitives(): - if item.info.get('yAxis') == 'right': - item.render(self._plotFrame.transformedDataY2ProjMat, - isXLog, isYLog) - else: - item.render(self._plotFrame.transformedDataProjMat, - isXLog, isYLog) - - # Render Items - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - self._progBase.use() - gl.glUniformMatrix4fv(self._progBase.uniforms['matrix'], 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - - for item in self._items.values(): - if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or - (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)): - # Ignore items <= 0. on log axes - continue - - closed = item['shape'] != 'polylines' - points = [self.dataToPixel(x, y, axis='left', check=False) - for (x, y) in zip(item['x'], item['y'])] - shape2D = Shape2D(points, - fill=item['fill'], - fillColor=item['color'], - stroke=True, - strokeColor=item['color'], - strokeClosed=closed) - - posAttrib = self._progBase.attributes['position'] - colorUnif = self._progBase.uniforms['color'] - hatchStepUnif = self._progBase.uniforms['hatchStep'] - shape2D.render(posAttrib, colorUnif, hatchStepUnif) - - gl.glDisable(gl.GL_SCISSOR_TEST) - - def resizeGL(self, width, height): - if width == 0 or height == 0: # Do not resize - return - - self._plotFrame.size = ( - int(self.getDevicePixelRatio() * width), - int(self.getDevicePixelRatio() * height)) - - self.matScreenProj = mat4Ortho(0, self._plotFrame.size[0], - self._plotFrame.size[1], 0, - 1, -1) - - # Store current ranges - previousXRange = self.getGraphXLimits() - previousYRange = self.getGraphYLimits(axis='left') - previousYRightRange = self.getGraphYLimits(axis='right') - - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \ - self._plotFrame.dataRanges - self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max) - - # If plot range has changed, then emit signal - if previousXRange != self.getGraphXLimits(): - self._plot.getXAxis()._emitLimitsChanged() - if previousYRange != self.getGraphYLimits(axis='left'): - self._plot.getYAxis(axis='left')._emitLimitsChanged() - if previousYRightRange != self.getGraphYLimits(axis='right'): - self._plot.getYAxis(axis='right')._emitLimitsChanged() - - # Add methods - - @staticmethod - def _castArrayTo(v): - """Returns best floating type to cast the array to. - - :param numpy.ndarray v: Array to cast - :rtype: numpy.dtype - :raise ValueError: If dtype is not supported - """ - if numpy.issubdtype(v.dtype, numpy.floating): - return numpy.float32 if v.itemsize <= 4 else numpy.float64 - elif numpy.issubdtype(v.dtype, numpy.integer): - return numpy.float32 if v.itemsize <= 2 else numpy.float64 - else: - raise ValueError('Unsupported data type') - - def addCurve(self, x, y, legend, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, z, selectable, - fill, alpha, symbolsize): - for parameter in (x, y, legend, color, symbol, linewidth, linestyle, - yaxis, z, selectable, fill, symbolsize): - assert parameter is not None - assert yaxis in ('left', 'right') - - # Convert input data - x = numpy.array(x, copy=False) - y = numpy.array(y, copy=False) - - # Check if float32 is enough - if (self._castArrayTo(x) is numpy.float32 and - self._castArrayTo(y) is numpy.float32): - dtype = numpy.float32 - else: - dtype = numpy.float64 - - x = numpy.array(x, dtype=dtype, copy=False, order='C') - y = numpy.array(y, dtype=dtype, copy=False, order='C') - - # Convert errors to float32 - if xerror is not None: - xerror = numpy.array( - xerror, dtype=numpy.float32, copy=False, order='C') - if yerror is not None: - yerror = numpy.array( - yerror, dtype=numpy.float32, copy=False, order='C') - - # Handle axes log scale: convert data - - if self._plotFrame.xAxis.isLog: - logX = numpy.log10(x) - - if xerror is not None: - # Transform xerror so that - # log10(x) +/- xerror' = log10(x +/- xerror) - if hasattr(xerror, 'shape') and len(xerror.shape) == 2: - xErrorMinus, xErrorPlus = xerror[0], xerror[1] - else: - xErrorMinus, xErrorPlus = xerror, xerror - xErrorMinus = logX - numpy.log10(x - xErrorMinus) - xErrorPlus = numpy.log10(x + xErrorPlus) - logX - xerror = numpy.array((xErrorMinus, xErrorPlus), - dtype=numpy.float32) - - x = logX - - isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or ( - yaxis == 'right' and self._plotFrame.y2Axis.isLog) - - if isYLog: - logY = numpy.log10(y) - - if yerror is not None: - # Transform yerror so that - # log10(y) +/- yerror' = log10(y +/- yerror) - if hasattr(yerror, 'shape') and len(yerror.shape) == 2: - yErrorMinus, yErrorPlus = yerror[0], yerror[1] - else: - yErrorMinus, yErrorPlus = yerror, yerror - yErrorMinus = logY - numpy.log10(y - yErrorMinus) - yErrorPlus = numpy.log10(y + yErrorPlus) - logY - yerror = numpy.array((yErrorMinus, yErrorPlus), - dtype=numpy.float32) - - y = logY - - # TODO check if need more filtering of error (e.g., clip to positive) - - # TODO check and improve this - if (len(color) == 4 and - type(color[3]) in [type(1), numpy.uint8, numpy.int8]): - color = numpy.array(color, dtype=numpy.float32) / 255. - - if isinstance(color, numpy.ndarray) and color.ndim == 2: - colorArray = color - color = None - else: - colorArray = None - color = colors.rgba(color) - - if alpha < 1.: # Apply image transparency - if colorArray is not None and colorArray.shape[1] == 4: - # multiply alpha channel - colorArray[:, 3] = colorArray[:, 3] * alpha - if color is not None: - color = color[0], color[1], color[2], color[3] * alpha - - behaviors = set() - if selectable: - behaviors.add('selectable') - - curve = GLPlotCurve2D(x, y, colorArray, - xError=xerror, - yError=yerror, - lineStyle=linestyle, - lineColor=color, - lineWidth=linewidth, - marker=symbol, - markerColor=color, - markerSize=symbolsize, - fillColor=color if fill else None, - isYLog=isYLog) - curve.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors, - 'yAxis': 'left' if yaxis is None else yaxis, - } - - if yaxis == "right": - self._plotFrame.isY2Axis = True - - self._plotContent.add(curve) - - return legend, 'curve' - - def addImage(self, data, legend, - origin, scale, z, - selectable, draggable, - colormap, alpha): - for parameter in (data, legend, origin, scale, z, - selectable, draggable): - assert parameter is not None - - behaviors = set() - if selectable: - behaviors.add('selectable') - if draggable: - behaviors.add('draggable') - - if data.ndim == 2: - # Ensure array is contiguous and eventually convert its type - if data.dtype in (numpy.float32, numpy.uint8, numpy.uint16): - data = numpy.array(data, copy=False, order='C') - else: - _logger.info( - '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) - image.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors - } - self._plotContent.add(image) - - elif len(data.shape) == 3: - # For RGB, RGBA data - assert data.shape[2] in (3, 4) - - if numpy.issubdtype(data.dtype, numpy.floating): - data = numpy.array(data, dtype=numpy.float32, copy=False) - elif numpy.issubdtype(data.dtype, numpy.integer): - data = numpy.array(data, dtype=numpy.uint8, copy=False) - else: - raise ValueError('Unsupported data type') - - image = GLPlotRGBAImage(data, origin, scale, alpha) - - image.info = { - 'legend': legend, - 'zOrder': z, - 'behaviors': behaviors - } - - if self._plotFrame.xAxis.isLog and image.xMin <= 0.: - raise RuntimeError( - 'Cannot add image with X <= 0 with X axis log scale') - if self._plotFrame.yAxis.isLog and image.yMin <= 0.: - raise RuntimeError( - 'Cannot add image with Y <= 0 with Y axis log scale') - - self._plotContent.add(image) - - else: - raise RuntimeError("Unsupported data shape {0}".format(data.shape)) - - return legend, 'image' - - def addItem(self, x, y, legend, shape, color, fill, overlay, z): - # TODO handle overlay - if shape not in ('polygon', 'rectangle', 'line', - 'vline', 'hline', 'polylines'): - raise NotImplementedError("Unsupported shape {0}".format(shape)) - - x = numpy.array(x, copy=False) - y = numpy.array(y, copy=False) - - if shape == 'rectangle': - xMin, xMax = x - x = numpy.array((xMin, xMin, xMax, xMax)) - yMin, yMax = y - y = numpy.array((yMin, yMax, yMax, yMin)) - - # TODO is this needed? - if self._plotFrame.xAxis.isLog and x.min() <= 0.: - raise RuntimeError( - 'Cannot add item with X <= 0 with X axis log scale') - if self._plotFrame.yAxis.isLog and y.min() <= 0.: - raise RuntimeError( - 'Cannot add item with Y <= 0 with Y axis log scale') - - # Ignore fill for polylines to mimic matplotlib - fill = fill if shape != 'polylines' else False - - self._items[legend] = { - 'shape': shape, - 'color': colors.rgba(color), - 'fill': 'hatch' if fill else None, - 'x': x, - 'y': y - } - - return legend, 'item' - - def addMarker(self, x, y, legend, text, color, - selectable, draggable, - symbol, linestyle, linewidth, constraint): - - if symbol is None: - symbol = '+' - - if linestyle != '-' or linewidth != 1: - _logger.warning( - 'OpenGL backend does not support marker line style and width.') - - behaviors = set() - if selectable: - behaviors.add('selectable') - if draggable: - behaviors.add('draggable') - - # Apply constraint to provided position - isConstraint = (draggable and constraint is not None and - x is not None and y is not None) - if isConstraint: - x, y = constraint(x, y) - - self._markers[legend] = { - 'x': x, - 'y': y, - 'legend': legend, - 'text': text, - 'color': colors.rgba(color), - 'behaviors': behaviors, - 'constraint': constraint if isConstraint else None, - 'symbol': symbol, - } - - return legend, 'marker' - - # Remove methods - - def remove(self, item): - legend, kind = item - - if kind == 'curve': - curve = self._plotContent.pop('curve', legend) - if curve is not None: - # Check if some curves remains on the right Y axis - y2AxisItems = (item for item in self._plotContent.primitives() - if item.info.get('yAxis', 'left') == 'right') - self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None - - self._glGarbageCollector.append(curve) - - elif kind == 'image': - image = self._plotContent.pop('image', legend) - if image is not None: - self._glGarbageCollector.append(image) - - elif kind == 'marker': - self._markers.pop(legend, False) - - elif kind == 'item': - self._items.pop(legend, False) - - else: - _logger.error('Unsupported kind: %s', str(kind)) - - # Interaction methods - - _QT_CURSORS = { - BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor, - BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor, - BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor, - BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor, - BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor, - } - - def setGraphCursorShape(self, cursor): - if cursor is None: - super(BackendOpenGL, self).unsetCursor() - else: - cursor = self._QT_CURSORS[cursor] - super(BackendOpenGL, self).setCursor(qt.QCursor(cursor)) - - def setGraphCursor(self, flag, color, linewidth, linestyle): - if linestyle is not '-': - _logger.warning( - "BackendOpenGL.setGraphCursor linestyle parameter ignored") - - if flag: - color = colors.rgba(color) - crosshairCursor = color, linewidth - else: - crosshairCursor = None - - if crosshairCursor != self._crosshairCursor: - self._crosshairCursor = crosshairCursor - - _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 - - def pickItems(self, x, y, kinds): - picked = [] - - dataPos = self.pixelToData(x, y, axis='left', check=True) - if dataPos is not None: - # Pick markers - if 'marker' in kinds: - for marker in reversed(list(self._markers.values())): - pixelPos = self.dataToPixel( - marker['x'], marker['y'], axis='left', check=False) - if pixelPos is None: # negative coord on a log axis - continue - - if marker['x'] is None: # Horizontal line - pt1 = self.pixelToData( - x, y - self._PICK_OFFSET, axis='left', check=False) - pt2 = self.pixelToData( - x, y + self._PICK_OFFSET, axis='left', check=False) - isPicked = (min(pt1[1], pt2[1]) <= marker['y'] <= - max(pt1[1], pt2[1])) - - elif marker['y'] is None: # Vertical line - pt1 = self.pixelToData( - x - self._PICK_OFFSET, y, axis='left', check=False) - pt2 = self.pixelToData( - x + self._PICK_OFFSET, y, axis='left', check=False) - isPicked = (min(pt1[0], pt2[0]) <= marker['x'] <= - max(pt1[0], pt2[0])) - - else: - isPicked = ( - numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and - numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET) - - if isPicked: - picked.append(dict(kind='marker', - legend=marker['legend'])) - - # Pick image and curves - if 'image' in kinds or 'curve' in kinds: - for item in self._plotContent.zOrderedPrimitives(reverse=True): - if ('image' in kinds and - isinstance(item, (GLPlotColormap, GLPlotRGBAImage))): - pickedPos = item.pick(*dataPos) - if pickedPos is not None: - picked.append(dict(kind='image', - legend=item.info['legend'])) - - elif 'curve' in kinds and isinstance(item, GLPlotCurve2D): - offset = self._PICK_OFFSET - if item.marker is not None: - offset = max(item.markerSize / 2., offset) - if item.lineStyle is not None: - offset = max(item.lineWidth / 2., offset) - - yAxis = item.info['yAxis'] - - inAreaPos = self._mouseInPlotArea(x - offset, y - offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) - if dataPos is None: - continue - xPick0, yPick0 = dataPos - - inAreaPos = self._mouseInPlotArea(x + offset, y + offset) - dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1], - axis=yAxis, check=True) - if dataPos is None: - continue - xPick1, yPick1 = dataPos - - if xPick0 < xPick1: - xPickMin, xPickMax = xPick0, xPick1 - else: - xPickMin, xPickMax = xPick1, xPick0 - - if yPick0 < yPick1: - yPickMin, yPickMax = yPick0, yPick1 - else: - yPickMin, yPickMax = yPick1, yPick0 - - # Apply log scale if axis is log - if self._plotFrame.xAxis.isLog: - xPickMin = numpy.log10(xPickMin) - xPickMax = numpy.log10(xPickMax) - - if (yAxis == 'left' and self._plotFrame.yAxis.isLog) or ( - yAxis == 'right' and self._plotFrame.y2Axis.isLog): - yPickMin = numpy.log10(yPickMin) - yPickMax = numpy.log10(yPickMax) - - pickedIndices = item.pick(xPickMin, yPickMin, - xPickMax, yPickMax) - if pickedIndices: - picked.append(dict(kind='curve', - legend=item.info['legend'], - indices=pickedIndices)) - - return picked - - # Update curve - - def setCurveColor(self, curve, color): - pass # TODO - - # Misc. - - def getWidgetHandle(self): - return self - - def postRedisplay(self): - self._sigPostRedisplay.emit() - - def replot(self): - self.update() # async redraw - # self.repaint() # immediate redraw - - def saveGraph(self, fileName, fileFormat, dpi): - if dpi is not None: - _logger.warning("saveGraph ignores dpi parameter") - - if fileFormat not in ['png', 'ppm', 'svg', 'tiff']: - raise NotImplementedError('Unsupported format: %s' % fileFormat) - - if not self.isValid(): - _logger.error('OpenGL 2.1 not available, cannot save OpenGL image') - width, height = self._plotFrame.size - data = numpy.zeros((height, width, 3), dtype=numpy.uint8) - else: - self.makeCurrent() - - data = numpy.empty( - (self._plotFrame.size[1], self._plotFrame.size[0], 3), - dtype=numpy.uint8, order='C') - - context = self.context() - framebufferTexture = self._plotFBOs.get(context) - if framebufferTexture is None: - # Fallback, supports direct rendering mode: _paintDirectGL - # might have issues as it can read on-screen framebuffer - fboName = self.defaultFramebufferObject() - width, height = self._plotFrame.size - else: - fboName = framebufferTexture.name - height, width = framebufferTexture.shape - - previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glReadPixels(0, 0, width, height, - gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer) - - # glReadPixels gives bottom to top, - # while images are stored as top to bottom - data = numpy.flipud(data) - - # fileName is either a file-like object or a str - saveImageToFile(data, fileName, fileFormat) - - # Graph labels - - def setGraphTitle(self, title): - self._plotFrame.title = title - - def setGraphXLabel(self, label): - self._plotFrame.xAxis.title = label - - def setGraphYLabel(self, label, axis): - if axis == 'left': - self._plotFrame.yAxis.title = label - else: # right axis - if label: - _logger.warning('Right axis label not implemented') - - # Non orthogonal axes - - def setBaseVectors(self, x=(1., 0.), y=(0., 1.)): - """Set base vectors. - - Useful for non-orthogonal axes. - If an axis is in log scale, skew is applied to log transformed values. - - Base vector does not work well with log axes, to investi - """ - if x != (1., 0.) and y != (0., 1.): - if self._plotFrame.xAxis.isLog: - _logger.warning("setBaseVectors disables X axis logarithmic.") - self.setXAxisLogarithmic(False) - if self._plotFrame.yAxis.isLog: - _logger.warning("setBaseVectors disables Y axis logarithmic.") - self.setYAxisLogarithmic(False) - - if self.isKeepDataAspectRatio(): - _logger.warning("setBaseVectors disables keepDataAspectRatio.") - self.keepDataAspectRatio(False) - - self._plotFrame.baseVectors = x, y - - def getBaseVectors(self): - return self._plotFrame.baseVectors - - def isDefaultBaseVectors(self): - return self._plotFrame.baseVectors == \ - self._plotFrame.DEFAULT_BASE_VECTORS - - # Graph limits - - def _setDataRanges(self, xlim=None, ylim=None, y2lim=None): - """Set the visible range of data in the plot frame. - - This clips the ranges to possible values (takes care of float32 - range + positive range for log). - This also takes care of non-orthogonal axes. - - This should be moved to PlotFrame. - """ - # Update axes range with a clipped range if too wide - self._plotFrame.setDataRanges(xlim, ylim, y2lim) - - if not self.isDefaultBaseVectors(): - # Update axes range with axes bounds in data coords - plotLeft, plotTop, plotWidth, plotHeight = \ - self.getPlotBoundsInPixels() - - self._plotFrame.xAxis.dataRange = sorted([ - self.pixelToData(x, y, axis='left', check=False)[0] - for (x, y) in ((plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight))]) - - self._plotFrame.yAxis.dataRange = sorted([ - self.pixelToData(x, y, axis='left', check=False)[1] - for (x, y) in ((plotLeft, plotTop + plotHeight), - (plotLeft, plotTop))]) - - self._plotFrame.y2Axis.dataRange = sorted([ - self.pixelToData(x, y, axis='right', check=False)[1] - for (x, y) in ((plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop))]) - - def _ensureAspectRatio(self, keepDim=None): - """Update plot bounds in order to keep aspect ratio. - - Warning: keepDim on right Y axis is not implemented ! - - :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:] - if plotWidth <= 2 or plotHeight <= 2: - return - - if keepDim is None: - dataBounds = self._plotContent.getBounds( - self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog) - if dataBounds.yAxis.range_ != 0.: - dataRatio = dataBounds.xAxis.range_ - dataRatio /= float(dataBounds.yAxis.range_) - - plotRatio = plotWidth / float(plotHeight) # Test != 0 before - - keepDim = 'x' if dataRatio > plotRatio else 'y' - else: # Limit case - keepDim = 'x' - - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \ - self._plotFrame.dataRanges - if keepDim == 'y': - dataW = (yMax - yMin) * plotWidth / float(plotHeight) - xCenter = 0.5 * (xMin + xMax) - xMin = xCenter - 0.5 * dataW - xMax = xCenter + 0.5 * dataW - elif keepDim == 'x': - dataH = (xMax - xMin) * plotHeight / float(plotWidth) - yCenter = 0.5 * (yMin + yMax) - yMin = yCenter - 0.5 * dataH - yMax = yCenter + 0.5 * dataH - y2Center = 0.5 * (y2Min + y2Max) - y2Min = y2Center - 0.5 * dataH - y2Max = y2Center + 0.5 * dataH - else: - raise RuntimeError('Unsupported dimension to keep: %s' % keepDim) - - # Update plot frame bounds - self._setDataRanges(xlim=(xMin, xMax), - ylim=(yMin, yMax), - y2lim=(y2Min, y2Max)) - - def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None, - keepDim=None): - # Update axes range with a clipped range if too wide - self._setDataRanges(xlim=xRange, - ylim=yRange, - y2lim=y2Range) - - # Keep data aspect ratio - if self.isKeepDataAspectRatio(): - self._ensureAspectRatio(keepDim) - - def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): - assert xmin < xmax - assert ymin < ymax - - if y2min is None or y2max is None: - y2Range = None - else: - assert y2min < y2max - y2Range = y2min, y2max - self._setPlotBounds((xmin, xmax), (ymin, ymax), y2Range) - - def getGraphXLimits(self): - return self._plotFrame.dataRanges.x - - def setGraphXLimits(self, xmin, xmax): - assert xmin < xmax - self._setPlotBounds(xRange=(xmin, xmax), keepDim='x') - - def getGraphYLimits(self, axis): - assert axis in ("left", "right") - if axis == "left": - return self._plotFrame.dataRanges.y - else: - return self._plotFrame.dataRanges.y2 - - def setGraphYLimits(self, ymin, ymax, axis): - assert ymin < ymax - assert axis in ("left", "right") - - if axis == "left": - self._setPlotBounds(yRange=(ymin, ymax), keepDim='y') - else: - self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y') - - # Graph axes - - def getXAxisTimeZone(self): - return self._plotFrame.xAxis.timeZone - - def setXAxisTimeZone(self, tz): - self._plotFrame.xAxis.timeZone = tz - - def isXAxisTimeSeries(self): - return self._plotFrame.xAxis.isTimeSeries - - def setXAxisTimeSeries(self, isTimeSeries): - self._plotFrame.xAxis.isTimeSeries = isTimeSeries - - def setXAxisLogarithmic(self, flag): - if flag != self._plotFrame.xAxis.isLog: - if flag and self._keepDataAspectRatio: - _logger.warning( - "KeepDataAspectRatio is ignored with log axes") - - if flag and not self.isDefaultBaseVectors(): - _logger.warning( - "setXAxisLogarithmic ignored because baseVectors are set") - return - - self._plotFrame.xAxis.isLog = flag - - def setYAxisLogarithmic(self, flag): - if (flag != self._plotFrame.yAxis.isLog or - flag != self._plotFrame.y2Axis.isLog): - if flag and self._keepDataAspectRatio: - _logger.warning( - "KeepDataAspectRatio is ignored with log axes") - - if flag and not self.isDefaultBaseVectors(): - _logger.warning( - "setYAxisLogarithmic ignored because baseVectors are set") - return - - self._plotFrame.yAxis.isLog = flag - self._plotFrame.y2Axis.isLog = flag - - def setYAxisInverted(self, flag): - if flag != self._plotFrame.isYAxisInverted: - self._plotFrame.isYAxisInverted = flag - - def isYAxisInverted(self): - return self._plotFrame.isYAxisInverted - - def isKeepDataAspectRatio(self): - if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog: - return False - else: - return self._keepDataAspectRatio - - def setKeepDataAspectRatio(self, flag): - if flag and (self._plotFrame.xAxis.isLog or - self._plotFrame.yAxis.isLog): - _logger.warning("KeepDataAspectRatio is ignored with log axes") - if flag and not self.isDefaultBaseVectors(): - _logger.warning( - "keepDataAspectRatio ignored because baseVectors are set") - - self._keepDataAspectRatio = flag - - def setGraphGrid(self, which): - assert which in (None, 'major', 'both') - self._plotFrame.grid = which is not None # TODO True grid support - - # Data <-> Pixel coordinates conversion - - def dataToPixel(self, x, y, axis, check=False): - assert axis in ('left', 'right') - - if x is None or y is None: - dataBounds = self._plotContent.getBounds( - self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog) - - if x is None: - x = dataBounds.xAxis.center - - if y is None: - if axis == 'left': - y = dataBounds.yAxis.center - else: - y = dataBounds.y2Axis.center - - result = self._plotFrame.dataToPixel(x, y, axis) - - if check and result is not None: - xPixel, yPixel = result - width, height = self._plotFrame.size - if (xPixel < self._plotFrame.margins.left or - xPixel > (width - self._plotFrame.margins.right) or - yPixel < self._plotFrame.margins.top or - yPixel > height - self._plotFrame.margins.bottom): - return None # (x, y) is out of plot area - - return result - - def pixelToData(self, x, y, axis, check): - assert axis in ("left", "right") - - if x is None: - x = self._plotFrame.size[0] / 2. - if y is None: - y = self._plotFrame.size[1] / 2. - - if check and (x < self._plotFrame.margins.left or - x > (self._plotFrame.size[0] - - self._plotFrame.margins.right) or - y < self._plotFrame.margins.top or - y > (self._plotFrame.size[1] - - self._plotFrame.margins.bottom)): - return None # (x, y) is out of plot area - - return self._plotFrame.pixelToData(x, y, axis) - - def getPlotBoundsInPixels(self): - return self._plotFrame.plotOrigin + self._plotFrame.plotSize - - def setAxesDisplayed(self, displayed): - BackendBase.BackendBase.setAxesDisplayed(self, displayed) - self._plotFrame.displayed = displayed diff --git a/silx/gui/plot/backends/__init__.py b/silx/gui/plot/backends/__init__.py deleted file mode 100644 index 966d9df..0000000 --- a/silx/gui/plot/backends/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017 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 package implements the backend of the Plot.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "21/03/2017" diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py deleted file mode 100644 index 12b6bbe..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ /dev/null @@ -1,1151 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides classes to render 2D lines and scatter plots -""" - -from __future__ import division - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import math -import logging -import warnings - -import numpy - -from silx.math.combo import min_max - -from ...._glutils import gl -from ...._glutils import Program, vertexBuffer -from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate - - -_logger = logging.getLogger(__name__) - - -_MPL_NONES = None, 'None', '', ' ' -"""Possible values for None""" - - -def _notNaNSlices(array, length=1): - """Returns slices of none NaN values in the array. - - :param numpy.ndarray array: 1D array from which to get slices - :param int length: Slices shorter than length gets discarded - :return: Array of (start, end) slice indices - :rtype: numpy.ndarray - """ - isnan = numpy.isnan(numpy.array(array, copy=False).reshape(-1)) - notnan = numpy.logical_not(isnan) - start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1 - if notnan[0]: - start = numpy.append(0, start) - end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1 - if notnan[-1]: - end = numpy.append(end, len(array)) - slices = numpy.transpose((start, end)) - if length > 1: - # discard slices with less than length values - slices = slices[numpy.diff(slices, axis=1).ravel() >= length] - return slices - - -# fill ######################################################################## - -class _Fill2D(object): - """Object rendering curve filling as polygons - - :param numpy.ndarray xData: X coordinates of points - :param numpy.ndarray yData: Y coordinates of points - :param float baseline: Y value of the 'bottom' of the fill. - 0 for linear Y scale, -38 for log Y scale - :param List[float] color: RGBA color as 4 float in [0, 1] - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - _PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - attribute float xPos; - attribute float yPos; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0); - } - """, - fragmentShader=""" - #version 120 - - uniform vec4 color; - - void main(void) { - gl_FragColor = color; - } - """, - attrib0='xPos') - - def __init__(self, xData=None, yData=None, - baseline=0, - color=(0., 0., 0., 1.), - offset=(0., 0.)): - self.xData = xData - self.yData = yData - self._xFillVboData = None - self._yFillVboData = None - self.color = color - self.offset = offset - - # Offset baseline - self.baseline = baseline - self.offset[1] - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if (self._xFillVboData is None and - self.xData is not None and self.yData is not None): - - # Get slices of not NaN values longer than 1 element - isnan = numpy.logical_or(numpy.isnan(self.xData), - numpy.isnan(self.yData)) - notnan = numpy.logical_not(isnan) - start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1 - if notnan[0]: - start = numpy.append(0, start) - end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1 - if notnan[-1]: - end = numpy.append(end, len(isnan)) - slices = numpy.transpose((start, end)) - # discard slices with less than length values - slices = slices[numpy.diff(slices, axis=1).reshape(-1) >= 2] - - # Number of points: slice + 2 * leading and trailing points - # Twice leading and trailing points to produce degenerated triangles - nbPoints = numpy.sum(numpy.diff(slices, axis=1)) + 4 * len(slices) - points = numpy.empty((nbPoints, 2), dtype=numpy.float32) - - offset = 0 - for start, end in slices: - # Duplicate first point for connecting degenerated triangle - points[offset:offset+2] = self.xData[start], self.baseline - - # 2nd point of the polygon is last point - points[offset+2] = self.xData[end-1], self.baseline - - # Add all points from the data - indices = start + buildFillMaskIndices(end - start) - - points[offset+3:offset+3+len(indices), 0] = self.xData[indices] - points[offset+3:offset+3+len(indices), 1] = self.yData[indices] - - # Duplicate last point for connecting degenerated triangle - points[offset+3+len(indices)] = points[offset+3+len(indices)-1] - - offset += len(indices) + 4 - - self._xFillVboData, self._yFillVboData = vertexBuffer(points.T) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - self.prepare() - - if self._xFillVboData is None: - return # Nothing to display - - self._PROGRAM.use() - - gl.glUniformMatrix4fv( - self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE, - numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32)) - - gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color) - - xPosAttrib = self._PROGRAM.attributes['xPos'] - yPosAttrib = self._PROGRAM.attributes['yPos'] - - gl.glEnableVertexAttribArray(xPosAttrib) - self._xFillVboData.setVertexAttrib(xPosAttrib) - - gl.glEnableVertexAttribArray(yPosAttrib) - self._yFillVboData.setVertexAttrib(yPosAttrib) - - # Prepare fill mask - gl.glEnable(gl.GL_STENCIL_TEST) - gl.glStencilMask(1) - gl.glStencilFunc(gl.GL_ALWAYS, 1, 1) - gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT) - gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) - gl.glDepthMask(gl.GL_FALSE) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self._xFillVboData.size) - - gl.glStencilFunc(gl.GL_EQUAL, 1, 1) - # Reset stencil while drawing - gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO) - gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) - gl.glDepthMask(gl.GL_TRUE) - - # Draw directly in NDC - gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE, - mat4Identity().astype(numpy.float32)) - - # NDC vertices - gl.glVertexAttribPointer( - xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, - numpy.array((-1., -1., 1., 1.), dtype=numpy.float32)) - gl.glVertexAttribPointer( - yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, - numpy.array((-1., 1., -1., 1.), dtype=numpy.float32)) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) - - gl.glDisable(gl.GL_STENCIL_TEST) - - def discard(self): - """Release VBOs""" - if self._xFillVboData is not None: - self._xFillVboData.vbo.discard() - - self._xFillVboData = None - self._yFillVboData = None - - -# line ######################################################################## - -SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':' - - -class _Lines2D(object): - """Object rendering curve as a polyline - - :param xVboData: X coordinates VBO - :param yVboData: Y coordinates VBO - :param colorVboData: VBO of colors - :param distVboData: VBO of distance along the polyline - :param str style: Line style in: '-', '--', '-.', ':' - :param List[float] color: RGBA color as 4 float in [0, 1] - :param float width: Line width - :param float dashPeriod: Period of dashes - :param drawMode: OpenGL drawing mode - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - STYLES = SOLID, DASHED, DASHDOT, DOTTED - """Supported line styles""" - - _SOLID_PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.) ; - vColor = color; - } - """, - fragmentShader=""" - #version 120 - - varying vec4 vColor; - - void main(void) { - gl_FragColor = vColor; - } - """, - attrib0='xPos') - - # Limitation: Dash using an estimate of distance in screen coord - # to avoid computing distance when viewport is resized - # results in inequal dashes when viewport aspect ratio is far from 1 - _DASH_PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - uniform vec2 halfViewportSize; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - attribute float distance; - - varying float vDist; - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.); - //Estimate distance in pixels - vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) * - halfViewportSize; - float pixelPerDataEstimate = length(probe)/sqrt(2.); - vDist = distance * pixelPerDataEstimate; - vColor = color; - } - """, - fragmentShader=""" - #version 120 - - /* Dashes: [0, x], [y, z] - Dash period: w */ - uniform vec4 dash; - - varying float vDist; - varying vec4 vColor; - - void main(void) { - float dist = mod(vDist, dash.w); - if ((dist > dash.x && dist < dash.y) || dist > dash.z) { - discard; - } - gl_FragColor = vColor; - } - """, - attrib0='xPos') - - def __init__(self, xVboData=None, yVboData=None, - colorVboData=None, distVboData=None, - style=SOLID, color=(0., 0., 0., 1.), - width=1, dashPeriod=20, drawMode=None, - offset=(0., 0.)): - self.xVboData = xVboData - self.yVboData = yVboData - self.distVboData = distVboData - self.colorVboData = colorVboData - self.useColorVboData = colorVboData is not None - - self.color = color - self.width = width - self._style = None - self.style = style - self.dashPeriod = dashPeriod - self.offset = offset - - self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP - - @property - def style(self): - """Line style (Union[str,None])""" - return self._style - - @style.setter - def style(self, style): - if style in _MPL_NONES: - self._style = None - else: - assert style in self.STYLES - self._style = style - - @classmethod - def init(cls): - """OpenGL context initialization""" - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - style = self.style - if style is None: - return - - elif style == SOLID: - program = self._SOLID_PROGRAM - program.use() - - else: # DASHED, DASHDOT, DOTTED - program = self._DASH_PROGRAM - program.use() - - x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT) - gl.glUniform2f(program.uniforms['halfViewportSize'], - 0.5 * viewWidth, 0.5 * viewHeight) - - if self.style == DOTTED: - dash = (0.1 * self.dashPeriod, - 0.6 * self.dashPeriod, - 0.7 * self.dashPeriod, - self.dashPeriod) - elif self.style == DASHDOT: - dash = (0.3 * self.dashPeriod, - 0.5 * self.dashPeriod, - 0.6 * self.dashPeriod, - self.dashPeriod) - else: - dash = (0.5 * self.dashPeriod, - self.dashPeriod, - self.dashPeriod, - self.dashPeriod) - - gl.glUniform4f(program.uniforms['dash'], *dash) - - distAttrib = program.attributes['distance'] - gl.glEnableVertexAttribArray(distAttrib) - self.distVboData.setVertexAttrib(distAttrib) - - gl.glEnable(gl.GL_LINE_SMOOTH) - - matrix = numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32) - gl.glUniformMatrix4fv(program.uniforms['matrix'], - 1, gl.GL_TRUE, matrix) - - colorAttrib = program.attributes['color'] - if self.useColorVboData and self.colorVboData is not None: - gl.glEnableVertexAttribArray(colorAttrib) - self.colorVboData.setVertexAttrib(colorAttrib) - else: - gl.glDisableVertexAttribArray(colorAttrib) - gl.glVertexAttrib4f(colorAttrib, *self.color) - - xPosAttrib = program.attributes['xPos'] - gl.glEnableVertexAttribArray(xPosAttrib) - self.xVboData.setVertexAttrib(xPosAttrib) - - yPosAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yPosAttrib) - self.yVboData.setVertexAttrib(yPosAttrib) - - gl.glLineWidth(self.width) - gl.glDrawArrays(self._drawMode, 0, self.xVboData.size) - - gl.glDisable(gl.GL_LINE_SMOOTH) - - -def _distancesFromArrays(xData, yData): - """Returns distances between each points - - :param numpy.ndarray xData: X coordinate of points - :param numpy.ndarray yData: Y coordinate of points - :rtype: numpy.ndarray - """ - deltas = numpy.dstack(( - numpy.ediff1d(xData, to_begin=numpy.float32(0.)), - numpy.ediff1d(yData, to_begin=numpy.float32(0.))))[0] - return numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))) - - -# points ###################################################################### - -DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \ - 'd', 'o', 's', '+', 'x', '.', ',', '*' - -H_LINE, V_LINE = '_', '|' - - -class _Points2D(object): - """Object rendering curve markers - - :param xVboData: X coordinates VBO - :param yVboData: Y coordinates VBO - :param colorVboData: VBO of colors - :param str marker: Kind of symbol to use, see :attr:`MARKERS`. - :param List[float] color: RGBA color as 4 float in [0, 1] - :param float size: Marker size - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK, - H_LINE, V_LINE) - """List of supported markers""" - - _VERTEX_SHADER = """ - #version 120 - - uniform mat4 matrix; - uniform int transform; - uniform float size; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.); - vColor = color; - gl_PointSize = size; - } - """ - - _FRAGMENT_SHADER_SYMBOLS = { - DIAMOND: """ - float alphaSymbol(vec2 coord, float size) { - vec2 centerCoord = abs(coord - vec2(0.5, 0.5)); - float f = centerCoord.x + centerCoord.y; - return clamp(size * (0.5 - f), 0.0, 1.0); - } - """, - CIRCLE: """ - float alphaSymbol(vec2 coord, float size) { - float radius = 0.5; - float r = distance(coord, vec2(0.5, 0.5)); - return clamp(size * (radius - r), 0.0, 1.0); - } - """, - SQUARE: """ - float alphaSymbol(vec2 coord, float size) { - return 1.0; - } - """, - PLUS: """ - float alphaSymbol(vec2 coord, float size) { - vec2 d = abs(size * (coord - vec2(0.5, 0.5))); - if (min(d.x, d.y) < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - X_MARKER: """ - float alphaSymbol(vec2 coord, float size) { - vec2 pos = floor(size * coord) + 0.5; - vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size)); - if (min(d_x.x, d_x.y) <= 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - ASTERISK: """ - float alphaSymbol(vec2 coord, float size) { - /* Combining +, x and circle */ - vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5))); - vec2 pos = floor(size * coord) + 0.5; - vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size)); - if (min(d_plus.x, d_plus.y) < 0.5) { - return 1.0; - } else if (min(d_x.x, d_x.y) <= 0.5) { - float r = distance(coord, vec2(0.5, 0.5)); - return clamp(size * (0.5 - r), 0.0, 1.0); - } else { - return 0.0; - } - } - """, - H_LINE: """ - float alphaSymbol(vec2 coord, float size) { - float dy = abs(size * (coord.y - 0.5)); - if (dy < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - V_LINE: """ - float alphaSymbol(vec2 coord, float size) { - float dx = abs(size * (coord.x - 0.5)); - if (dx < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """ - } - - _FRAGMENT_SHADER_TEMPLATE = """ - #version 120 - - uniform float size; - - varying vec4 vColor; - - %s - - void main(void) { - float alpha = alphaSymbol(gl_PointCoord, size); - if (alpha <= 0.0) { - discard; - } else { - gl_FragColor = vec4(vColor.rgb, alpha * clamp(vColor.a, 0.0, 1.0)); - } - } - """ - - _PROGRAMS = {} - - def __init__(self, xVboData=None, yVboData=None, colorVboData=None, - marker=SQUARE, color=(0., 0., 0., 1.), size=7, - offset=(0., 0.)): - self.color = color - self._marker = None - self.marker = marker - self.size = size - self.offset = offset - - self.xVboData = xVboData - self.yVboData = yVboData - self.colorVboData = colorVboData - self.useColorVboData = colorVboData is not None - - @property - def marker(self): - """Symbol used to display markers (str)""" - return self._marker - - @marker.setter - def marker(self, marker): - if marker in _MPL_NONES: - self._marker = None - else: - assert marker in self.MARKERS - self._marker = marker - - @classmethod - def _getProgram(cls, marker): - """On-demand shader program creation.""" - if marker == PIXEL: - marker = SQUARE - elif marker == POINT: - marker = CIRCLE - - if marker not in cls._PROGRAMS: - cls._PROGRAMS[marker] = Program( - vertexShader=cls._VERTEX_SHADER, - fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE % - cls._FRAGMENT_SHADER_SYMBOLS[marker]), - attrib0='xPos') - - return cls._PROGRAMS[marker] - - @classmethod - def init(cls): - """OpenGL context initialization""" - version = gl.glGetString(gl.GL_VERSION) - majorVersion = int(version[0]) - assert majorVersion >= 2 - gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2 - gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2 - if majorVersion >= 3: # OpenGL 3 - gl.glEnable(gl.GL_PROGRAM_POINT_SIZE) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - if self.marker is None: - return - - program = self._getProgram(self.marker) - program.use() - - matrix = numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32) - gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix) - - if self.marker == PIXEL: - size = 1 - elif self.marker == POINT: - size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point - else: - size = self.size - gl.glUniform1f(program.uniforms['size'], size) - # gl.glPointSize(self.size) - - cAttrib = program.attributes['color'] - if self.useColorVboData and self.colorVboData is not None: - gl.glEnableVertexAttribArray(cAttrib) - self.colorVboData.setVertexAttrib(cAttrib) - else: - gl.glDisableVertexAttribArray(cAttrib) - gl.glVertexAttrib4f(cAttrib, *self.color) - - xAttrib = program.attributes['xPos'] - gl.glEnableVertexAttribArray(xAttrib) - self.xVboData.setVertexAttrib(xAttrib) - - yAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yAttrib) - self.yVboData.setVertexAttrib(yAttrib) - - gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size) - - gl.glUseProgram(0) - - -# error bars ################################################################## - -class _ErrorBars(object): - """Display errors bars. - - This is using its own VBO as opposed to fill/points/lines. - There is no picking on error bars. - - It uses 2 vertices per error bars and uses :class:`_Lines2D` to - render error bars and :class:`_Points2D` to render the ends. - - :param numpy.ndarray xData: X coordinates of the data. - :param numpy.ndarray yData: Y coordinates of the data. - :param xError: The absolute error on the X axis. - :type xError: A float, or a numpy.ndarray of float32. - If it is an array, it can either be a 1D array of - same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for negative errors, - row 1 for positive errors. - :param yError: The absolute error on the Y axis. - :type yError: A float, or a numpy.ndarray of float32. See xError. - :param float xMin: The min X value already computed by GLPlotCurve2D. - :param float yMin: The min Y value already computed by GLPlotCurve2D. - :param List[float] color: RGBA color as 4 float in [0, 1] - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - def __init__(self, xData, yData, xError, yError, - xMin, yMin, - color=(0., 0., 0., 1.), - offset=(0., 0.)): - self._attribs = None - self._xMin, self._yMin = xMin, yMin - self.offset = offset - - if xError is not None or yError is not None: - self._xData = numpy.array( - xData, order='C', dtype=numpy.float32, copy=False) - self._yData = numpy.array( - yData, order='C', dtype=numpy.float32, copy=False) - - # This also works if xError, yError is a float/int - self._xError = numpy.array( - xError, order='C', dtype=numpy.float32, copy=False) - self._yError = numpy.array( - yError, order='C', dtype=numpy.float32, copy=False) - else: - self._xData, self._yData = None, None - self._xError, self._yError = None, None - - self._lines = _Lines2D( - None, None, color=color, drawMode=gl.GL_LINES, offset=offset) - self._xErrPoints = _Points2D( - None, None, color=color, marker=V_LINE, offset=offset) - self._yErrPoints = _Points2D( - None, None, color=color, marker=H_LINE, offset=offset) - - def _buildVertices(self): - """Generates error bars vertices""" - nbLinesPerDataPts = (0 if self._xError is None else 2) + \ - (0 if self._yError is None else 2) - - nbDataPts = len(self._xData) - - # interleave coord+error, coord-error. - # xError vertices first if any, then yError vertices if any. - xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, - dtype=numpy.float32) - yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, - dtype=numpy.float32) - - if self._xError is not None: # errors on the X axis - if len(self._xError.shape) == 2: - xErrorMinus, xErrorPlus = self._xError[0], self._xError[1] - else: - # numpy arrays of len 1 or len(xData) - xErrorMinus, xErrorPlus = self._xError, self._xError - - # Interleave vertices for xError - endXError = 4 * nbDataPts - xCoords[0:endXError-3:4] = self._xData + xErrorPlus - xCoords[1:endXError-2:4] = self._xData - xCoords[2:endXError-1:4] = self._xData - xCoords[3:endXError:4] = self._xData - xErrorMinus - - yCoords[0:endXError-3:4] = self._yData - yCoords[1:endXError-2:4] = self._yData - yCoords[2:endXError-1:4] = self._yData - yCoords[3:endXError:4] = self._yData - - else: - endXError = 0 - - if self._yError is not None: # errors on the Y axis - if len(self._yError.shape) == 2: - yErrorMinus, yErrorPlus = self._yError[0], self._yError[1] - else: - # numpy arrays of len 1 or len(yData) - yErrorMinus, yErrorPlus = self._yError, self._yError - - # Interleave vertices for yError - xCoords[endXError::4] = self._xData - xCoords[endXError+1::4] = self._xData - xCoords[endXError+2::4] = self._xData - xCoords[endXError+3::4] = self._xData - - yCoords[endXError::4] = self._yData + yErrorPlus - yCoords[endXError+1::4] = self._yData - yCoords[endXError+2::4] = self._yData - yCoords[endXError+3::4] = self._yData - yErrorMinus - - return xCoords, yCoords - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if self._xData is None: - return - - if self._attribs is None: - xCoords, yCoords = self._buildVertices() - - xAttrib, yAttrib = vertexBuffer((xCoords, yCoords)) - self._attribs = xAttrib, yAttrib - - self._lines.xVboData = xAttrib - self._lines.yVboData = yAttrib - - # Set xError points using the same VBO as lines - self._xErrPoints.xVboData = xAttrib.copy() - self._xErrPoints.xVboData.size //= 2 - self._xErrPoints.yVboData = yAttrib.copy() - self._xErrPoints.yVboData.size //= 2 - - # Set yError points using the same VBO as lines - self._yErrPoints.xVboData = xAttrib.copy() - self._yErrPoints.xVboData.size //= 2 - self._yErrPoints.xVboData.offset += (xAttrib.itemsize * - xAttrib.size // 2) - self._yErrPoints.yVboData = yAttrib.copy() - self._yErrPoints.yVboData.size //= 2 - self._yErrPoints.yVboData.offset += (yAttrib.itemsize * - yAttrib.size // 2) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - self.prepare() - - if self._attribs is not None: - self._lines.render(matrix) - self._xErrPoints.render(matrix) - self._yErrPoints.render(matrix) - - def discard(self): - """Release VBOs""" - if self._attribs is not None: - self._lines.xVboData, self._lines.yVboData = None, None - self._xErrPoints.xVboData, self._xErrPoints.yVboData = None, None - self._yErrPoints.xVboData, self._yErrPoints.yVboData = None, None - self._attribs[0].vbo.discard() - self._attribs = None - - -# curves ###################################################################### - -def _proxyProperty(*componentsAttributes): - """Create a property to access an attribute of attribute(s). - Useful for composition. - Supports multiple components this way: - getter returns the first found, setter sets all - """ - def getter(self): - for compName, attrName in componentsAttributes: - try: - component = getattr(self, compName) - except AttributeError: - pass - else: - return getattr(component, attrName) - - def setter(self, value): - for compName, attrName in componentsAttributes: - component = getattr(self, compName) - setattr(component, attrName, value) - return property(getter, setter) - - -class GLPlotCurve2D(object): - def __init__(self, xData, yData, colorData=None, - xError=None, yError=None, - lineStyle=SOLID, - lineColor=(0., 0., 0., 1.), - lineWidth=1, - lineDashPeriod=20, - marker=SQUARE, - markerColor=(0., 0., 0., 1.), - markerSize=7, - fillColor=None, - isYLog=False): - - self.colorData = colorData - - # Compute x bounds - if xError is None: - self.xMin, self.xMax = min_max(xData, min_positive=False) - else: - # Takes the error into account - if hasattr(xError, 'shape') and len(xError.shape) == 2: - xErrorMinus, xErrorPlus = xError[0], xError[1] - else: - xErrorMinus, xErrorPlus = xError, xError - self.xMin = numpy.nanmin(xData - xErrorMinus) - self.xMax = numpy.nanmax(xData + xErrorPlus) - - # Compute y bounds - if yError is None: - self.yMin, self.yMax = min_max(yData, min_positive=False) - else: - # Takes the error into account - if hasattr(yError, 'shape') and len(yError.shape) == 2: - yErrorMinus, yErrorPlus = yError[0], yError[1] - else: - yErrorMinus, yErrorPlus = yError, yError - self.yMin = numpy.nanmin(yData - yErrorMinus) - self.yMax = numpy.nanmax(yData + yErrorPlus) - - # Handle data offset - if xData.itemsize > 4 or yData.itemsize > 4: # Use normalization - # offset data, do not offset error as it is relative - self.offset = self.xMin, self.yMin - self.xData = (xData - self.offset[0]).astype(numpy.float32) - self.yData = (yData - self.offset[1]).astype(numpy.float32) - - else: # float32 - self.offset = 0., 0. - self.xData = xData - self.yData = yData - - if fillColor is not None: - # Use different baseline depending of Y log scale - self.fill = _Fill2D(self.xData, self.yData, - baseline=-38 if isYLog else 0, - color=fillColor, - offset=self.offset) - else: - self.fill = None - - self._errorBars = _ErrorBars(self.xData, self.yData, - xError, yError, - self.xMin, self.yMin, - offset=self.offset) - - self.lines = _Lines2D() - self.lines.style = lineStyle - self.lines.color = lineColor - self.lines.width = lineWidth - self.lines.dashPeriod = lineDashPeriod - self.lines.offset = self.offset - - self.points = _Points2D() - self.points.marker = marker - self.points.color = markerColor - self.points.size = markerSize - self.points.offset = self.offset - - xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData')) - - yVboData = _proxyProperty(('lines', 'yVboData'), ('points', 'yVboData')) - - colorVboData = _proxyProperty(('lines', 'colorVboData'), - ('points', 'colorVboData')) - - useColorVboData = _proxyProperty(('lines', 'useColorVboData'), - ('points', 'useColorVboData')) - - distVboData = _proxyProperty(('lines', 'distVboData')) - - lineStyle = _proxyProperty(('lines', 'style')) - - lineColor = _proxyProperty(('lines', 'color')) - - lineWidth = _proxyProperty(('lines', 'width')) - - lineDashPeriod = _proxyProperty(('lines', 'dashPeriod')) - - marker = _proxyProperty(('points', 'marker')) - - markerColor = _proxyProperty(('points', 'color')) - - markerSize = _proxyProperty(('points', 'size')) - - @classmethod - def init(cls): - """OpenGL context initialization""" - _Lines2D.init() - _Points2D.init() - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if self.xVboData is None: - xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None - if self.lineStyle in (DASHED, DASHDOT, DOTTED): - dists = _distancesFromArrays(self.xData, self.yData) - if self.colorData is None: - xAttrib, yAttrib, dAttrib = vertexBuffer( - (self.xData, self.yData, dists)) - else: - xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer( - (self.xData, self.yData, self.colorData, dists)) - elif self.colorData is None: - xAttrib, yAttrib = vertexBuffer((self.xData, self.yData)) - else: - xAttrib, yAttrib, cAttrib = vertexBuffer( - (self.xData, self.yData, self.colorData)) - - self.xVboData = xAttrib - self.yVboData = yAttrib - self.distVboData = dAttrib - - if cAttrib is not None and self.colorData.dtype.kind == 'u': - cAttrib.normalization = True # Normalize uint to [0, 1] - self.colorVboData = cAttrib - self.useColorVboData = cAttrib is not None - - def render(self, matrix, isXLog, isYLog): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - :param bool isXLog: - :param bool isYLog: - """ - self.prepare() - if self.fill is not None: - self.fill.render(matrix) - self._errorBars.render(matrix) - self.lines.render(matrix) - self.points.render(matrix) - - def discard(self): - """Release VBOs""" - if self.xVboData is not None: - self.xVboData.vbo.discard() - - self.xVboData = None - self.yVboData = None - self.colorVboData = None - self.distVboData = None - - self._errorBars.discard() - if self.fill is not None: - self.fill.discard() - - def pick(self, xPickMin, yPickMin, xPickMax, yPickMax): - """Perform picking on the curve according to its rendering. - - The picking area is [xPickMin, xPickMax], [yPickMin, yPickMax]. - - In case a segment between 2 points with indices i, i+1 is picked, - only its lower index end point (i.e., i) is added to the result. - In case an end point with index i is picked it is added to the result, - and the segment [i-1, i] is not tested for picking. - - :return: The indices of the picked data - :rtype: list of int - """ - if (self.marker is None and self.lineStyle is None) or \ - self.xMin > xPickMax or xPickMin > self.xMax or \ - self.yMin > yPickMax or yPickMin > self.yMax: - return None - - # offset picking bounds - xPickMin = xPickMin - self.offset[0] - xPickMax = xPickMax - self.offset[0] - yPickMin = yPickMin - self.offset[1] - yPickMax = yPickMax - self.offset[1] - - if self.lineStyle is not None: - # Using Cohen-Sutherland algorithm for line clipping - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) - codes = ((self.yData > yPickMax) << 3) | \ - ((self.yData < yPickMin) << 2) | \ - ((self.xData > xPickMax) << 1) | \ - (self.xData < xPickMin) - - notNaN = numpy.logical_not(numpy.logical_or( - numpy.isnan(self.xData), numpy.isnan(self.yData))) - - # Add all points that are inside the picking area - indices = numpy.nonzero( - numpy.logical_and(codes == 0, notNaN))[0].tolist() - - # Segment that might cross the area with no end point inside it - segToTestIdx = numpy.nonzero((codes[:-1] != 0) & - (codes[1:] != 0) & - ((codes[:-1] & codes[1:]) == 0))[0] - - TOP, BOTTOM, RIGHT, LEFT = (1 << 3), (1 << 2), (1 << 1), (1 << 0) - - for index in segToTestIdx: - if index not in indices: - x0, y0 = self.xData[index], self.yData[index] - x1, y1 = self.xData[index + 1], self.yData[index + 1] - code1 = codes[index + 1] - - # check for crossing with horizontal bounds - # y0 == y1 is a never event: - # => pt0 and pt1 in same vertical area are not in segToTest - if code1 & TOP: - x = x0 + (x1 - x0) * (yPickMax - y0) / (y1 - y0) - elif code1 & BOTTOM: - x = x0 + (x1 - x0) * (yPickMin - y0) / (y1 - y0) - else: - x = None # No horizontal bounds intersection test - - if x is not None and xPickMin <= x <= xPickMax: - # Intersection - indices.append(index) - - else: - # check for crossing with vertical bounds - # x0 == x1 is a never event (see remark for y) - if code1 & RIGHT: - y = y0 + (y1 - y0) * (xPickMax - x0) / (x1 - x0) - elif code1 & LEFT: - y = y0 + (y1 - y0) * (xPickMin - x0) / (x1 - x0) - else: - y = None # No vertical bounds intersection test - - if y is not None and yPickMin <= y <= yPickMax: - # Intersection - indices.append(index) - - indices.sort() - - else: - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) - indices = numpy.nonzero((self.xData >= xPickMin) & - (self.xData <= xPickMax) & - (self.yData >= yPickMin) & - (self.yData <= yPickMax))[0].tolist() - - return indices diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py deleted file mode 100644 index 4ad1547..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotFrame.py +++ /dev/null @@ -1,1116 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This modules provides the rendering of plot titles, axes and grid. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -# TODO -# keep aspect ratio managed here? -# smarter dirty flag handling? - -import datetime as dt -import math -import weakref -import logging -from collections import namedtuple - -import numpy - -from ...._glutils import gl, Program -from ..._utils import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX -from .GLSupport import mat4Ortho -from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270 -from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10 -from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString -from ..._utils.dtime_ticklayout import timestamp - -_logger = logging.getLogger(__name__) - - -# PlotAxis #################################################################### - -class PlotAxis(object): - """Represents a 1D axis of the plot. - This class is intended to be used with :class:`GLPlotFrame`. - """ - - def __init__(self, plot, - tickLength=(0., 0.), - labelAlign=CENTER, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=CENTER, - titleRotate=0, titleOffset=(0., 0.)): - self._ticks = None - - self._plot = weakref.ref(plot) - - self._isDateTime = False - self._timeZone = None - self._isLog = False - self._dataRange = 1., 100. - self._displayCoords = (0., 0.), (1., 0.) - self._title = '' - - self._tickLength = tickLength - self._labelAlign = labelAlign - self._labelVAlign = labelVAlign - self._titleAlign = titleAlign - self._titleVAlign = titleVAlign - self._titleRotate = titleRotate - self._titleOffset = titleOffset - - @property - def dataRange(self): - """The range of the data represented on the axis as a tuple - of 2 floats: (min, max).""" - return self._dataRange - - @dataRange.setter - def dataRange(self, dataRange): - assert len(dataRange) == 2 - assert dataRange[0] <= dataRange[1] - dataRange = float(dataRange[0]), float(dataRange[1]) - - if dataRange != self._dataRange: - self._dataRange = dataRange - self._dirtyTicks() - - @property - def isLog(self): - """Whether the axis is using a log10 scale or not as a bool.""" - return self._isLog - - @isLog.setter - def isLog(self, isLog): - isLog = bool(isLog) - if isLog != self._isLog: - self._isLog = isLog - self._dirtyTicks() - - @property - def timeZone(self): - """Returnss datetime.tzinfo that is used if this axis plots date times.""" - return self._timeZone - - @timeZone.setter - def timeZone(self, tz): - """Sets dateetime.tzinfo that is used if this axis plots date times.""" - self._timeZone = tz - self._dirtyTicks() - - @property - def isTimeSeries(self): - """Whether the axis is showing floats as datetime objects""" - return self._isDateTime - - @isTimeSeries.setter - def isTimeSeries(self, isTimeSeries): - isTimeSeries = bool(isTimeSeries) - if isTimeSeries != self._isDateTime: - self._isDateTime = isTimeSeries - self._dirtyTicks() - - @property - def displayCoords(self): - """The coordinates of the start and end points of the axis - in display space (i.e., in pixels) as a tuple of 2 tuples of - 2 floats: ((x0, y0), (x1, y1)). - """ - return self._displayCoords - - @displayCoords.setter - def displayCoords(self, displayCoords): - assert len(displayCoords) == 2 - assert len(displayCoords[0]) == 2 - assert len(displayCoords[1]) == 2 - displayCoords = tuple(displayCoords[0]), tuple(displayCoords[1]) - if displayCoords != self._displayCoords: - self._displayCoords = displayCoords - self._dirtyTicks() - - @property - def title(self): - """The text label associated with this axis as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - - plot = self._plot() - if plot is not None: - plot._dirty() - - @property - def ticks(self): - """Ticks as tuples: ((x, y) in display, dataPos, textLabel).""" - if self._ticks is None: - self._ticks = tuple(self._ticksGenerator()) - return self._ticks - - def getVerticesAndLabels(self): - """Create the list of vertices for axis and associated text labels. - - :returns: A tuple: List of 2D line vertices, List of Text2D labels. - """ - vertices = list(self.displayCoords) # Add start and end points - labels = [] - tickLabelsSize = [0., 0.] - - xTickLength, yTickLength = self._tickLength - for (xPixel, yPixel), dataPos, text in self.ticks: - if text is None: - tickScale = 0.5 - else: - tickScale = 1. - - label = Text2D(text=text, - x=xPixel - xTickLength, - y=yPixel - yTickLength, - align=self._labelAlign, - valign=self._labelVAlign) - - width, height = label.size - if width > tickLabelsSize[0]: - tickLabelsSize[0] = width - if height > tickLabelsSize[1]: - tickLabelsSize[1] = height - - labels.append(label) - - vertices.append((xPixel, yPixel)) - vertices.append((xPixel + tickScale * xTickLength, - yPixel + tickScale * yTickLength)) - - (x0, y0), (x1, y1) = self.displayCoords - xAxisCenter = 0.5 * (x0 + x1) - yAxisCenter = 0.5 * (y0 + y1) - - xOffset, yOffset = self._titleOffset - - # Adaptative title positioning: - # tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2) - # xOffset = -tickLabelsSize[0] * xTickLength / tickNorm - # xOffset -= 3 * xTickLength - # yOffset = -tickLabelsSize[1] * yTickLength / tickNorm - # yOffset -= 3 * yTickLength - - axisTitle = Text2D(text=self.title, - x=xAxisCenter + xOffset, - y=yAxisCenter + yOffset, - align=self._titleAlign, - valign=self._titleVAlign, - rotate=self._titleRotate) - labels.append(axisTitle) - - return vertices, labels - - 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() - - @staticmethod - def _frange(start, stop, step): - """range for float (including stop).""" - while start <= stop: - yield start - start += step - - def _ticksGenerator(self): - """Generator of ticks as tuples: - ((x, y) in display, dataPos, textLabel). - """ - dataMin, dataMax = self.dataRange - if self.isLog and dataMin <= 0.: - _logger.warning( - 'Getting ticks while isLog=True and dataRange[0]<=0.') - dataMin = 1. - if dataMax < dataMin: - dataMax = 1. - - if dataMin != dataMax: # data range is not null - (x0, y0), (x1, y1) = self.displayCoords - - if self.isLog: - - if self.isTimeSeries: - _logger.warning("Time series not implemented for log-scale") - - logMin, logMax = math.log10(dataMin), math.log10(dataMax) - tickMin, tickMax, step, _ = niceNumbersForLog10(logMin, logMax) - - xScale = (x1 - x0) / (logMax - logMin) - yScale = (y1 - y0) / (logMax - logMin) - - for logPos in self._frange(tickMin, tickMax, step): - if logMin <= logPos <= logMax: - dataPos = 10 ** logPos - xPixel = x0 + (logPos - logMin) * xScale - yPixel = y0 + (logPos - logMin) * yScale - text = '1e%+03d' % logPos - yield ((xPixel, yPixel), dataPos, text) - - if step == 1: - ticks = list(self._frange(tickMin, tickMax, step))[:-1] - for logPos in ticks: - dataOrigPos = 10 ** logPos - for index in range(2, 10): - dataPos = dataOrigPos * index - if dataMin <= dataPos <= dataMax: - logSubPos = math.log10(dataPos) - xPixel = x0 + (logSubPos - logMin) * xScale - yPixel = y0 + (logSubPos - logMin) * yScale - yield ((xPixel, yPixel), dataPos, None) - - else: - xScale = (x1 - x0) / (dataMax - dataMin) - yScale = (y1 - y0) / (dataMax - dataMin) - - nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) - - # Density of 1.3 label per 92 pixels - # i.e., 1.3 label per inch on a 92 dpi screen - tickDensity = 1.3 / 92 - - if not self.isTimeSeries: - tickMin, tickMax, step, nbFrac = niceNumbersAdaptative( - dataMin, dataMax, nbPixels, tickDensity) - - for dataPos in self._frange(tickMin, tickMax, step): - if dataMin <= dataPos <= dataMax: - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - if nbFrac == 0: - text = '%g' % dataPos - else: - text = ('%.' + str(nbFrac) + 'f') % dataPos - yield ((xPixel, yPixel), dataPos, text) - else: - # Time series - dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) - dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) - - tickDateTimes, spacing, unit = calcTicksAdaptive( - dtMin, dtMax, nbPixels, tickDensity) - - for tickDateTime in tickDateTimes: - if dtMin <= tickDateTime <= dtMax: - - dataPos = timestamp(tickDateTime) - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - fmtStr = bestFormatString(spacing, unit) - text = tickDateTime.strftime(fmtStr) - - yield ((xPixel, yPixel), dataPos, text) - - -# GLPlotFrame ################################################################# - -class GLPlotFrame(object): - """Base class for rendering a 2D frame surrounded by axes.""" - - _TICK_LENGTH_IN_PIXELS = 5 - _LINE_WIDTH = 1 - - _SHADERS = { - 'vertex': """ - attribute vec2 position; - uniform mat4 matrix; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragment': """ - uniform vec4 color; - uniform float tickFactor; /* = 1./tickLength or 0. for solid line */ - - void main(void) { - if (mod(tickFactor * (gl_FragCoord.x + gl_FragCoord.y), 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - } - - _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom')) - - # Margins used when plot frame is not displayed - _NoDisplayMargins = _Margins(0, 0, 0, 0) - - def __init__(self, margins): - """ - :param margins: The margins around plot area for axis and labels. - :type margins: dict with 'left', 'right', 'top', 'bottom' keys and - values as ints. - """ - self._renderResources = None - - self._margins = self._Margins(**margins) - - self.axes = [] # List of PlotAxis to be updated by subclasses - - self._grid = False - self._size = 0., 0. - self._title = '' - self._displayed = True - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return self._renderResources is None - - GRID_NONE = 0 - GRID_MAIN_TICKS = 1 - GRID_SUB_TICKS = 2 - GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS) - - @property - def 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 - self._dirty() - - @property - def margins(self): - """Margins in pixels around the plot.""" - if not self.displayed: - return self._NoDisplayMargins - else: - return self._margins - - @property - def grid(self): - """Grid display mode: - - 0: No grid. - - 1: Grid on main ticks. - - 2: Grid on sub-ticks for log scale axes. - - 3: Grid on main and sub ticks.""" - return self._grid - - @grid.setter - def grid(self, grid): - assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS, - self.GRID_SUB_TICKS, self.GRID_ALL_TICKS) - if grid != self._grid: - self._grid = grid - self._dirty() - - @property - def size(self): - """Size in pixels of the plot area including margins.""" - return self._size - - @size.setter - def size(self, size): - assert len(size) == 2 - size = tuple(size) - if size != self._size: - self._size = size - self._dirty() - - @property - def plotOrigin(self): - """Plot area origin (left, top) in widget coordinates in pixels.""" - return self.margins.left, self.margins.top - - @property - def plotSize(self): - """Plot area size (width, height) in pixels.""" - w, h = self.size - w -= self.margins.left + self.margins.right - h -= self.margins.top + self.margins.bottom - return w, h - - @property - def title(self): - """Main title as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirty() - - # In-place update - # if self._renderResources is not None: - # self._renderResources[-1][-1].text = title - - def _dirty(self): - # When Text2D require discard we need to handle it - self._renderResources = None - - def _buildGridVertices(self): - if self._grid == self.GRID_NONE: - return [] - - elif self._grid == self.GRID_MAIN_TICKS: - def test(text): - return text is not None - elif self._grid == self.GRID_SUB_TICKS: - def test(text): - return text is None - elif self._grid == self.GRID_ALL_TICKS: - def test(_): - return True - else: - logging.warning('Wrong grid mode: %d' % self._grid) - return [] - - return self._buildGridVerticesWithTest(test) - - def _buildGridVerticesWithTest(self, test): - """Override in subclass to generate grid vertices""" - return [] - - def _buildVerticesAndLabels(self): - # To fill with copy of axes lists - vertices = [] - labels = [] - - for axis in self.axes: - axisVertices, axisLabels = axis.getVerticesAndLabels() - vertices += axisVertices - labels += axisLabels - - vertices = numpy.array(vertices, dtype=numpy.float32) - - # Add main title - xTitle = (self.size[0] + self.margins.left - - self.margins.right) // 2 - yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS - labels.append(Text2D(text=self.title, - x=xTitle, - y=yTitle, - align=CENTER, - valign=BOTTOM)) - - # grid - gridVertices = numpy.array(self._buildGridVertices(), - dtype=numpy.float32) - - self._renderResources = (vertices, gridVertices, labels) - - _program = Program( - _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position') - - def render(self): - if not self.displayed: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], 0., 0., 0., 1.) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - for label in labels: - label.render(matProj) - - def renderGrid(self): - if self._grid == self.GRID_NONE: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], 0.7, 0.7, 0.7, 1.) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, gridVertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices)) - - -# GLPlotFrame2D ############################################################### - -class GLPlotFrame2D(GLPlotFrame): - def __init__(self, margins): - """ - :param margins: The margins around plot area for axis and labels. - :type margins: dict with 'left', 'right', 'top', 'bottom' keys and - values as ints. - """ - super(GLPlotFrame2D, self).__init__(margins) - self.axes.append(PlotAxis(self, - tickLength=(0., -5.), - labelAlign=CENTER, labelVAlign=TOP, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=0, - titleOffset=(0, self.margins.bottom // 2))) - - self._x2AxisCoords = () - - self.axes.append(PlotAxis(self, - tickLength=(5., 0.), - labelAlign=RIGHT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=BOTTOM, - titleRotate=ROTATE_270, - titleOffset=(-3 * self.margins.left // 4, - 0))) - - self._y2Axis = PlotAxis(self, - tickLength=(-5., 0.), - labelAlign=LEFT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=ROTATE_270, - titleOffset=(3 * self.margins.right // 4, - 0)) - - self._isYAxisInverted = False - - self._dataRanges = { - 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)} - - self._baseVectors = (1., 0.), (0., 1.) - - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - def _dirty(self): - super(GLPlotFrame2D, self)._dirty() - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return (super(GLPlotFrame2D, self).isDirty or - self._transformedDataRanges is None or - self._transformedDataProjMat is None or - self._transformedDataY2ProjMat is None) - - @property - def xAxis(self): - return self.axes[0] - - @property - def yAxis(self): - return self.axes[1] - - @property - def y2Axis(self): - return self._y2Axis - - @property - def isY2Axis(self): - """Whether to display the left Y axis or not.""" - return len(self.axes) == 3 - - @isY2Axis.setter - def isY2Axis(self, isY2Axis): - if isY2Axis != self.isY2Axis: - if isY2Axis: - self.axes.append(self._y2Axis) - else: - self.axes = self.axes[:2] - - self._dirty() - - @property - def isYAxisInverted(self): - """Whether Y axes are inverted or not as a bool.""" - return self._isYAxisInverted - - @isYAxisInverted.setter - def isYAxisInverted(self, value): - value = bool(value) - if value != self._isYAxisInverted: - self._isYAxisInverted = value - self._dirty() - - DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.) - """Values of baseVectors for orthogonal axes.""" - - @property - def baseVectors(self): - """Coordinates of the X and Y axes in the orthogonal plot coords. - - Raises ValueError if corresponding matrix is singular. - - 2 tuples of 2 floats: (xx, xy), (yx, yy) - """ - return self._baseVectors - - @baseVectors.setter - def baseVectors(self, baseVectors): - self._dirty() - - (xx, xy), (yx, yy) = baseVectors - vectors = (float(xx), float(xy)), (float(yx), float(yy)) - - det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]) - if det == 0.: - raise ValueError("Singular matrix for base vectors: " + - str(vectors)) - - if vectors != self._baseVectors: - self._baseVectors = vectors - self._dirty() - - @property - def dataRanges(self): - """Ranges of data visible in the plot on x, y and y2 axes. - - This is different to the axes range when axes are not orthogonal. - - Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - """ - return self._DataRanges(self._dataRanges['x'], - self._dataRanges['y'], - self._dataRanges['y2']) - - @staticmethod - def _clipToSafeRange(min_, max_, isLog): - # Clip range if needed - minLimit = FLOAT32_MINPOS if isLog else FLOAT32_SAFE_MIN - min_ = numpy.clip(min_, minLimit, FLOAT32_SAFE_MAX) - max_ = numpy.clip(max_, minLimit, FLOAT32_SAFE_MAX) - assert min_ < max_ - return min_, max_ - - def setDataRanges(self, x=None, y=None, y2=None): - """Set data range over each axes. - - The provided ranges are clipped to possible values - (i.e., 32 float range + positive range for log scale). - - :param x: (min, max) data range over X axis - :param y: (min, max) data range over Y axis - :param y2: (min, max) data range over Y2 axis - """ - if x is not None: - self._dataRanges['x'] = \ - self._clipToSafeRange(x[0], x[1], self.xAxis.isLog) - - if y is not None: - self._dataRanges['y'] = \ - self._clipToSafeRange(y[0], y[1], self.yAxis.isLog) - - if y2 is not None: - self._dataRanges['y2'] = \ - self._clipToSafeRange(y2[0], y2[1], self.y2Axis.isLog) - - self.xAxis.dataRange = self._dataRanges['x'] - self.yAxis.dataRange = self._dataRanges['y'] - self.y2Axis.dataRange = self._dataRanges['y2'] - - _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2')) - - @property - def transformedDataRanges(self): - """Bounds of the displayed area in transformed data coordinates - (i.e., log scale applied if any as well as skew) - - 3-tuple of 2-tuple (min, max) for each axis: x, y, y2. - """ - if self._transformedDataRanges is None: - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self.dataRanges - - if self.xAxis.isLog: - try: - xMin = math.log10(xMin) - except ValueError: - _logger.info('xMin: warning log10(%f)', xMin) - xMin = 0. - try: - xMax = math.log10(xMax) - except ValueError: - _logger.info('xMax: warning log10(%f)', xMax) - xMax = 0. - - if self.yAxis.isLog: - try: - yMin = math.log10(yMin) - except ValueError: - _logger.info('yMin: warning log10(%f)', yMin) - yMin = 0. - try: - yMax = math.log10(yMax) - except ValueError: - _logger.info('yMax: warning log10(%f)', yMax) - yMax = 0. - - try: - y2Min = math.log10(y2Min) - except ValueError: - _logger.info('yMin: warning log10(%f)', y2Min) - y2Min = 0. - try: - y2Max = math.log10(y2Max) - except ValueError: - _logger.info('yMax: warning log10(%f)', y2Max) - y2Max = 0. - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - corners = [(xMin, yMin), (xMin, yMax), - (xMax, yMin), (xMax, yMax), - (xMin, y2Min), (xMin, y2Max), - (xMax, y2Min), (xMax, y2Max)] - - corners = numpy.array( - [numpy.dot(skew_mat, corner) for corner in corners], - dtype=numpy.float32) - xMin, xMax = corners[:, 0].min(), corners[:, 0].max() - yMin, yMax = corners[0:4, 1].min(), corners[0:4, 1].max() - y2Min, y2Max = corners[4:, 1].min(), corners[4:, 1].max() - - self._transformedDataRanges = self._DataRanges( - (xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - - return self._transformedDataRanges - - @property - def transformedDataProjMat(self): - """Orthographic projection matrix for rendering transformed data - - :type: numpy.matrix - """ - if self._transformedDataProjMat is None: - xMin, xMax = self.transformedDataRanges.x - yMin, yMax = self.transformedDataRanges.y - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1) - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - mat = numpy.dot(mat, numpy.array(( - (xx, yx, 0., 0.), - (xy, yy, 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64)) - - self._transformedDataProjMat = mat - - return self._transformedDataProjMat - - @property - def transformedDataY2ProjMat(self): - """Orthographic projection matrix for rendering transformed data - for the 2nd Y axis - - :type: numpy.matrix - """ - if self._transformedDataY2ProjMat is None: - xMin, xMax = self.transformedDataRanges.x - y2Min, y2Max = self.transformedDataRanges.y2 - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1) - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - mat = numpy.dot(mat, numpy.matrix(( - (xx, yx, 0., 0.), - (xy, yy, 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64)) - - self._transformedDataY2ProjMat = mat - - return self._transformedDataY2ProjMat - - def dataToPixel(self, x, y, axis='left'): - """Convert data coordinate to widget pixel coordinate. - """ - assert axis in ('left', 'right') - - trBounds = self.transformedDataRanges - - if self.xAxis.isLog: - if x < FLOAT32_MINPOS: - return None - xDataTr = math.log10(x) - else: - xDataTr = x - - if self.yAxis.isLog: - if y < FLOAT32_MINPOS: - return None - yDataTr = math.log10(y) - else: - yDataTr = y - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - coords = numpy.dot(skew_mat, numpy.array((xDataTr, yDataTr))) - xDataTr, yDataTr = coords - - plotWidth, plotHeight = self.plotSize - - xPixel = int(self.margins.left + - plotWidth * (xDataTr - trBounds.x[0]) / - (trBounds.x[1] - trBounds.x[0])) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - yOffset = (plotHeight * (yDataTr - usedAxis[0]) / - (usedAxis[1] - usedAxis[0])) - - if self.isYAxisInverted: - yPixel = int(self.margins.top + yOffset) - else: - yPixel = int(self.size[1] - self.margins.bottom - yOffset) - - return xPixel, yPixel - - def pixelToData(self, x, y, axis="left"): - """Convert pixel position to data coordinates. - - :param float x: X coord - :param float y: Y coord - :param str axis: Y axis to use in ('left', 'right') - :return: (x, y) position in data coords - """ - assert axis in ("left", "right") - - plotWidth, plotHeight = self.plotSize - - trBounds = self.transformedDataRanges - - xData = (x - self.margins.left + 0.5) / float(plotWidth) - xData = trBounds.x[0] + xData * (trBounds.x[1] - trBounds.x[0]) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - if self.isYAxisInverted: - yData = (y - self.margins.top + 0.5) / float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - else: - yData = self.size[1] - self.margins.bottom - y - 0.5 - yData /= float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - - # non-orthogonal axis - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - skew_mat = numpy.linalg.inv(skew_mat) - - coords = numpy.dot(skew_mat, numpy.array((xData, yData))) - xData, yData = coords - - if self.xAxis.isLog: - xData = pow(10, xData) - if self.yAxis.isLog: - yData = pow(10, yData) - - return xData, yData - - def _buildGridVerticesWithTest(self, test): - vertices = [] - - if self.baseVectors == self.DEFAULT_BASE_VECTORS: - for axis in self.axes: - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - vertices.append((xPixel, yPixel)) - if axis == self.xAxis: - vertices.append((xPixel, self.margins.top)) - elif axis == self.yAxis: - vertices.append((self.size[0] - self.margins.right, - yPixel)) - else: # axis == self.y2Axis - vertices.append((self.margins.left, yPixel)) - - else: - # Get plot corners in data coords - plotLeft, plotTop = self.plotOrigin - plotWidth, plotHeight = self.plotSize - - corners = [(plotLeft, plotTop), - (plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop)] - - for axis in self.axes: - if axis == self.xAxis: - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ((cornersInData[0], cornersInData[3]), # top - (cornersInData[1], cornersInData[0]), # left - (cornersInData[3], cornersInData[2])) # right - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(x0, x1) <= data < max(x0, x1): - yIntersect = (data - x0) * \ - (y1 - y0) / (x1 - x0) + y0 - - pixelPos = self.dataToPixel( - data, yIntersect) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - else: # y or y2 axes - if axis == self.yAxis: - axis_name = 'left' - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ( - (cornersInData[3], cornersInData[2]), # right - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - else: # axis == self.y2Axis - axis_name = 'right' - corners = numpy.array([self.pixelToData( - x, y, axis='right') for (x, y) in corners]) - borders = ( - (cornersInData[1], cornersInData[0]), # left - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(y0, y1) <= data < max(y0, y1): - xIntersect = (data - y0) * \ - (x1 - x0) / (y1 - y0) + x0 - - pixelPos = self.dataToPixel( - xIntersect, data, axis=axis_name) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - return vertices - - def _buildVerticesAndLabels(self): - width, height = self.size - - xCoords = (self.margins.left - 0.5, - width - self.margins.right + 0.5) - yCoords = (height - self.margins.bottom + 0.5, - self.margins.top - 0.5) - - self.axes[0].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[1], yCoords[0])) - - self._x2AxisCoords = ((xCoords[0], yCoords[1]), - (xCoords[1], yCoords[1])) - - if self.isYAxisInverted: - # Y axes are inverted, axes coordinates are inverted - yCoords = yCoords[1], yCoords[0] - - self.axes[1].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[0], yCoords[1])) - - self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]), - (xCoords[1], yCoords[1])) - - super(GLPlotFrame2D, self)._buildVerticesAndLabels() - - vertices, gridVertices, labels = self._renderResources - - # Adds vertices for borders without axis - extraVertices = [] - extraVertices += self._x2AxisCoords - if not self.isY2Axis: - extraVertices += self._y2Axis.displayCoords - - extraVertices = numpy.array( - extraVertices, copy=False, dtype=numpy.float32) - vertices = numpy.append(vertices, extraVertices, axis=0) - - self._renderResources = (vertices, gridVertices, labels) diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py deleted file mode 100644 index 6f3c487..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotImage.py +++ /dev/null @@ -1,674 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides a class to render 2D array as a colormap or RGB(A) image -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import math -import numpy - -from silx.math.combo import min_max - -from ...._glutils import gl, Program, Texture -from ..._utils import FLOAT32_MINPOS -from .GLSupport import mat4Translate, mat4Scale -from .GLTexture import Image - - -class _GLPlotData2D(object): - def __init__(self, data, origin, scale): - self.data = data - assert len(origin) == 2 - self.origin = tuple(origin) - assert len(scale) == 2 - self.scale = tuple(scale) - - def pick(self, x, y): - if self.xMin <= x <= self.xMax and self.yMin <= y <= self.yMax: - ox, oy = self.origin - sx, sy = self.scale - col = int((x - ox) / sx) - row = int((y - oy) / sy) - return col, row - else: - return None - - @property - def xMin(self): - ox, sx = self.origin[0], self.scale[0] - return ox if sx >= 0. else ox + sx * self.data.shape[1] - - @property - def yMin(self): - oy, sy = self.origin[1], self.scale[1] - return oy if sy >= 0. else oy + sy * self.data.shape[0] - - @property - def xMax(self): - ox, sx = self.origin[0], self.scale[0] - return ox + sx * self.data.shape[1] if sx >= 0. else ox - - @property - def yMax(self): - 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): - - _SHADERS = { - 'linear': { - 'vertex': """ - #version 120 - - uniform mat4 matrix; - attribute vec2 texCoords; - attribute vec2 position; - - varying vec2 coords; - - void main(void) { - coords = texCoords; - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragTransform': """ - vec2 textureCoords(void) { - return coords; - } - """}, - - 'log': { - 'vertex': """ - #version 120 - - attribute vec2 position; - uniform mat4 matrix; - uniform mat4 matOffset; - uniform bvec2 isLog; - - varying vec2 coords; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec4 dataPos = matOffset * vec4(position, 0.0, 1.0); - if (isLog.x) { - dataPos.x = oneOverLog10 * log(dataPos.x); - } - if (isLog.y) { - dataPos.y = oneOverLog10 * log(dataPos.y); - } - coords = dataPos.xy; - gl_Position = matrix * dataPos; - } - """, - 'fragTransform': """ - uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; - - vec2 textureCoords(void) { - vec2 pos = coords; - if (isLog.x) { - pos.x = pow(10., coords.x); - } - if (isLog.y) { - pos.y = pow(10., coords.y); - } - return pos * bounds.oneOverRange - bounds.originOverRange; - // TODO texture coords in range different from [0, 1] - } - """}, - - 'fragment': """ - #version 120 - - uniform sampler2D data; - uniform struct { - sampler2D texture; - bool isLog; - float min; - float oneOverRange; - } cmap; - uniform float alpha; - - varying vec2 coords; - - %s - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - float value = texture2D(data, textureCoords()).r; - if (cmap.isLog) { - if (value > 0.) { - 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.); - } - - gl_FragColor = texture2D(cmap.texture, vec2(value, 0.5)); - gl_FragColor.a *= alpha; - } - """ - } - - _DATA_TEX_UNIT = 0 - _CMAP_TEX_UNIT = 1 - - _INTERNAL_FORMATS = { - numpy.dtype(numpy.float32): gl.GL_R32F, - # Use normalized integer for unsigned int formats - numpy.dtype(numpy.uint16): gl.GL_R16, - numpy.dtype(numpy.uint8): gl.GL_R8, - } - - _linearProgram = Program(_SHADERS['linear']['vertex'], - _SHADERS['fragment'] % - _SHADERS['linear']['fragTransform'], - attrib0='position') - - _logProgram = Program(_SHADERS['log']['vertex'], - _SHADERS['fragment'] % - _SHADERS['log']['fragTransform'], - attrib0='position') - - def __init__(self, data, origin, scale, - colormap, cmapIsLog=False, cmapRange=None, - alpha=1.0): - """Create a 2D colormap - - :param data: The 2D scalar data array to display - :type data: numpy.ndarray with 2 dimensions (dtype=numpy.float32) - :param origin: (x, y) coordinates of the origin of the data array - :type origin: 2-tuple of floats. - :param scale: (sx, sy) scale factors of the data array. - This is the size of a data pixel in plot data space. - :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 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 - :type cmapRange: (float, float) or None - :param float alpha: Opacity from 0 (transparent) to 1 (opaque) - """ - assert data.dtype in self._INTERNAL_FORMATS - - super(GLPlotColormap, self).__init__(data, origin, scale) - self.colormap = numpy.array(colormap, copy=False) - self.cmapIsLog = cmapIsLog - self._cmapRange = (1., 10.) # Colormap range - self.cmapRange = cmapRange # Update _cmapRange - self._alpha = numpy.clip(alpha, 0., 1.) - - self._cmap_texture = None - self._texture = None - self._textureIsDirty = False - - def discard(self): - if self._cmap_texture is not None: - self._cmap_texture.discard() - self._cmap_texture = None - - if self._texture is not None: - self._texture.discard() - self._texture = None - self._textureIsDirty = False - - @property - def cmapRange(self): - if self.cmapIsLog: - assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0. - return self._cmapRange - - @cmapRange.setter - def cmapRange(self, cmapRange): - assert len(cmapRange) == 2 - assert cmapRange[0] <= cmapRange[1] - self._cmapRange = float(cmapRange[0]), float(cmapRange[1]) - - @property - def alpha(self): - return self._alpha - - def updateData(self, data): - assert data.dtype in self._INTERNAL_FORMATS - oldData = self.data - self.data = data - - if self._texture is not None: - if (self.data.shape != oldData.shape or - self.data.dtype != oldData.dtype): - self.discard() - else: - self._textureIsDirty = True - - def prepare(self): - if self._cmap_texture is None: - # TODO share cmap texture accross Images - # put all cmaps in one texture - colormap = numpy.empty((16, 256, self.colormap.shape[1]), - dtype=self.colormap.dtype) - colormap[:] = self.colormap - format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB - self._cmap_texture = Texture(internalFormat=format_, - data=colormap, - format_=format_, - texUnit=self._CMAP_TEX_UNIT, - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)) - - if self._texture is None: - internalFormat = self._INTERNAL_FORMATS[self.data.dtype] - - self._texture = Image(internalFormat, - self.data, - format_=gl.GL_RED, - texUnit=self._DATA_TEX_UNIT) - elif self._textureIsDirty: - self._textureIsDirty = True - self._texture.updateAll(format_=gl.GL_RED, data=self.data) - - def _setCMap(self, prog): - dataMin, dataMax = self.cmapRange # If log, it is stricly positive - - if self.data.dtype in (numpy.uint16, numpy.uint8): - # Using unsigned int as normalized integer in OpenGL - # So normalize range - maxInt = float(numpy.iinfo(self.data.dtype).max) - dataMin, dataMax = dataMin / maxInt, dataMax / maxInt - - if self.cmapIsLog: - dataMin = math.log10(dataMin) - dataMax = math.log10(dataMax) - - 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) - if dataMax > dataMin: - oneOverRange = 1. / (dataMax - dataMin) - else: - oneOverRange = 0. # Fall-back - gl.glUniform1f(prog.uniforms['cmap.oneOverRange'], oneOverRange) - - self._cmap_texture.bind() - - def _renderLinear(self, matrix): - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(matrix, - mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._setCMap(prog) - - self._texture.render(prog.attributes['position'], - prog.attributes['texCoords'], - self._DATA_TEX_UNIT) - - def _renderLog10(self, matrix, isXLog, isYLog): - xMin, yMin = self.xMin, self.yMin - if ((isXLog and xMin < FLOAT32_MINPOS) or - (isYLog and yMin < FLOAT32_MINPOS)): - # Do not render images that are partly or totally <= 0 - return - - self.prepare() - - prog = self._logProgram - prog.use() - - ox, oy = self.origin - - gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog) - - ex = ox + self.scale[0] * self.data.shape[1] - ey = oy + self.scale[1] * self.data.shape[0] - - xOneOverRange = 1. / (ex - ox) - yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], - ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], - xOneOverRange, yOneOverRange) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._setCMap(prog) - - try: - tiles = self._texture.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - if len(tiles) > 1: - raise NotImplementedError( - "Image over multiple textures not supported with log scale") - - texture, vertices, info = tiles[0] - - texture.bind(self._DATA_TEX_UNIT) - - posAttrib = prog.attributes['position'] - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) - - def render(self, matrix, isXLog, isYLog): - if any((isXLog, isYLog)): - self._renderLog10(matrix, isXLog, isYLog) - else: - self._renderLinear(matrix) - - # Unbind colormap texture - gl.glActiveTexture(gl.GL_TEXTURE0 + self._cmap_texture.texUnit) - gl.glBindTexture(self._cmap_texture.target, 0) - - -# image ####################################################################### - -class GLPlotRGBAImage(_GLPlotData2D): - - _SHADERS = { - 'linear': { - 'vertex': """ - #version 120 - - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 coords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - coords = texCoords; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D tex; - uniform float alpha; - - varying vec2 coords; - - void main(void) { - gl_FragColor = texture2D(tex, coords); - gl_FragColor.a *= alpha; - } - """}, - - 'log': { - 'vertex': """ - #version 120 - - attribute vec2 position; - uniform mat4 matrix; - uniform mat4 matOffset; - uniform bvec2 isLog; - - varying vec2 coords; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec4 dataPos = matOffset * vec4(position, 0.0, 1.0); - if (isLog.x) { - dataPos.x = oneOverLog10 * log(dataPos.x); - } - if (isLog.y) { - dataPos.y = oneOverLog10 * log(dataPos.y); - } - coords = dataPos.xy; - gl_Position = matrix * dataPos; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D tex; - uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; - uniform float alpha; - - varying vec2 coords; - - vec2 textureCoords(void) { - vec2 pos = coords; - if (isLog.x) { - pos.x = pow(10., coords.x); - } - if (isLog.y) { - pos.y = pow(10., coords.y); - } - return pos * bounds.oneOverRange - bounds.originOverRange; - // TODO texture coords in range different from [0, 1] - } - - void main(void) { - gl_FragColor = texture2D(tex, textureCoords()); - gl_FragColor.a *= alpha; - } - """} - } - - _DATA_TEX_UNIT = 0 - - _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32), - numpy.dtype(numpy.uint8)) - - _linearProgram = Program(_SHADERS['linear']['vertex'], - _SHADERS['linear']['fragment'], - attrib0='position') - - _logProgram = Program(_SHADERS['log']['vertex'], - _SHADERS['log']['fragment'], - attrib0='position') - - def __init__(self, data, origin, scale, alpha): - """Create a 2D RGB(A) image from data - - :param data: The 2D image data array to display - :type data: numpy.ndarray with 3 dimensions - (dtype=numpy.uint8 or numpy.float32) - :param origin: (x, y) coordinates of the origin of the data array - :type origin: 2-tuple of floats. - :param scale: (sx, sy) scale factors of the data array. - This is the size of a data pixel in plot data space. - :type scale: 2-tuple of floats. - :param float alpha: Opacity from 0 (transparent) to 1 (opaque) - """ - assert data.dtype in self._SUPPORTED_DTYPES - super(GLPlotRGBAImage, self).__init__(data, origin, scale) - self._texture = None - self._textureIsDirty = False - self._alpha = numpy.clip(alpha, 0., 1.) - - @property - def alpha(self): - return self._alpha - - def discard(self): - if self._texture is not None: - self._texture.discard() - self._texture = None - self._textureIsDirty = False - - def updateData(self, data): - assert data.dtype in self._SUPPORTED_DTYPES - oldData = self.data - self.data = data - - if self._texture is not None: - if self.data.shape != oldData.shape: - self.discard() - else: - self._textureIsDirty = True - - def prepare(self): - if self._texture is None: - format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB - - self._texture = Image(format_, - self.data, - format_=format_, - texUnit=self._DATA_TEX_UNIT) - elif self._textureIsDirty: - self._textureIsDirty = False - - # We should check that internal format is the same - 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): - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(matrix, mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._texture.render(prog.attributes['position'], - prog.attributes['texCoords'], - self._DATA_TEX_UNIT) - - def _renderLog(self, matrix, isXLog, isYLog): - self.prepare() - - prog = self._logProgram - prog.use() - - ox, oy = self.origin - - gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - ex = ox + self.scale[0] * self.data.shape[1] - ey = oy + self.scale[1] * self.data.shape[0] - - xOneOverRange = 1. / (ex - ox) - yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], - ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], - xOneOverRange, yOneOverRange) - - try: - tiles = self._texture.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - if len(tiles) > 1: - raise NotImplementedError( - "Image over multiple textures not supported with log scale") - - texture, vertices, info = tiles[0] - - texture.bind(self._DATA_TEX_UNIT) - - posAttrib = prog.attributes['position'] - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) - - def render(self, matrix, isXLog, isYLog): - if any((isXLog, isYLog)): - self._renderLog(matrix, isXLog, isYLog) - else: - self._renderLinear(matrix) diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py deleted file mode 100644 index 18c5eb7..0000000 --- a/silx/gui/plot/backends/glutils/GLSupport.py +++ /dev/null @@ -1,201 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides convenient classes and functions for OpenGL rendering. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import numpy - -from ...._glutils import gl - - -def buildFillMaskIndices(nIndices, dtype=None): - """Returns triangle strip indices for rendering a filled polygon mask - - :param int nIndices: Number of points - :param Union[numpy.dtype,None] dtype: - If specified the dtype of the returned indices array - :return: 1D array of indices constructing a triangle strip - :rtype: numpy.ndarray - """ - if dtype is None: - if nIndices <= numpy.iinfo(numpy.uint16).max + 1: - dtype = numpy.uint16 - else: - dtype = numpy.uint32 - - lastIndex = nIndices - 1 - splitIndex = lastIndex // 2 + 1 - indices = numpy.empty(nIndices, dtype=dtype) - indices[::2] = numpy.arange(0, splitIndex, step=1, dtype=dtype) - indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1, - dtype=dtype) - return indices - - -class Shape2D(object): - _NO_HATCH = 0 - _HATCH_STEP = 20 - - def __init__(self, points, fill='solid', stroke=True, - fillColor=(0., 0., 0., 1.), strokeColor=(0., 0., 0., 1.), - strokeClosed=True): - self.vertices = numpy.array(points, dtype=numpy.float32, copy=False) - self.strokeClosed = strokeClosed - - self._indices = buildFillMaskIndices(len(self.vertices)) - - tVertex = numpy.transpose(self.vertices) - xMin, xMax = min(tVertex[0]), max(tVertex[0]) - yMin, yMax = min(tVertex[1]), max(tVertex[1]) - self.bboxVertices = numpy.array(((xMin, yMin), (xMin, yMax), - (xMax, yMin), (xMax, yMax)), - dtype=numpy.float32) - self._xMin, self._xMax = xMin, xMax - self._yMin, self._yMax = yMin, yMax - - self.fill = fill - self.fillColor = fillColor - self.stroke = stroke - self.strokeColor = strokeColor - - @property - def xMin(self): - return self._xMin - - @property - def xMax(self): - return self._xMax - - @property - def yMin(self): - return self._yMin - - @property - def yMax(self): - return self._yMax - - def prepareFillMask(self, posAttrib): - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.vertices) - - gl.glEnable(gl.GL_STENCIL_TEST) - gl.glStencilMask(1) - gl.glStencilFunc(gl.GL_ALWAYS, 1, 1) - gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT) - gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) - gl.glDepthMask(gl.GL_FALSE) - - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, len(self._indices), - gl.GL_UNSIGNED_SHORT, self._indices) - - gl.glStencilFunc(gl.GL_EQUAL, 1, 1) - # Reset stencil while drawing - gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO) - gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) - gl.glDepthMask(gl.GL_TRUE) - - def renderFill(self, posAttrib): - self.prepareFillMask(posAttrib) - - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.bboxVertices) - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self.bboxVertices)) - - gl.glDisable(gl.GL_STENCIL_TEST) - - def renderStroke(self, posAttrib): - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.vertices) - gl.glLineWidth(1) - drawMode = gl.GL_LINE_LOOP if self.strokeClosed else gl.GL_LINE_STRIP - gl.glDrawArrays(drawMode, 0, len(self.vertices)) - - def render(self, posAttrib, colorUnif, hatchStepUnif): - assert self.fill in ['hatch', 'solid', None] - if self.fill is not None: - gl.glUniform4f(colorUnif, *self.fillColor) - step = self._HATCH_STEP if self.fill == 'hatch' else self._NO_HATCH - gl.glUniform1i(hatchStepUnif, step) - self.renderFill(posAttrib) - - if self.stroke: - gl.glUniform4f(colorUnif, *self.strokeColor) - gl.glUniform1i(hatchStepUnif, self._NO_HATCH) - self.renderStroke(posAttrib) - - -# matrix ###################################################################### - -def mat4Ortho(left, right, bottom, top, near, far): - """Orthographic projection matrix (row-major)""" - return numpy.array(( - (2./(right - left), 0., 0., -(right+left)/float(right-left)), - (0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)), - (0., 0., -2./(far-near), -(far+near)/float(far-near)), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Translate(x=0., y=0., z=0.): - """Translation matrix (row-major)""" - return numpy.array(( - (1., 0., 0., x), - (0., 1., 0., y), - (0., 0., 1., z), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Scale(sx=1., sy=1., sz=1.): - """Scale matrix (row-major)""" - return numpy.array(( - (sx, 0., 0., 0.), - (0., sy, 0., 0.), - (0., 0., sz, 0.), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Identity(): - """Identity matrix""" - return numpy.array(( - (1., 0., 0., 0.), - (0., 1., 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64) diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py deleted file mode 100644 index 3d262bc..0000000 --- a/silx/gui/plot/backends/glutils/GLText.py +++ /dev/null @@ -1,270 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides minimalistic text support for OpenGL. -It provides Latin-1 (ISO8859-1) characters for one monospace font at one size. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -from collections import OrderedDict -import numpy - -from ...._glutils import font, gl, getGLContext, Program, Texture -from .GLSupport import mat4Translate - - -# TODO: Font should be configurable by the main program: using mpl.rcParams? - - -class _Cache(object): - """LRU (Least Recent Used) cache. - - :param int maxsize: Maximum number of (key, value) pairs in the cache - :param callable callback: - Called when a (key, value) pair is removed from the cache. - It must take 2 arguments: key and value. - """ - - def __init__(self, maxsize=128, callback=None): - self._maxsize = int(maxsize) - self._callback = callback - self._cache = OrderedDict() - - def __contains__(self, item): - return item in self._cache - - def __getitem__(self, key): - if key in self._cache: - # Remove/add key from ordered dict to store last access info - value = self._cache.pop(key) - self._cache[key] = value - return value - else: - raise KeyError - - def __setitem__(self, key, value): - """Add a key, value pair to the cache. - - :param key: The key to set - :param value: The corresponding value - """ - if key not in self._cache and len(self._cache) >= self._maxsize: - removedKey, removedValue = self._cache.popitem(last=False) - if self._callback is not None: - self._callback(removedKey, removedValue) - self._cache[key] = value - - -# Text2D ###################################################################### - -LEFT, CENTER, RIGHT = 'left', 'center', 'right' -TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom' -ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270 - - -class Text2D(object): - - _SHADERS = { - 'vertex': """ - #version 120 - - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 vCoords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - vCoords = texCoords; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D texText; - uniform vec4 color; - uniform vec4 bgColor; - - varying vec2 vCoords; - - void main(void) { - gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r); - } - """ - } - - _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)), - dtype=numpy.float32).ravel() - - _program = Program(_SHADERS['vertex'], - _SHADERS['fragment'], - attrib0='position') - - # Discard texture objects when removed from the cache - _textures = _Cache(callback=lambda key, value: value[0].discard()) - """Cache already created textures""" - - _sizes = _Cache() - """Cache already computed sizes""" - - def __init__(self, text, x=0, y=0, - color=(0., 0., 0., 1.), - bgColor=None, - align=LEFT, valign=BASELINE, - rotate=0): - self._vertices = None - self._text = text - self.x = x - self.y = y - self.color = color - self.bgColor = bgColor - - if align not in (LEFT, CENTER, RIGHT): - raise ValueError( - "Horizontal alignment not supported: {0}".format(align)) - self._align = align - - if valign not in (TOP, CENTER, BASELINE, BOTTOM): - raise ValueError( - "Vertical alignment not supported: {0}".format(valign)) - self._valign = valign - - self._rotate = numpy.radians(rotate) - - def _getTexture(self, text): - key = getGLContext(), text - - if key not in self._textures: - image, offset = font.rasterText(text, - font.getDefaultFontFamily()) - if text not in self._sizes: - self._sizes[text] = image.shape[1], image.shape[0] - - self._textures[key] = ( - 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 self._textures[key] - - @property - def text(self): - return self._text - - @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] - - def getVertices(self, offset, shape): - height, width = shape - - if self._align == LEFT: - xOrig = 0 - elif self._align == RIGHT: - xOrig = - width - else: # CENTER - xOrig = - width // 2 - - if self._valign == BASELINE: - yOrig = - offset - elif self._valign == TOP: - yOrig = 0 - elif self._valign == BOTTOM: - yOrig = - height - else: # CENTER - yOrig = - height // 2 - - vertices = numpy.array(( - (xOrig, yOrig), - (xOrig + width, yOrig), - (xOrig, yOrig + height), - (xOrig + width, yOrig + height)), dtype=numpy.float32) - - cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate) - vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array(( - cos * vertices[:, 0] - sin * vertices[:, 1], - sin * vertices[:, 0] + cos * vertices[:, 1]), - dtype=numpy.float32))) - - return vertices - - def render(self, matrix): - if not self.text: - return - - prog = self._program - prog.use() - - texUnit = 0 - texture, offset = self._getTexture(self.text) - - gl.glUniform1i(prog.uniforms['texText'], texUnit) - - mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y))) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform4f(prog.uniforms['color'], *self.color) - if self.bgColor is not None: - bgColor = self.bgColor - else: - bgColor = self.color[0], self.color[1], self.color[2], 0. - gl.glUniform4f(prog.uniforms['bgColor'], *bgColor) - - vertices = self.getVertices(offset, texture.shape) - - posAttrib = prog.attributes['position'] - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - vertices) - - texAttrib = prog.attributes['texCoords'] - gl.glEnableVertexAttribArray(texAttrib) - gl.glVertexAttribPointer(texAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._TEX_COORDS) - - with texture: - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) diff --git a/silx/gui/plot/backends/glutils/GLTexture.py b/silx/gui/plot/backends/glutils/GLTexture.py deleted file mode 100644 index 25dd9f1..0000000 --- a/silx/gui/plot/backends/glutils/GLTexture.py +++ /dev/null @@ -1,239 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 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 classes wrapping OpenGL texture.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -from ctypes import c_void_p -import logging - -import numpy - -from ...._glutils import gl, Texture, numpyToGLType - - -_logger = logging.getLogger(__name__) - - -def _checkTexture2D(internalFormat, shape, - format_=None, type_=gl.GL_FLOAT, border=0): - """Check if texture size with provided parameters is supported - - :rtype: bool - """ - height, width = shape - gl.glTexImage2D(gl.GL_PROXY_TEXTURE_2D, 0, internalFormat, - width, height, border, - format_ or internalFormat, - type_, c_void_p(0)) - width = gl.glGetTexLevelParameteriv( - gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH) - return bool(width) - - -MIN_TEXTURE_SIZE = 64 - - -def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA, - format_=None, - type_=gl.GL_FLOAT, - border=0): - """Returns a supported size for a corresponding square texture - - :returns: GL_MAX_TEXTURE_SIZE or a smaller supported size (not optimal) - :rtype: int - """ - # Is this useful? - maxTexSize = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE) - while maxTexSize > MIN_TEXTURE_SIZE and \ - not _checkTexture2D(internalFormat, (maxTexSize, maxTexSize), - format_, type_, border): - maxTexSize //= 2 - return max(MIN_TEXTURE_SIZE, maxTexSize) - - -class Image(object): - """Image of any size eventually using multiple textures or larger texture - """ - - _WRAP = (gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE) - _MIN_FILTER = gl.GL_NEAREST - _MAG_FILTER = gl.GL_NEAREST - - def __init__(self, internalFormat, data, format_=None, texUnit=0): - self.internalFormat = internalFormat - self.height, self.width = data.shape[0:2] - type_ = numpyToGLType(data.dtype) - - if _checkTexture2D(internalFormat, data.shape[0:2], format_, type_): - texture = Texture(internalFormat, - data, - format_, - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - vertices = numpy.array(( - (0., 0., 0., 0.), - (self.width, 0., 1., 0.), - (0., self.height, 0., 1.), - (self.width, self.height, 1., 1.)), dtype=numpy.float32) - self.tiles = ((texture, vertices, - {'xOrigData': 0, 'yOrigData': 0, - 'wData': self.width, 'hData': self.height}),) - - else: - # Handle dimension too large: make tiles - maxTexSize = _getMaxSquareTexture2DSize(internalFormat, - format_, type_) - - nCols = (self.width+maxTexSize-1) // maxTexSize - colWidths = [self.width // nCols] * nCols - colWidths[-1] += self.width % nCols - - nRows = (self.height+maxTexSize-1) // maxTexSize - rowHeights = [self.height//nRows] * nRows - rowHeights[-1] += self.height % nRows - - tiles = [] - yOrig = 0 - for hData in rowHeights: - xOrig = 0 - for wData in colWidths: - if (hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE) \ - and not _checkTexture2D(internalFormat, - (hData, wData), - format_, - type_): - # Ensure texture size is at least MIN_TEXTURE_SIZE - tH = max(hData, MIN_TEXTURE_SIZE) - tW = max(wData, MIN_TEXTURE_SIZE) - - uMax, vMax = float(wData)/tW, float(hData)/tH - - # TODO issue with type_ and alignment - texture = Texture(internalFormat, - data=None, - format_=format_, - shape=(tH, tW), - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - # TODO handle unpack - texture.update(format_, - data[yOrig:yOrig+hData, - xOrig:xOrig+wData]) - # texture.update(format_, type_, data, - # width=wData, height=hData, - # unpackRowLength=width, - # unpackSkipPixels=xOrig, - # unpackSkipRows=yOrig) - else: - uMax, vMax = 1, 1 - # TODO issue with type_ and unpacking tiles - # TODO idea to handle unpack: use array strides - # As it is now, it will make a copy - texture = Texture(internalFormat, - data[yOrig:yOrig+hData, - xOrig:xOrig+wData], - format_, - shape=(hData, wData), - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - # TODO - # unpackRowLength=width, - # unpackSkipPixels=xOrig, - # unpackSkipRows=yOrig) - vertices = numpy.array(( - (xOrig, yOrig, 0., 0.), - (xOrig + wData, yOrig, uMax, 0.), - (xOrig, yOrig + hData, 0., vMax), - (xOrig + wData, yOrig + hData, uMax, vMax)), - dtype=numpy.float32) - tiles.append((texture, vertices, - {'xOrigData': xOrig, 'yOrigData': yOrig, - 'wData': wData, 'hData': hData})) - xOrig += wData - yOrig += hData - self.tiles = tuple(tiles) - - def discard(self): - for texture, vertices, _ in self.tiles: - texture.discard() - del self.tiles - - def updateAll(self, format_, data, texUnit=0): - if not hasattr(self, 'tiles'): - raise RuntimeError("No texture, discard has already been called") - - assert data.shape[:2] == (self.height, self.width) - if len(self.tiles) == 1: - self.tiles[0][0].update(format_, data, texUnit=texUnit) - else: - for texture, _, info in self.tiles: - yOrig, xOrig = info['yOrigData'], info['xOrigData'] - height, width = info['hData'], info['wData'] - texture.update(format_, - data[yOrig:yOrig+height, xOrig:xOrig+width], - texUnit=texUnit) - # TODO check - # width=info['wData'], height=info['hData'], - # texUnit=texUnit, unpackAlign=unpackAlign, - # unpackRowLength=self.width, - # unpackSkipPixels=info['xOrigData'], - # unpackSkipRows=info['yOrigData']) - - def render(self, posAttrib, texAttrib, texUnit=0): - try: - tiles = self.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - - for texture, vertices, _ in tiles: - texture.bind(texUnit) - - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - texCoordsPtr = c_void_p(vertices.ctypes.data + - 2 * vertices.itemsize) - gl.glEnableVertexAttribArray(texAttrib) - gl.glVertexAttribPointer(texAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, texCoordsPtr) - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py deleted file mode 100644 index 83c7ae0..0000000 --- a/silx/gui/plot/backends/glutils/PlotImageFile.py +++ /dev/null @@ -1,153 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 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. -# -# ############################################################################*/ -"""Function to save an image to a file.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import base64 -import struct -import sys -import zlib - - -# Image writer ################################################################ - -def convertRGBDataToPNG(data): - """Convert a RGB bitmap to PNG. - - It only supports RGB bitmap with one byte per channel stored as a 3D array. - See `Definitive Guide <http://www.libpng.org/pub/png/book/>`_ and - `Specification <http://www.libpng.org/pub/png/spec/1.2/>`_ for details. - - :param data: A 3D array (h, w, rgb) storing an RGB image - :type data: numpy.ndarray of unsigned bytes - :returns: The PNG encoded data - :rtype: bytes - """ - height, width = data.shape[0], data.shape[1] - depth = 8 # 8 bit per channel - colorType = 2 # 'truecolor' = RGB - interlace = 0 # No - - IHDRdata = struct.pack(">ccccIIBBBBB", b'I', b'H', b'D', b'R', - width, height, depth, colorType, - 0, 0, interlace) - - # Add filter 'None' before each scanline - preparedData = b'\x00' + b'\x00'.join(line.tostring() for line in data) - compressedData = zlib.compress(preparedData, 8) - - IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T') - IDATdata += compressedData - - return b''.join([ - b'\x89PNG\r\n\x1a\n', # PNG signature - # IHDR chunk: Image Header - struct.pack(">I", 13), # length - IHDRdata, - struct.pack(">I", zlib.crc32(IHDRdata) & 0xffffffff), # CRC - # IDAT chunk: Payload - struct.pack(">I", len(compressedData)), - IDATdata, - struct.pack(">I", zlib.crc32(IDATdata) & 0xffffffff), # CRC - b'\x00\x00\x00\x00IEND\xaeB`\x82' # IEND chunk: footer - ]) - - -def saveImageToFile(data, fileNameOrObj, fileFormat): - """Save a RGB image to a file. - - :param data: A 3D array (h, w, 3) storing an RGB image. - :type data: numpy.ndarray with of unsigned bytes. - :param fileNameOrObj: Filename or object to use to write the image. - :type fileNameOrObj: A str or a 'file-like' object with a 'write' method. - :param str fileFormat: The type of the file in: 'png', 'ppm', 'svg', 'tiff'. - """ - assert len(data.shape) == 3 - assert data.shape[2] == 3 - assert fileFormat in ('png', 'ppm', 'svg', 'tiff') - - if not hasattr(fileNameOrObj, 'write'): - if sys.version_info < (3, ): - fileObj = open(fileNameOrObj, "wb") - else: - if fileFormat in ('png', 'ppm', 'tiff'): - # Open in binary mode - fileObj = open(fileNameOrObj, 'wb') - else: - fileObj = open(fileNameOrObj, 'w', newline='') - else: # Use as a file-like object - fileObj = fileNameOrObj - - if fileFormat == 'svg': - height, width = data.shape[:2] - base64Data = base64.b64encode(convertRGBDataToPNG(data)) - - fileObj.write( - '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n') - fileObj.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n') - fileObj.write( - ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n') - fileObj.write('<svg xmlns:xlink="http://www.w3.org/1999/xlink"\n') - fileObj.write(' xmlns="http://www.w3.org/2000/svg"\n') - fileObj.write(' version="1.1"\n') - fileObj.write(' width="%d"\n' % width) - fileObj.write(' height="%d">\n' % height) - fileObj.write(' <image xlink:href="data:image/png;base64,') - fileObj.write(base64Data.decode('ascii')) - fileObj.write('"\n') - fileObj.write(' x="0"\n') - fileObj.write(' y="0"\n') - fileObj.write(' width="%d"\n' % width) - fileObj.write(' height="%d"\n' % height) - fileObj.write(' id="image" />\n') - fileObj.write('</svg>') - - elif fileFormat == 'ppm': - height, width = data.shape[:2] - - fileObj.write(b'P6\n') - fileObj.write(b'%d %d\n' % (width, height)) - fileObj.write(b'255\n') - fileObj.write(data.tostring()) - - elif fileFormat == 'png': - fileObj.write(convertRGBDataToPNG(data)) - - elif fileFormat == 'tiff': - if fileObj == fileNameOrObj: - raise NotImplementedError( - 'Save TIFF to a file-like object not implemented') - - from silx.third_party.TiffIO import TiffIO - - tif = TiffIO(fileNameOrObj, mode='wb+') - tif.writeImage(data, info={'Title': 'OpenGL Plot Snapshot'}) - - if fileObj != fileNameOrObj: - fileObj.close() diff --git a/silx/gui/plot/backends/glutils/__init__.py b/silx/gui/plot/backends/glutils/__init__.py deleted file mode 100644 index 771de39..0000000 --- a/silx/gui/plot/backends/glutils/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 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 convenient classes for the OpenGL rendering backend. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import logging - - -_logger = logging.getLogger(__name__) - - -from .GLPlotCurve import * # noqa -from .GLPlotFrame import * # noqa -from .GLPlotImage import * # noqa -from .GLSupport import * # noqa -from .GLText import * # noqa -from .GLTexture import * # noqa |