diff options
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-x | silx/gui/plot/backends/BackendBase.py | 578 | ||||
-rwxr-xr-x | silx/gui/plot/backends/BackendMatplotlib.py | 1544 | ||||
-rwxr-xr-x | silx/gui/plot/backends/BackendOpenGL.py | 1420 | ||||
-rw-r--r-- | silx/gui/plot/backends/__init__.py | 29 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotCurve.py | 1375 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotFrame.py | 1219 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotImage.py | 756 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotItem.py | 99 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotTriangles.py | 197 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLSupport.py | 158 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLText.py | 287 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLTexture.py | 241 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/PlotImageFile.py | 153 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/__init__.py | 46 |
14 files changed, 0 insertions, 8102 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py deleted file mode 100755 index 6fc1aa7..0000000 --- a/silx/gui/plot/backends/BackendBase.py +++ /dev/null @@ -1,578 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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__ = "21/12/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.__xAxisTimeSeries = False - self._xAxisTimeZone = None - # Store a weakref to get access to the plot state. - self._setPlot(plot) - - @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) - - # Add methods - - def addCurve(self, x, y, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, - fill, alpha, symbolsize, baseline): - """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 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 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 object() - - def addImage(self, data, - origin, scale, - colormap, alpha): - """Add an image to the plot. - - :param numpy.ndarray data: (nrows, ncolumns) data or - (nrows, ncolumns, RGBA) ubyte array - :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 ~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 object() - - def addTriangles(self, x, y, triangles, - color, alpha): - """Add a set of triangles. - - :param numpy.ndarray x: The data corresponding to the x axis - :param numpy.ndarray y: The data corresponding to the y axis - :param numpy.ndarray triangles: The indices to make triangles - as a (Ntriangle, 3) array - :param numpy.ndarray color: color(s) as (npoints, 4) array - :param float alpha: Opacity as a float in [0., 1.] - :returns: The triangles' unique identifier used by the backend - """ - return object() - - def addShape(self, x, y, shape, color, fill, overlay, - linestyle, linewidth, linebgcolor): - """Add an item (i.e. a shape) to the plot. - - :param numpy.ndarray x: The X coords of the points of the shape - :param numpy.ndarray y: The Y coords of the points of the shape - :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 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 str linebgcolor: Background color of the line, e.g., 'blue', 'b', - '#FF0000'. It is used to draw dotted line using a second color. - :returns: The handle used by the backend to univocally access the item - """ - return object() - - def addMarker(self, x, y, text, color, - symbol, linestyle, linewidth, constraint, yaxis): - """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 text: Text associated to the marker (or None for no text) - :param str color: Color to be used for instance 'blue', 'b', '#FF0000' - :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. - :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. - :param str yaxis: The Y axis this marker belongs to in: 'left', 'right' - :return: Handle used by the backend to univocally access the marker - """ - return object() - - # 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 getItemsFromBackToFront(self, condition=None): - """Returns the list of plot items order as rendered by the backend. - - This is the order used for rendering. - By default, it takes into account overlays, z value and order of addition of items, - but backends can override it. - - :param callable condition: - Callable taking an item as input and returning False for items to skip. - If None (default), no item is skipped. - :rtype: List[~silx.gui.plot.items.Item] - """ - # Sort items: Overlays first, then others - # and in each category ordered by z and then by order of addition - # as content keeps this order. - content = self._plot.getItems() - if condition is not None: - content = [item for item in content if condition(item)] - - return sorted( - content, - key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue())) - - def pickItem(self, x, y, item): - """Return picked indices if any, or None. - - :param float x: The x pixel coord where to pick. - :param float y: The y pixel coord where to pick. - :param item: A backend item created with add* methods. - :return: None if item was not picked, else returns - picked indices information. - :rtype: Union[None,List] - """ - return None - - # 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 - """ - return self.__xAxisTimeSeries - - def setXAxisTimeSeries(self, isTimeSeries): - """Set whether the X-axis is a time series - - :param bool flag: True to switch to time series, False for regular axis. - """ - self.__xAxisTimeSeries = bool(isTimeSeries) - - 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): - """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'). - :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 setAxesMargins(self, left: float, top: float, right: float, bottom: float): - """Set the size of plot margins as ratios. - - Values are expected in [0., 1.] - - :param float left: - :param float top: - :param float right: - :param float bottom: - """ - pass - - def setForegroundColors(self, foregroundColor, gridColor): - """Set foreground and grid colors used to display this widget. - - :param List[float] foregroundColor: RGBA foreground color of the widget - :param List[float] gridColor: RGBA grid color of the data view - """ - pass - - def setBackgroundColors(self, backgroundColor, dataBackgroundColor): - """Set background colors used to display this widget. - - :param List[float] backgroundColor: RGBA background color of the widget - :param Union[Tuple[float],None] dataBackgroundColor: - RGBA background color of the data view - """ - pass diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py deleted file mode 100755 index 432b0b0..0000000 --- a/silx/gui/plot/backends/BackendMatplotlib.py +++ /dev/null @@ -1,1544 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2021 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__ = "21/12/2018" - - -import logging -import datetime as dt -from typing import Tuple -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 ...utils.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.text import Text -from matplotlib.collections import PathCollection, LineCollection -from matplotlib.ticker import Formatter, ScalarFormatter, Locator -from matplotlib.tri import Triangulation -from matplotlib.collections import TriMesh -from matplotlib import path as mpath - -from . import BackendBase -from .. import items -from .._utils import FLOAT32_MINPOS -from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp - -_PATCH_LINESTYLE = { - "-": 'solid', - "--": 'dashed', - '-.': 'dashdot', - ':': 'dotted', - '': "solid", - None: "solid", -} -"""Patches do not uses the same matplotlib syntax""" - -_MARKER_PATHS = {} -"""Store cached extra marker paths""" - -_SPECIAL_MARKERS = { - 'tickleft': 0, - 'tickright': 1, - 'tickup': 2, - 'tickdown': 3, - 'caretleft': 4, - 'caretright': 5, - 'caretup': 6, - 'caretdown': 7, -} - - -def normalize_linestyle(linestyle): - """Normalize known old-style linestyle, else return the provided value.""" - return _PATCH_LINESTYLE.get(linestyle, linestyle) - -def get_path_from_symbol(symbol): - """Get the path representation of a symbol, else None if - it is not provided. - - :param str symbol: Symbol description used by silx - :rtype: Union[None,matplotlib.path.Path] - """ - if symbol == u'\u2665': - path = _MARKER_PATHS.get(symbol, None) - if path is not None: - return path - vertices = numpy.array([ - [0,-99], - [31,-73], [47,-55], [55,-46], - [63,-37], [94,-2], [94,33], - [94,69], [71,89], [47,89], - [24,89], [8,74], [0,58], - [-8,74], [-24,89], [-47,89], - [-71,89], [-94,69], [-94,33], - [-94,-2], [-63,-37], [-55,-46], - [-47,-55], [-31,-73], [0,-99], - [0,-99]]) - codes = [mpath.Path.CURVE4] * len(vertices) - codes[0] = mpath.Path.MOVETO - codes[-1] = mpath.Path.CLOSEPOLY - path = mpath.Path(vertices, codes) - _MARKER_PATHS[symbol] = path - return path - return None - -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 _PickableContainer(Container): - """Artists container with a :meth:`contains` method""" - - def __init__(self, *args, **kwargs): - Container.__init__(self, *args, **kwargs) - self.__zorder = None - - @property - def axes(self): - """Mimin Artist.axes""" - for child in self.get_children(): - if hasattr(child, 'axes'): - return child.axes - return None - - def draw(self, *args, **kwargs): - """artist-like draw to broadcast draw to children""" - for child in self.get_children(): - child.draw(*args, **kwargs) - - def get_zorder(self): - """Mimic Artist.get_zorder""" - return self.__zorder - - def set_zorder(self, z): - """Mimic Artist.set_zorder to broadcast to children""" - if z != self.__zorder: - self.__zorder = z - for child in self.get_children(): - child.set_zorder(z) - - def contains(self, mouseevent): - """Mimic Artist.contains, and call it on all children. - - :param mouseevent: - :return: Picking status and associated information as a dict - :rtype: (bool,dict) - """ - # Goes through children from front to back and return first picked one. - for child in reversed(self.get_children()): - picked, info = child.contains(mouseevent) - if picked: - return picked, info - return False, {} - - -class _TextWithOffset(Text): - """Text object which can be displayed at a specific position - of the plot, but with a pixel offset""" - - def __init__(self, *args, **kwargs): - Text.__init__(self, *args, **kwargs) - self.pixel_offset = (0, 0) - self.__cache = None - - def draw(self, renderer): - self.__cache = None - return Text.draw(self, renderer) - - def __get_xy(self): - if self.__cache is not None: - return self.__cache - - align = self.get_horizontalalignment() - if align == "left": - xoffset = self.pixel_offset[0] - elif align == "right": - xoffset = -self.pixel_offset[0] - else: - xoffset = 0 - - align = self.get_verticalalignment() - if align == "top": - yoffset = -self.pixel_offset[1] - elif align == "bottom": - yoffset = self.pixel_offset[1] - else: - yoffset = 0 - - trans = self.get_transform() - x = super(_TextWithOffset, self).convert_xunits(self._x) - y = super(_TextWithOffset, self).convert_xunits(self._y) - pos = x, y - - try: - invtrans = trans.inverted() - except numpy.linalg.LinAlgError: - # Cannot inverse transform, fallback: pos without offset - self.__cache = None - return pos - - proj = trans.transform_point(pos) - proj = proj + numpy.array((xoffset, yoffset)) - pos = invtrans.transform_point(proj) - self.__cache = pos - return pos - - def convert_xunits(self, x): - """Return the pixel position of the annotated point.""" - return self.__get_xy()[0] - - def convert_yunits(self, y): - """Return the pixel position of the annotated point.""" - return self.__get_xy()[1] - - -class _MarkerContainer(_PickableContainer): - """Marker artists container supporting draw/remove and text position update - - :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, symbol, x, y, yAxis): - self.line = artists[0] - self.text = artists[1] if len(artists) > 1 else None - self.symbol = symbol - self.x = x - self.y = y - self.yAxis = yAxis - - _PickableContainer.__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, yinverted): - """Update marker text position and visibility according to plot limits - - :param xmin: X axis lower limit - :param xmax: X axis upper limit - :param ymin: Y axis lower limit - :param ymax: Y axis upper limit - :param yinverted: True if the y axis is inverted - """ - if self.text is not None: - visible = ((self.x is None or xmin <= self.x <= xmax) and - (self.y is None or ymin <= self.y <= ymax)) - self.text.set_visible(visible) - - if self.x is not None and self.y is not None: - if self.symbol is None: - valign = 'baseline' - else: - if yinverted: - valign = 'bottom' - else: - valign = 'top' - self.text.set_verticalalignment(valign) - - elif self.y is None: # vertical line - # Always display it on top - center = (ymax + ymin) * 0.5 - pos = (ymax - ymin) * 0.5 * 0.99 - if yinverted: - pos = -pos - self.text.set_y(center + pos) - - elif self.x is None: # Horizontal line - delta = abs(xmax - xmin) - if xmin > xmax: - xmax = xmin - xmax -= 0.005 * delta - self.text.set_x(xmax) - - def contains(self, mouseevent): - """Mimic Artist.contains, and call it on the line Artist. - - :param mouseevent: - :return: Picking status and associated information as a dict - :rtype: (bool,dict) - """ - return self.line.contains(mouseevent) - - -class _DoubleColoredLinePatch(matplotlib.patches.Patch): - """Matplotlib patch to display any patch using double color.""" - - def __init__(self, patch): - super(_DoubleColoredLinePatch, self).__init__() - self.__patch = patch - self.linebgcolor = None - - def __getattr__(self, name): - return getattr(self.__patch, name) - - def draw(self, renderer): - oldLineStype = self.__patch.get_linestyle() - if self.linebgcolor is not None and oldLineStype != "solid": - oldLineColor = self.__patch.get_edgecolor() - oldHatch = self.__patch.get_hatch() - self.__patch.set_linestyle("solid") - self.__patch.set_edgecolor(self.linebgcolor) - self.__patch.set_hatch(None) - self.__patch.draw(renderer) - self.__patch.set_linestyle(oldLineStype) - self.__patch.set_edgecolor(oldLineColor) - self.__patch.set_hatch(oldHatch) - self.__patch.draw(renderer) - - def set_transform(self, transform): - self.__patch.set_transform(transform) - - def get_path(self): - return self.__patch.get_path() - - def contains(self, mouseevent, radius=None): - return self.__patch.contains(mouseevent, radius) - - def contains_point(self, point, radius=None): - return self.__patch.contains_point(point, radius) - - -class Image(AxesImage): - """An AxesImage with a fast path for uint8 RGBA images. - - :param List[float] silx_origin: (ox, oy) Offset of the image. - :param List[float] silx_scale: (sx, sy) Scale of the image. - """ - - def __init__(self, *args, - silx_origin=(0., 0.), - silx_scale=(1., 1.), - **kwargs): - super().__init__(*args, **kwargs) - self.__silx_origin = silx_origin - self.__silx_scale = silx_scale - - def contains(self, mouseevent): - """Overridden to fill 'ind' with row and column""" - inside, info = super().contains(mouseevent) - if inside: - x, y = mouseevent.xdata, mouseevent.ydata - ox, oy = self.__silx_origin - sx, sy = self.__silx_scale - height, width = self.get_size() - column = numpy.clip(int((x - ox) / sx), 0, width - 1) - row = numpy.clip(int((y - oy) / sy), 0, height - 1) - info['ind'] = (row,), (column,) - return inside, info - - def set_data(self, A): - """Overridden to add a fast path for RGBA unit8 images""" - A = numpy.array(A, copy=False) - if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8: - super(Image, self).set_data(A) - else: - # Call AxesImage.set_data with small data to set attributes - super(Image, self).set_data(numpy.zeros((2, 2, 4), dtype=A.dtype)) - self._A = A # Override stored data - - -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") - # Make sure background of Axes is displayed - self.ax2.patch.set_visible(False) - self.ax.patch.set_visible(True) - - # Set axis zorder=0.5 so grid is displayed at 0.5 - self.ax.set_axisbelow(True) - - # disable the use of offsets - try: - axes = [ - self.ax.get_yaxis().get_major_formatter(), - self.ax.get_xaxis().get_major_formatter(), - self.ax2.get_yaxis().get_major_formatter(), - self.ax2.get_xaxis().get_major_formatter(), - ] - for axis in axes: - axis.set_useOffset(False) - axis.set_scientific(False) - except: - _logger.warning('Cannot disabled axes offsets in %s ' - % matplotlib.__version__) - - self.ax2.set_autoscaley_on(True) - - # 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._background = None - - self._colormaps = {} - - self._graphCursor = tuple() - - self._enableAxis('right', False) - self._isXAxisTimeSeries = False - - def getItemsFromBackToFront(self, condition=None): - """Order as BackendBase + take into account matplotlib Axes structure""" - def axesOrder(item): - if item.isOverlay(): - return 2 - elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == 'right': - return 1 - else: - return 0 - - return sorted( - BackendBase.BackendBase.getItemsFromBackToFront( - self, condition=condition), - key=axesOrder) - - def _overlayItems(self): - """Generator of backend renderer for overlay items""" - for item in self._plot.getItems(): - if (item.isOverlay() and - item.isVisible() and - item._backendRenderer is not None): - yield item._backendRenderer - - def _hasOverlays(self): - """Returns whether there is an overlay layer or not. - - The overlay layers contains overlay items and the crosshair. - - :rtype: bool - """ - if self._graphCursor: - return True # There is the crosshair - - for item in self._overlayItems(): - return True # There is at least one overlay item - return False - - # Add methods - - def _getMarkerFromSymbol(self, symbol): - """Returns a marker that can be displayed by matplotlib. - - :param str symbol: A symbol description used by silx - :rtype: Union[str,int,matplotlib.path.Path] - """ - path = get_path_from_symbol(symbol) - if path is not None: - return path - num = _SPECIAL_MARKERS.get(symbol, None) - if num is not None: - return num - # This symbol must be supported by matplotlib - return symbol - - def addCurve(self, x, y, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, - fill, alpha, symbolsize, baseline): - for parameter in (x, y, color, symbol, linewidth, linestyle, - yaxis, 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.float64) / 255. - - if yaxis == "right": - axes = self.ax2 - self._enableAxis("right", True) - else: - axes = self.ax - - pickradius = 3 - - 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 - - # Nx1 error array deprecated in matplotlib >=3.1 (removed in 3.3) - if (isinstance(xerror, numpy.ndarray) and xerror.ndim == 2 and - xerror.shape[1] == 1): - xerror = numpy.ravel(xerror) - if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and - yerror.shape[1] == 1): - yerror = numpy.ravel(yerror) - - errorbars = axes.errorbar(x, y, - 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.float64]: - 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, - linestyle=linestyle, - color=actualColor[0], - linewidth=linewidth, - picker=True, - pickradius=pickradius, - marker=None) - artists += list(curveList) - - marker = self._getMarkerFromSymbol(symbol) - scatter = axes.scatter(x, y, - color=actualColor, - marker=marker, - picker=True, - pickradius=pickradius, - s=symbolsize**2) - artists.append(scatter) - - if fill: - if baseline is None: - _baseline = FLOAT32_MINPOS - else: - _baseline = baseline - artists.append(axes.fill_between( - x, _baseline, y, facecolor=actualColor[0], linestyle='')) - - else: # Curve - curveList = axes.plot(x, y, - linestyle=linestyle, - color=color, - linewidth=linewidth, - marker=symbol, - picker=True, - pickradius=pickradius, - markersize=symbolsize) - artists += list(curveList) - - if fill: - if baseline is None: - _baseline = FLOAT32_MINPOS - else: - _baseline = baseline - artists.append( - axes.fill_between(x, _baseline, y, facecolor=color)) - - for artist in artists: - if alpha < 1: - artist.set_alpha(alpha) - - return _PickableContainer(artists) - - def addImage(self, data, origin, scale, colormap, alpha): - # Non-uniform image - # http://wiki.scipy.org/Cookbook/Histograms - # Non-linear axes - # http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib - for parameter in (data, origin, scale): - 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] - - # All image are shown as RGBA image - image = Image(self.ax, - interpolation='nearest', - picker=True, - origin='lower', - silx_origin=origin, - silx_scale=scale) - - 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 data.ndim == 2: # Data image, convert to RGBA image - data = colormap.applyToData(data) - elif data.dtype == numpy.uint16: - # Normalize uint16 data to have a similar behavior as opengl backend - data = data.astype(numpy.float32) - data /= 65535 - - image.set_data(data) - self.ax.add_artist(image) - return image - - def addTriangles(self, x, y, triangles, color, alpha): - for parameter in (x, y, triangles, color, alpha): - assert parameter is not None - - color = numpy.array(color, copy=False) - assert color.ndim == 2 and len(color) == len(x) - - if color.dtype not in [numpy.float32, numpy.float64]: - color = color.astype(numpy.float32) / 255. - - collection = TriMesh( - Triangulation(x, y, triangles), - alpha=alpha, - pickradius=0) # 0 enables picking on filled triangle - collection.set_color(color) - self.ax.add_collection(collection) - - return collection - - def addShape(self, x, y, shape, color, fill, overlay, - linestyle, linewidth, linebgcolor): - if (linebgcolor is not None and - shape not in ('rectangle', 'polygon', 'polylines')): - _logger.warning( - 'linebgcolor not implemented for %s with matplotlib backend', - shape) - xView = numpy.array(x, copy=False) - yView = numpy.array(y, copy=False) - - linestyle = normalize_linestyle(linestyle) - - if shape == "line": - item = self.ax.plot(x, y, color=color, - linestyle=linestyle, linewidth=linewidth, - marker=None)[0] - - elif shape == "hline": - if hasattr(y, "__len__"): - y = y[-1] - item = self.ax.axhline(y, color=color, - linestyle=linestyle, linewidth=linewidth) - - elif shape == "vline": - if hasattr(x, "__len__"): - x = x[-1] - item = self.ax.axvline(x, color=color, - linestyle=linestyle, linewidth=linewidth) - - 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, - linestyle=linestyle, - linewidth=linewidth) - if fill: - item.set_hatch('.') - - if linestyle != "solid" and linebgcolor is not None: - item = _DoubleColoredLinePatch(item) - item.linebgcolor = linebgcolor - - 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, - color=color, - linestyle=linestyle, - linewidth=linewidth) - if fill and shape == 'polygon': - item.set_hatch('/') - - if linestyle != "solid" and linebgcolor is not None: - item = _DoubleColoredLinePatch(item) - item.linebgcolor = linebgcolor - - self.ax.add_patch(item) - - else: - raise NotImplementedError("Unsupported item shape %s" % shape) - - if overlay: - item.set_animated(True) - - return item - - def addMarker(self, x, y, text, color, - symbol, linestyle, linewidth, constraint, yaxis): - textArtist = None - - xmin, xmax = self.getGraphXLimits() - ymin, ymax = self.getGraphYLimits(axis=yaxis) - - if yaxis == 'left': - ax = self.ax - elif yaxis == 'right': - ax = self.ax2 - else: - assert(False) - - marker = self._getMarkerFromSymbol(symbol) - if x is not None and y is not None: - line = ax.plot(x, y, - linestyle=" ", - color=color, - marker=marker, - markersize=10.)[-1] - - if text is not None: - textArtist = _TextWithOffset(x, y, text, - color=color, - horizontalalignment='left') - if symbol is not None: - textArtist.pixel_offset = 10, 3 - elif x is not None: - line = ax.axvline(x, - color=color, - linewidth=linewidth, - linestyle=linestyle) - if text is not None: - # Y position will be updated in updateMarkerText call - textArtist = _TextWithOffset(x, 1., text, - color=color, - horizontalalignment='left', - verticalalignment='top') - textArtist.pixel_offset = 5, 3 - elif y is not None: - line = ax.axhline(y, - color=color, - linewidth=linewidth, - linestyle=linestyle) - - if text is not None: - # X position will be updated in updateMarkerText call - textArtist = _TextWithOffset(1., y, text, - color=color, - horizontalalignment='right', - verticalalignment='top') - textArtist.pixel_offset = 5, 3 - else: - raise RuntimeError('A marker must at least have one coordinate') - - line.set_picker(True) - line.set_pickradius(5) - - # All markers are overlays - line.set_animated(True) - if textArtist is not None: - ax.add_artist(textArtist) - textArtist.set_animated(True) - - artists = [line] if textArtist is None else [line, textArtist] - container = _MarkerContainer(artists, symbol, x, y, yaxis) - container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted()) - - return container - - def _updateMarkers(self): - xmin, xmax = self.ax.get_xbound() - ymin1, ymax1 = self.ax.get_ybound() - ymin2, ymax2 = self.ax2.get_ybound() - yinverted = self.isYAxisInverted() - for item in self._overlayItems(): - if isinstance(item, _MarkerContainer): - if item.yAxis == 'left': - item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted) - else: - item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted) - - # Remove methods - - def remove(self, 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: - 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 _drawOverlays(self): - """Draw overlays if any.""" - def condition(item): - return (item.isVisible() and - item._backendRenderer is not None and - item.isOverlay()) - - for item in self.getItemsFromBackToFront(condition=condition): - if (isinstance(item, items.YAxisMixIn) and - item.getYAxis() == 'right'): - axes = self.ax2 - else: - axes = self.ax - axes.draw_artist(item._backendRenderer) - - for item in self._graphCursor: - self.ax.draw_artist(item) - - def updateZOrder(self): - """Reorder all items with z order from 0 to 1""" - items = self.getItemsFromBackToFront( - lambda item: item.isVisible() and item._backendRenderer is not None) - count = len(items) - for index, item in enumerate(items): - if item.getZValue() < 0.5: - # Make sure matplotlib z order is below the grid (with z=0.5) - zorder = 0.5 * index / count - else: # Make sure matplotlib z order is above the grid (> 0.5) - zorder = 1. + index / count - if zorder != item._backendRenderer.get_zorder(): - item._backendRenderer.set_zorder(zorder) - - def saveGraph(self, fileName, fileFormat, dpi): - self.updateZOrder() - - # 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.ax.apply_aspect() - self.ax2.apply_aspect() - self._dirtyLimits = False - 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.ax.apply_aspect() - self.ax2.apply_aspect() - self._dirtyLimits = False - - 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() - self._updateMarkers() - - 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 _getDevicePixelRatio(self) -> float: - """Compatibility wrapper for devicePixelRatioF""" - return 1. - - def _mplToQtPosition(self, x: float, y: float) -> Tuple[float, float]: - """Convert matplotlib "display" space coord to Qt widget logical pixel - """ - ratio = self._getDevicePixelRatio() - # Convert from matplotlib origin (bottom) to Qt origin (top) - # and apply device pixel ratio - return x / ratio, (self.fig.get_window_extent().height - y) / ratio - - def _qtToMplPosition(self, x: float, y: float) -> Tuple[float, float]: - """Convert Qt widget logical pixel to matplotlib "display" space coord - """ - ratio = self._getDevicePixelRatio() - # Apply device pixel ration and - # convert from Qt origin (top) to matplotlib origin (bottom) - return x * ratio, self.fig.get_window_extent().height - (y * ratio) - - def dataToPixel(self, x, y, axis): - ax = self.ax2 if axis == "right" else self.ax - displayPos = ax.transData.transform_point((x, y)).transpose() - return self._mplToQtPosition(*displayPos) - - def pixelToData(self, x, y, axis): - ax = self.ax2 if axis == "right" else self.ax - displayPos = self._qtToMplPosition(x, y) - return tuple(ax.transData.inverted().transform_point(displayPos)) - - def getPlotBoundsInPixels(self): - bbox = self.ax.get_window_extent() - # Warning this is not returning int... - ratio = self._getDevicePixelRatio() - return tuple(int(value / ratio) for value in ( - bbox.xmin, - self.fig.get_window_extent().height - bbox.ymax, - bbox.width, - bbox.height)) - - def setAxesMargins(self, left: float, top: float, right: float, bottom: float): - width, height = 1. - left - right, 1. - top - bottom - position = left, bottom, width, height - - # Toggle display of axes and viewbox rect - isFrameOn = position != (0., 0., 1., 1.) - self.ax.set_frame_on(isFrameOn) - self.ax2.set_frame_on(isFrameOn) - - self.ax.set_position(position) - self.ax2.set_position(position) - - self._synchronizeBackgroundColors() - self._synchronizeForegroundColors() - self._plot._setDirtyPlot() - - def _synchronizeBackgroundColors(self): - backgroundColor = self._plot.getBackgroundColor().getRgbF() - - dataBackgroundColor = self._plot.getDataBackgroundColor() - if dataBackgroundColor.isValid(): - dataBackgroundColor = dataBackgroundColor.getRgbF() - else: - dataBackgroundColor = backgroundColor - - if self.ax.get_frame_on(): - self.fig.patch.set_facecolor(backgroundColor) - if self._matplotlibVersion < _parse_version('2'): - self.ax.set_axis_bgcolor(dataBackgroundColor) - else: - self.ax.set_facecolor(dataBackgroundColor) - else: - self.fig.patch.set_facecolor(dataBackgroundColor) - - def _synchronizeForegroundColors(self): - foregroundColor = self._plot.getForegroundColor().getRgbF() - - gridColor = self._plot.getGridColor() - if gridColor.isValid(): - gridColor = gridColor.getRgbF() - else: - gridColor = foregroundColor - - for axes in (self.ax, self.ax2): - if axes.get_frame_on(): - axes.spines['bottom'].set_color(foregroundColor) - axes.spines['top'].set_color(foregroundColor) - axes.spines['right'].set_color(foregroundColor) - axes.spines['left'].set_color(foregroundColor) - axes.tick_params(axis='x', colors=foregroundColor) - axes.tick_params(axis='y', colors=foregroundColor) - axes.yaxis.label.set_color(foregroundColor) - axes.xaxis.label.set_color(foregroundColor) - axes.title.set_color(foregroundColor) - - for line in axes.get_xgridlines(): - line.set_color(gridColor) - - for line in axes.get_ygridlines(): - line.set_color(gridColor) - # axes.grid().set_markeredgecolor(gridColor) - - def setBackgroundColors(self, backgroundColor, dataBackgroundColor): - self._synchronizeBackgroundColors() - - def setForegroundColors(self, foregroundColor, gridColor): - self._synchronizeForegroundColors() - - -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 postRedisplay(self): - self._sigPostRedisplay.emit() - - def _getDevicePixelRatio(self) -> float: - """Compatibility wrapper for devicePixelRatioF""" - if hasattr(self, 'devicePixelRatioF'): - ratio = self.devicePixelRatioF() - else: # Qt < 5.6 compatibility - ratio = float(self.devicePixelRatio()) - # Safety net: avoid returning 0 - return ratio if ratio != 0. else 1. - - # Mouse event forwarding - - _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'} - - def _onMousePress(self, event): - button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None) - if button is not None: - x, y = self._mplToQtPosition(event.x, event.y) - self._plot.onMousePress(int(x), int(y), button) - - def _onMouseMove(self, event): - x, y = self._mplToQtPosition(event.x, event.y) - if self._graphCursor: - position = self._plot.pixelToData( - x, y, axis='left', check=True) - lineh, linev = self._graphCursor - if position is not None: - linev.set_visible(True) - linev.set_xdata((position[0], position[0])) - lineh.set_visible(True) - lineh.set_ydata((position[1], position[1])) - self._plot._setDirtyPlot(overlayOnly=True) - elif lineh.get_visible(): - lineh.set_visible(False) - linev.set_visible(False) - self._plot._setDirtyPlot(overlayOnly=True) - # onMouseMove must trigger replot if dirty flag is raised - - self._plot.onMouseMove(int(x), int(y)) - - def _onMouseRelease(self, event): - button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None) - if button is not None: - x, y = self._mplToQtPosition(event.x, event.y) - self._plot.onMouseRelease(int(x), int(y), button) - - def _onMouseWheel(self, event): - x, y = self._mplToQtPosition(event.x, event.y) - self._plot.onMouseWheel(int(x), int(y), event.step) - - def leaveEvent(self, event): - """QWidget event handler""" - try: - plot = self._plot - except RuntimeError: - pass - else: - plot.onMouseLeaveWidget() - - # picking - - def pickItem(self, x, y, item): - xDisplay, yDisplay = self._qtToMplPosition(x, y) - mouseEvent = MouseEvent( - 'button_press_event', self, int(xDisplay), int(yDisplay)) - # Override axes and data position with the axes - mouseEvent.inaxes = item.axes - mouseEvent.xdata, mouseEvent.ydata = self.pixelToData( - x, y, axis='left' if item.axes is self.ax else 'right') - picked, info = item.contains(mouseEvent) - - if not picked: - return None - - elif isinstance(item, TriMesh): - # Convert selected triangle to data point indices - triangulation = item._triangulation - indices = triangulation.get_masked_triangles()[info['ind'][0]] - - # Sort picked triangle points by distance to mouse - # from furthest to closest to put closest point last - # This is to be somewhat consistent with last scatter point - # being the top one. - xdata, ydata = self.pixelToData(x, y, axis='left') - dists = ((triangulation.x[indices] - xdata) ** 2 + - (triangulation.y[indices] - ydata) ** 2) - return indices[numpy.flip(numpy.argsort(dists), axis=0)] - - else: # Returns indices if any - return info.get('ind', ()) - - # 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._hasOverlays(): - # This is needed with matplotlib 1.5.x and 2.0.x - self._plot._setDirtyPlot() - - 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. - """ - self.updateZOrder() - - # 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._hasOverlays(): - # 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._hasOverlays(): - 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 100755 index 6fde9df..0000000 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ /dev/null @@ -1,1420 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2021 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__ = "21/12/2018" - -import logging -import weakref - -import numpy - -from .. import items -from .._utils import FLOAT32_MINPOS -from . import BackendBase -from ... import colors -from ... import qt - -from ..._glutils import gl -from ... import _glutils as glu -from . import glutils -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 - -# Content ##################################################################### - -class _ShapeItem(dict): - def __init__(self, x, y, shape, color, fill, overlay, - linestyle, linewidth, linebgcolor): - super(_ShapeItem, self).__init__() - - 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)) - - # Ignore fill for polylines to mimic matplotlib - fill = fill if shape != 'polylines' else False - - self.update({ - 'shape': shape, - 'color': colors.rgba(color), - 'fill': 'hatch' if fill else None, - 'x': x, - 'y': y, - 'linestyle': linestyle, - 'linewidth': linewidth, - 'linebgcolor': linebgcolor, - }) - - -class _MarkerItem(dict): - def __init__(self, x, y, text, color, - symbol, linestyle, linewidth, constraint, yaxis): - super(_MarkerItem, self).__init__() - - if symbol is None: - symbol = '+' - - # Apply constraint to provided position - isConstraint = (constraint is not None and - x is not None and y is not None) - if isConstraint: - x, y = constraint(x, y) - - self.update({ - 'x': x, - 'y': y, - 'text': text, - 'color': colors.rgba(color), - 'constraint': constraint if isConstraint else None, - 'symbol': symbol, - 'linestyle': linestyle, - 'linewidth': linewidth, - 'yaxis': yaxis, - }) - - -# 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 ############################################################### - - -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._backgroundColor = 1., 1., 1., 1. - self._dataBackgroundColor = 1., 1., 1., 1. - - self.matScreenProj = glutils.mat4Identity() - - self._progBase = glu.Program( - _baseVertShd, _baseFragShd, attrib0='position') - self._progTex = glu.Program( - _texVertShd, _texFragShd, attrib0='position') - self._plotFBOs = weakref.WeakKeyDictionary() - - self._keepDataAspectRatio = False - - self._crosshairCursor = None - self._mousePosInPixels = None - - self._glGarbageCollector = [] - - self._plotFrame = glutils.GLPlotFrame2D( - foregroundColor=(0., 0., 0., 1.), - gridColor=(.7, .7, .7, 1.), - marginRatios=(.15, .1, .1, .15)) - self._plotFrame.size = ( # Init size with size int - int(self.getDevicePixelRatio() * 640), - int(self.getDevicePixelRatio() * 480)) - - # Make postRedisplay asynchronous using Qt signal - self._sigPostRedisplay.connect( - super(BackendOpenGL, self).postRedisplay, - qt.Qt.QueuedConnection) - - self.setAutoFillBackground(False) - self.setMouseTracking(True) - - # QWidget - - _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'} - - def sizeHint(self): - return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend - - def mousePressEvent(self, event): - if event.button() not in self._MOUSE_BTNS: - return super(BackendOpenGL, self).mousePressEvent(event) - self._plot.onMousePress( - event.x(), event.y(), self._MOUSE_BTNS[event.button()]) - event.accept() - - def mouseMoveEvent(self, event): - qtPos = event.x(), event.y() - - previousMousePosInPixels = self._mousePosInPixels - if qtPos == self._mouseInPlotArea(*qtPos): - devicePixelRatio = self.getDevicePixelRatio() - devicePos = qtPos[0] * devicePixelRatio, qtPos[1] * devicePixelRatio - self._mousePosInPixels = devicePos # Mouse in plot area - else: - self._mousePosInPixels = None # Mouse outside plot area - - if (self._crosshairCursor is not None and - previousMousePosInPixels != self._mousePosInPixels): - # Avoid replot when cursor remains outside plot area - self._plot._setDirtyPlot(overlayOnly=True) - - self._plot.onMouseMove(*qtPos) - event.accept() - - def mouseReleaseEvent(self, event): - if event.button() not in self._MOUSE_BTNS: - return super(BackendOpenGL, self).mouseReleaseEvent(event) - self._plot.onMouseRelease( - event.x(), event.y(), self._MOUSE_BTNS[event.button()]) - event.accept() - - def wheelEvent(self, event): - if hasattr(event, 'angleDelta'): # Qt 5 - delta = event.angleDelta().y() - else: # Qt 4 support - delta = event.delta() - angleInDegrees = delta / 8. - self._plot.onMouseWheel(event.x(), event.y(), angleInDegrees) - event.accept() - - def leaveEvent(self, _): - self._plot.onMouseLeaveWidget() - - # OpenGLWidget API - - def initializeGL(self): - gl.testGL() - - 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._renderOverlayGL() - - def _paintFBOGL(self): - context = glu.Context.getCurrent() - plotFBOTex = self._plotFBOs.get(context) - if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or - plotFBOTex is None): - self._plotVertices = ( - # Vertex coordinates - numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)), - dtype=numpy.float32), - # Texture coordinates - numpy.array(((0., 0.), (1., 0.), (0., 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.glClearColor(*self._backgroundColor) - 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, - glutils.mat4Identity().astype(numpy.float32)) - - gl.glEnableVertexAttribArray(self._progTex.attributes['position']) - gl.glVertexAttribPointer(self._progTex.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._plotVertices[0]) - - gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords']) - gl.glVertexAttribPointer(self._progTex.attributes['texCoords'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._plotVertices[1]) - - with plotFBOTex.texture: - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0])) - - self._renderOverlayGL() - - def paintGL(self): - with glu.Context.current(self.context()): - # Release OpenGL resources - for item in self._glGarbageCollector: - item.discard() - self._glGarbageCollector = [] - - gl.glClearColor(*self._backgroundColor) - gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT) - - # Check if window is large enough - if self._plotFrame.plotSize <= (2, 2): - return - - # Sync plot frame with window - self._plotFrame.devicePixelRatio = self.getDevicePixelRatio() - # self._paintDirectGL() - self._paintFBOGL() - - def _renderItems(self, overlay=False): - """Render items according to :class:`PlotWidget` order - - Note: Scissor test should already be set. - - :param bool overlay: - False (the default) to render item that are not overlays. - True to render items that are overlays. - """ - # Values that are often used - plotWidth, plotHeight = self._plotFrame.plotSize - isXLog = self._plotFrame.xAxis.isLog - isYLog = self._plotFrame.yAxis.isLog - isYInverted = self._plotFrame.isYAxisInverted - - # Used by marker rendering - labels = [] - pixelOffset = 3 - - context = glutils.RenderContext( - isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch()) - - for plotItem in self.getItemsFromBackToFront( - condition=lambda i: i.isVisible() and i.isOverlay() == overlay): - if plotItem._backendRenderer is None: - continue - - item = plotItem._backendRenderer - - if isinstance(item, glutils.GLPlotItem): # Render data items - gl.glViewport(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - # Set matrix - if item.yaxis == 'right': - context.matrix = self._plotFrame.transformedDataY2ProjMat - else: - context.matrix = self._plotFrame.transformedDataProjMat - item.render(context) - - elif isinstance(item, _ShapeItem): # Render shape items - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - 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 - - if item['shape'] == 'hline': - width = self._plotFrame.size[0] - _, yPixel = self._plotFrame.dataToPixel( - 0.5 * sum(self._plotFrame.dataRanges[0]), - item['y'], - axis='left') - subShapes = [numpy.array(((0., yPixel), (width, yPixel)), - dtype=numpy.float32)] - - elif item['shape'] == 'vline': - xPixel, _ = self._plotFrame.dataToPixel( - item['x'], - 0.5 * sum(self._plotFrame.dataRanges[1]), - axis='left') - height = self._plotFrame.size[1] - subShapes = [numpy.array(((xPixel, 0), (xPixel, height)), - dtype=numpy.float32)] - - else: - # Split sub-shapes at not finite values - splits = numpy.nonzero(numpy.logical_not(numpy.logical_and( - numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0] - splits = numpy.concatenate(([-1], splits, [len(item['x'])])) - subShapes = [] - for begin, end in zip(splits[:-1] + 1, splits[1:]): - if end > begin: - subShapes.append(numpy.array([ - self._plotFrame.dataToPixel(x, y, axis='left') - for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])])) - - for points in subShapes: # Draw each sub-shape - # Draw the fill - if (item['fill'] is not None and - item['shape'] not in ('hline', 'vline')): - self._progBase.use() - gl.glUniformMatrix4fv( - self._progBase.uniforms['matrix'], 1, gl.GL_TRUE, - self.matScreenProj.astype(numpy.float32)) - gl.glUniform2i(self._progBase.uniforms['isLog'], False, False) - gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.) - - shape2D = glutils.FilledShape2D( - points, style=item['fill'], color=item['color']) - shape2D.render( - posAttrib=self._progBase.attributes['position'], - colorUnif=self._progBase.uniforms['color'], - hatchStepUnif=self._progBase.uniforms['hatchStep']) - - # Draw the stroke - if item['linestyle'] not in ('', ' ', None): - if item['shape'] != 'polylines': - # close the polyline - points = numpy.append(points, - numpy.atleast_2d(points[0]), axis=0) - - lines = glutils.GLLines2D( - points[:, 0], points[:, 1], - style=item['linestyle'], - color=item['color'], - dash2ndColor=item['linebgcolor'], - width=item['linewidth']) - context.matrix = self.matScreenProj - lines.render(context) - - elif isinstance(item, _MarkerItem): - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - - xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis'] - - if ((isXLog and xCoord is not None and xCoord <= 0) or - (isYLog 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 xCoord is None: # Horizontal line in data space - pixelPos = self._plotFrame.dataToPixel( - 0.5 * sum(self._plotFrame.dataRanges[0]), - yCoord, - axis=yAxis) - - if item['text'] is not None: - x = self._plotFrame.size[0] - \ - self._plotFrame.margins.right - pixelOffset - y = pixelPos[1] - pixelOffset - label = glutils.Text2D( - item['text'], x, y, - color=item['color'], - bgColor=(1., 1., 1., 0.5), - align=glutils.RIGHT, - valign=glutils.BOTTOM, - devicePixelRatio=self.getDevicePixelRatio()) - labels.append(label) - - width = self._plotFrame.size[0] - lines = glutils.GLLines2D( - (0, width), (pixelPos[1], pixelPos[1]), - style=item['linestyle'], - color=item['color'], - width=item['linewidth']) - context.matrix = self.matScreenProj - lines.render(context) - - else: # yCoord is None: vertical line in data space - yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2] - pixelPos = self._plotFrame.dataToPixel( - xCoord, 0.5 * sum(yRange), axis=yAxis) - - if item['text'] is not None: - x = pixelPos[0] + pixelOffset - y = self._plotFrame.margins.top + pixelOffset - label = glutils.Text2D( - item['text'], x, y, - color=item['color'], - bgColor=(1., 1., 1., 0.5), - align=glutils.LEFT, - valign=glutils.TOP, - devicePixelRatio=self.getDevicePixelRatio()) - labels.append(label) - - height = self._plotFrame.size[1] - lines = glutils.GLLines2D( - (pixelPos[0], pixelPos[0]), (0, height), - style=item['linestyle'], - color=item['color'], - width=item['linewidth']) - context.matrix = self.matScreenProj - lines.render(context) - - else: - xmin, xmax = self._plot.getXAxis().getLimits() - ymin, ymax = self._plot.getYAxis(axis=yAxis).getLimits() - if not xmin < xCoord < xmax or not ymin < yCoord < ymax: - # Do not render markers outside visible plot area - continue - pixelPos = self._plotFrame.dataToPixel( - xCoord, yCoord, axis=yAxis) - - if isYInverted: - valign = glutils.BOTTOM - vPixelOffset = -pixelOffset - else: - valign = glutils.TOP - vPixelOffset = pixelOffset - - if item['text'] is not None: - x = pixelPos[0] + pixelOffset - y = pixelPos[1] + vPixelOffset - label = glutils.Text2D( - item['text'], x, y, - color=item['color'], - bgColor=(1., 1., 1., 0.5), - align=glutils.LEFT, - valign=valign, - devicePixelRatio=self.getDevicePixelRatio()) - labels.append(label) - - # For now simple implementation: using a curve for each marker - # Should pack all markers to a single set of points - markerCurve = glutils.GLPlotCurve2D( - numpy.array((pixelPos[0],), dtype=numpy.float64), - numpy.array((pixelPos[1],), dtype=numpy.float64), - marker=item['symbol'], - markerColor=item['color'], - markerSize=11) - - context = glutils.RenderContext( - matrix=self.matScreenProj, - isXLog=False, - isYLog=False, - dpi=self.getDotsPerInch()) - markerCurve.render(context) - markerCurve.discard() - - else: - _logger.error('Unsupported item: %s', str(item)) - continue - - # Render marker labels - gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1]) - for label in labels: - label.render(self.matScreenProj) - - def _renderOverlayGL(self): - """Render overlay layer: overlay items and crosshair.""" - plotWidth, plotHeight = self._plotFrame.plotSize - - # Scissor to plot area - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - self._renderItems(overlay=True) - - # Render crosshair cursor - if self._crosshairCursor is not None and self._mousePosInPixels is not None: - 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'] - - 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): - """Render base layer of plot area. - - It renders the background, grid and items except overlays - """ - plotWidth, plotHeight = self._plotFrame.plotSize - - gl.glScissor(self._plotFrame.margins.left, - self._plotFrame.margins.bottom, - plotWidth, plotHeight) - gl.glEnable(gl.GL_SCISSOR_TEST) - - if self._dataBackgroundColor != self._backgroundColor: - gl.glClearColor(*self._dataBackgroundColor) - gl.glClear(gl.GL_COLOR_BUFFER_BIT) - - self._plotFrame.renderGrid() - - # Matrix - trBounds = self._plotFrame.transformedDataRanges - if trBounds.x[0] != trBounds.x[1] and trBounds.y[0] != trBounds.y[1]: - # Do rendering of items - self._renderItems(overlay=False) - - 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 = glutils.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, - color, symbol, linewidth, linestyle, - yaxis, - xerror, yerror, - fill, alpha, symbolsize, baseline): - for parameter in (x, y, color, symbol, linewidth, linestyle, - yaxis, 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 - with numpy.errstate(divide='ignore', invalid='ignore'): - # Ignore divide by zero, invalid value encountered in log10 - xErrorMinus = logX - numpy.log10(x - xErrorMinus) - xErrorPlus = numpy.log10(x + xErrorPlus) - logX - 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 - with numpy.errstate(divide='ignore', invalid='ignore'): - # Ignore divide by zero, invalid value encountered in log10 - yErrorMinus = logY - numpy.log10(y - yErrorMinus) - yErrorPlus = numpy.log10(y + yErrorPlus) - logY - 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 - - fillColor = None - if fill is True: - fillColor = color - curve = glutils.GLPlotCurve2D( - x, y, colorArray, - xError=xerror, - yError=yerror, - lineStyle=linestyle, - lineColor=color, - lineWidth=linewidth, - marker=symbol, - markerColor=color, - markerSize=symbolsize, - fillColor=fillColor, - baseline=baseline, - isYLog=isYLog) - curve.yaxis = 'left' if yaxis is None else yaxis - - if yaxis == "right": - self._plotFrame.isY2Axis = True - - return curve - - def addImage(self, data, - origin, scale, - colormap, alpha): - for parameter in (data, origin, scale): - assert parameter is not None - - if data.ndim == 2: - # Ensure array is contiguous and eventually convert its type - dtypes = [dtype for dtype in ( - numpy.float32, numpy.float16, numpy.uint8, numpy.uint16) - if glu.isSupportedGLType(dtype)] - if data.dtype in dtypes: - data = numpy.array(data, copy=False, order='C') - else: - _logger.info( - 'addImage: Convert %s data to float32', str(data.dtype)) - data = numpy.array(data, dtype=numpy.float32, order='C') - - normalization = colormap.getNormalization() - if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS: - # Fast path applying colormap on the GPU - cmapRange = colormap.getColormapRange(data=data) - colormapLut = colormap.getNColors(nbColors=256) - gamma = colormap.getGammaNormalizationParameter() - nanColor = colors.rgba(colormap.getNaNColor()) - - image = glutils.GLPlotColormap( - data, - origin, - scale, - colormapLut, - normalization, - gamma, - cmapRange, - alpha, - nanColor) - - else: # Fallback applying colormap on CPU - rgba = colormap.applyToData(data) - image = glutils.GLPlotRGBAImage(rgba, origin, scale, alpha) - - 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 data.dtype in [numpy.uint8, numpy.uint16]: - pass - elif numpy.issubdtype(data.dtype, numpy.integer): - data = numpy.array(data, dtype=numpy.uint8, copy=False) - else: - raise ValueError('Unsupported data type') - - image = glutils.GLPlotRGBAImage(data, origin, scale, alpha) - - else: - raise RuntimeError("Unsupported data shape {0}".format(data.shape)) - - # TODO is this needed? - 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') - - return image - - def addTriangles(self, x, y, triangles, - color, alpha): - # Handle axes log scale: convert data - if self._plotFrame.xAxis.isLog: - x = numpy.log10(x) - if self._plotFrame.yAxis.isLog: - y = numpy.log10(y) - - triangles = glutils.GLPlotTriangles(x, y, color, triangles, alpha) - - return triangles - - def addShape(self, x, y, shape, color, fill, overlay, - linestyle, linewidth, linebgcolor): - x = numpy.array(x, copy=False) - y = numpy.array(y, copy=False) - - # 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') - - return _ShapeItem(x, y, shape, color, fill, overlay, - linestyle, linewidth, linebgcolor) - - def addMarker(self, x, y, text, color, - symbol, linestyle, linewidth, constraint, yaxis): - return _MarkerItem(x, y, text, color, - symbol, linestyle, linewidth, constraint, yaxis) - - # Remove methods - - def remove(self, item): - if isinstance(item, glutils.GLPlotItem): - if item.yaxis == 'right': - # Check if some curves remains on the right Y axis - y2AxisItems = (item for item in self._plot.getItems() - if isinstance(item, items.YAxisMixIn) and - item.getYAxis() == 'right') - self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None - - if item.isInitialized(): - self._glGarbageCollector.append(item) - - elif isinstance(item, (_MarkerItem, _ShapeItem)): - pass # No-op - - else: - _logger.error('Unsupported item: %s', str(item)) - - # 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 != '-': - _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): - """Returns closest visible position in the plot. - - This is performed in Qt widget pixel, not device pixel. - - :param float x: X coordinate in Qt widget pixel - :param float y: Y coordinate in Qt widget pixel - :return: (x, y) closest point in the plot. - :rtype: List[float] - """ - left, top, width, height = self.getPlotBoundsInPixels() - return (numpy.clip(x, left, left + width - 1), # TODO -1? - numpy.clip(y, top, top + height - 1)) - - def __pickCurves(self, item, x, y): - """Perform picking on a curve item. - - :param GLPlotCurve2D item: - :param float x: X position of the mouse in widget coordinates - :param float y: Y position of the mouse in widget coordinates - :return: List of indices of picked points or None if not picked - :rtype: Union[List[int],None] - """ - offset = self._PICK_OFFSET - if item.marker is not None: - # Convert markerSize from points to qt pixels - qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio() - size = item.markerSize / 72. * qtDpi - offset = max(size / 2., offset) - if item.lineStyle is not None: - # Convert line width from points to qt pixels - qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio() - lineWidth = item.lineWidth / 72. * qtDpi - offset = max(lineWidth / 2., offset) - - inAreaPos = self._mouseInPlotArea(x - offset, y - offset) - dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1], - axis=item.yaxis, check=True) - if dataPos is None: - return None - xPick0, yPick0 = dataPos - - inAreaPos = self._mouseInPlotArea(x + offset, y + offset) - dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1], - axis=item.yaxis, check=True) - if dataPos is None: - return None - 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 (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or ( - item.yaxis == 'right' and self._plotFrame.y2Axis.isLog): - yPickMin = numpy.log10(yPickMin) - yPickMax = numpy.log10(yPickMax) - - return item.pick(xPickMin, yPickMin, - xPickMax, yPickMax) - - def pickItem(self, x, y, item): - # Picking is performed in Qt widget pixels not device pixels - dataPos = self._plot.pixelToData(x, y, axis='left', check=True) - if dataPos is None: - return None # Outside plot area - - if item is None: - _logger.error("No item provided for picking") - return None - - # Pick markers - if isinstance(item, _MarkerItem): - yaxis = item['yaxis'] - pixelPos = self._plot.dataToPixel( - item['x'], item['y'], axis=yaxis, check=False) - if pixelPos is None: - return None # negative coord on a log axis - - if item['x'] is None: # Horizontal line - pt1 = self._plot.pixelToData( - x, y - self._PICK_OFFSET, axis=yaxis, check=False) - pt2 = self._plot.pixelToData( - x, y + self._PICK_OFFSET, axis=yaxis, check=False) - isPicked = (min(pt1[1], pt2[1]) <= item['y'] <= - max(pt1[1], pt2[1])) - - elif item['y'] is None: # Vertical line - pt1 = self._plot.pixelToData( - x - self._PICK_OFFSET, y, axis=yaxis, check=False) - pt2 = self._plot.pixelToData( - x + self._PICK_OFFSET, y, axis=yaxis, check=False) - isPicked = (min(pt1[0], pt2[0]) <= item['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) - - return (0,) if isPicked else None - - # Pick image, curve, triangles - elif isinstance(item, glutils.GLPlotItem): - if isinstance(item, glutils.GLPlotCurve2D): - return self.__pickCurves(item, x, y) - else: - return item.pick(*dataPos) # Might be None - - # 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 - self._plotFrame.y2Axis.title = label - - # 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) - - 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._plotFrame.plotSize - if plotWidth <= 2 or plotHeight <= 2: - return - - if keepDim is None: - ranges = self._plot.getDataRange() - if (ranges.y is not None and - ranges.x is not None and - (ranges.y[1] - ranges.y[0]) != 0.): - dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0]) - 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") - - 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") - - 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") - - 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): - result = self._plotFrame.dataToPixel(x, y, axis) - if result is None: - return None - else: - devicePixelRatio = self.getDevicePixelRatio() - return tuple(value/devicePixelRatio for value in result) - - def pixelToData(self, x, y, axis): - devicePixelRatio = self.getDevicePixelRatio() - return self._plotFrame.pixelToData( - x * devicePixelRatio, y * devicePixelRatio, axis) - - def getPlotBoundsInPixels(self): - devicePixelRatio = self.getDevicePixelRatio() - return tuple(int(value / devicePixelRatio) - for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize) - - def setAxesMargins(self, left: float, top: float, right: float, bottom: float): - self._plotFrame.marginRatios = left, top, right, bottom - - def setForegroundColors(self, foregroundColor, gridColor): - self._plotFrame.foregroundColor = foregroundColor - self._plotFrame.gridColor = gridColor - - def setBackgroundColors(self, backgroundColor, dataBackgroundColor): - self._backgroundColor = backgroundColor - self._dataBackgroundColor = dataBackgroundColor 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 34844c6..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ /dev/null @@ -1,1375 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2021 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 numpy - -from silx.math.combo import min_max - -from ...._glutils import gl -from ...._glutils import Program, vertexBuffer, VertexBufferAttrib -from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate -from .GLPlotImage import GLPlotItem - - -_logger = logging.getLogger(__name__) - - -_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)) * 2 + 4 * len(slices) - points = numpy.empty((nbPoints, 2), dtype=numpy.float32) - - offset = 0 - # invert baseline for filling - new_y_data = numpy.append(self.yData, self.baseline) - for start, end in slices: - # Duplicate first point for connecting degenerated triangle - points[offset:offset+2] = self.xData[start], new_y_data[start] - - # 2nd point of the polygon is last point - points[offset+2] = self.xData[start], self.baseline[start] - - indices = numpy.append(numpy.arange(start, end), - numpy.arange(len(self.xData) + end-1, len(self.xData) + start-1, -1)) - indices = indices[buildFillMaskIndices(len(indices))] - - points[offset+3:offset+3+len(indices), 0] = self.xData[indices % len(self.xData)] - points[offset+3:offset+3+len(indices), 1] = new_y_data[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, context): - """Perform rendering - - :param RenderContext context: - """ - 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(context.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.isInitialized(): - self._xFillVboData.vbo.discard() - - self._xFillVboData = None - self._yFillVboData = None - - def isInitialized(self): - return self._xFillVboData is not None - - -# line ######################################################################## - -SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':' - - -class GLLines2D(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; - uniform vec4 dash2ndColor; - - 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) { - if (dash2ndColor.a == 0.) { - discard; // Discard full transparent bg color - } else { - gl_FragColor = dash2ndColor; - } - } else { - gl_FragColor = vColor; - } - } - """, - attrib0='xPos') - - def __init__(self, xVboData=None, yVboData=None, - colorVboData=None, distVboData=None, - style=SOLID, color=(0., 0., 0., 1.), dash2ndColor=None, - width=1, dashPeriod=10., drawMode=None, - offset=(0., 0.)): - if (xVboData is not None and - not isinstance(xVboData, VertexBufferAttrib)): - xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32) - self.xVboData = xVboData - - if (yVboData is not None and - not isinstance(yVboData, VertexBufferAttrib)): - yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32) - self.yVboData = yVboData - - # Compute distances if not given while providing numpy array coordinates - if (isinstance(self.xVboData, numpy.ndarray) and - isinstance(self.yVboData, numpy.ndarray) and - distVboData is None): - distVboData = distancesFromArrays(self.xVboData, self.yVboData) - - if (distVboData is not None and - not isinstance(distVboData, VertexBufferAttrib)): - distVboData = numpy.array( - distVboData, copy=False, dtype=numpy.float32) - self.distVboData = distVboData - - if colorVboData is not None: - assert isinstance(colorVboData, VertexBufferAttrib) - self.colorVboData = colorVboData - self.useColorVboData = colorVboData is not None - - self.color = color - self.dash2ndColor = dash2ndColor - 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, context): - """Perform rendering - - :param RenderContext context: - """ - width = self.width / 72. * context.dpi - - 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) - - dashPeriod = self.dashPeriod * width - if self.style == DOTTED: - dash = (0.2 * dashPeriod, - 0.5 * dashPeriod, - 0.7 * dashPeriod, - dashPeriod) - elif self.style == DASHDOT: - dash = (0.3 * dashPeriod, - 0.5 * dashPeriod, - 0.6 * dashPeriod, - dashPeriod) - else: - dash = (0.5 * dashPeriod, - dashPeriod, - dashPeriod, - dashPeriod) - - gl.glUniform4f(program.uniforms['dash'], *dash) - - if self.dash2ndColor is None: - # Use fully transparent color which gets discarded in shader - dash2ndColor = (0., 0., 0., 0.) - else: - dash2ndColor = self.dash2ndColor - gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor) - - distAttrib = program.attributes['distance'] - gl.glEnableVertexAttribArray(distAttrib) - if isinstance(self.distVboData, VertexBufferAttrib): - self.distVboData.setVertexAttrib(distAttrib) - else: - gl.glVertexAttribPointer(distAttrib, - 1, - gl.GL_FLOAT, - False, - 0, - self.distVboData) - - if width != 1: - gl.glEnable(gl.GL_LINE_SMOOTH) - - matrix = numpy.dot(context.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) - if isinstance(self.xVboData, VertexBufferAttrib): - self.xVboData.setVertexAttrib(xPosAttrib) - else: - gl.glVertexAttribPointer(xPosAttrib, - 1, - gl.GL_FLOAT, - False, - 0, - self.xVboData) - - yPosAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yPosAttrib) - if isinstance(self.yVboData, VertexBufferAttrib): - self.yVboData.setVertexAttrib(yPosAttrib) - else: - gl.glVertexAttribPointer(yPosAttrib, - 1, - gl.GL_FLOAT, - False, - 0, - self.yVboData) - - gl.glLineWidth(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 - """ - # Split array into sub-shapes at not finite points - splits = numpy.nonzero(numpy.logical_not(numpy.logical_and( - numpy.isfinite(xData), numpy.isfinite(yData))))[0] - splits = numpy.concatenate(([-1], splits, [len(xData) - 1])) - - # Compute distance independently for each sub-shapes, - # putting not finite points as last points of sub-shapes - distances = [] - for begin, end in zip(splits[:-1] + 1, splits[1:] + 1): - if begin == end: # Empty shape - continue - elif end - begin == 1: # Single element - distances.append([0]) - else: - deltas = numpy.dstack(( - numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)), - numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0] - distances.append( - numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1)))) - return numpy.concatenate(distances) - - -# points ###################################################################### - -DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \ - 'd', 'o', 's', '+', 'x', '.', ',', '*' - -H_LINE, V_LINE, HEART = '_', '|', u'\u2665' - -TICK_LEFT = "tickleft" -TICK_RIGHT = "tickright" -TICK_UP = "tickup" -TICK_DOWN = "tickdown" -CARET_LEFT = "caretleft" -CARET_RIGHT = "caretright" -CARET_UP = "caretup" -CARET_DOWN = "caretdown" - - -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, HEART, TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN, - CARET_LEFT, CARET_RIGHT, CARET_UP, CARET_DOWN) - """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; - } - } - """, - HEART: """ - float alphaSymbol(vec2 coord, float size) { - coord = (coord - 0.5) * 2.; - coord *= 0.75; - coord.y += 0.25; - float a = atan(coord.x,-coord.y)/3.141593; - float r = length(coord); - float h = abs(a); - float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h); - float res = clamp(r-d, 0., 1.); - // antialiasing - res = smoothstep(0.1, 0.001, res); - return res; - } - """, - TICK_LEFT: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float dy = abs(coord.y); - if (dy < 0.5 && coord.x < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - TICK_RIGHT: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float dy = abs(coord.y); - if (dy < 0.5 && coord.x > -0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - TICK_UP: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float dx = abs(coord.x); - if (dx < 0.5 && coord.y < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - TICK_DOWN: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float dx = abs(coord.x); - if (dx < 0.5 && coord.y > -0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - CARET_LEFT: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float d = abs(coord.x) - abs(coord.y); - if (d >= -0.1 && coord.x > 0.5) { - return smoothstep(-0.1, 0.1, d); - } else { - return 0.0; - } - } - """, - CARET_RIGHT: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float d = abs(coord.x) - abs(coord.y); - if (d >= -0.1 && coord.x < 0.5) { - return smoothstep(-0.1, 0.1, d); - } else { - return 0.0; - } - } - """, - CARET_UP: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float d = abs(coord.y) - abs(coord.x); - if (d >= -0.1 && coord.y > 0.5) { - return smoothstep(-0.1, 0.1, d); - } else { - return 0.0; - } - } - """, - CARET_DOWN: """ - float alphaSymbol(vec2 coord, float size) { - coord = size * (coord - 0.5); - float d = abs(coord.y) - abs(coord.x); - if (d >= -0.1 && coord.y < 0.5) { - return smoothstep(-0.1, 0.1, d); - } 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, context): - """Perform rendering - - :param RenderContext context: - """ - if self.marker is None: - return - - program = self._getProgram(self.marker) - program.use() - - matrix = numpy.dot(context.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 - size = size / 72. * context.dpi - - if self.marker in (PLUS, H_LINE, V_LINE, - TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN): - # Convert to nearest odd number - size = size // 2 * 2 + 1. - - gl.glUniform1f(program.uniforms['size'], size) - # gl.glPointSize(self.size) - - 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:`GLLines2D` 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 = GLLines2D( - 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, context): - """Perform rendering - - :param RenderContext context: - """ - self.prepare() - - if self._attribs is not None: - self._lines.render(context) - self._xErrPoints.render(context) - self._yErrPoints.render(context) - - def discard(self): - """Release VBOs""" - if self.isInitialized(): - 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 - - def isInitialized(self): - return self._attribs is not 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(GLPlotItem): - 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, - baseline=None, - isYLog=False): - super().__init__() - 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: - def deduce_baseline(baseline): - if baseline is None: - _baseline = 0 - else: - _baseline = baseline - if not isinstance(_baseline, numpy.ndarray): - _baseline = numpy.repeat(_baseline, - len(self.xData)) - if isYLog is True: - with numpy.errstate(divide='ignore', invalid='ignore'): - log_val = numpy.log10(_baseline) - _baseline = numpy.where(_baseline>0.0, log_val, -38) - return _baseline - - _baseline = deduce_baseline(baseline) - - # Use different baseline depending of Y log scale - self.fill = _Fill2D(self.xData, self.yData, - baseline=_baseline, - 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 = GLLines2D() - 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""" - GLLines2D.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, context): - """Perform rendering - - :param RenderContext context: Rendering information - """ - self.prepare() - if self.fill is not None: - self.fill.render(context) - self._errorBars.render(context) - self.lines.render(context) - self.points.render(context) - - 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 isInitialized(self): - return (self.xVboData is not None or - self._errorBars.isInitialized() or - (self.fill is not None and self.fill.isInitialized())) - - 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: Union[List[int],None] - """ - 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 numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings - 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 numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings - indices = numpy.nonzero((self.xData >= xPickMin) & - (self.xData <= xPickMax) & - (self.yData >= yPickMin) & - (self.yData <= yPickMax))[0].tolist() - - return tuple(indices) if len(indices) > 0 else None diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py deleted file mode 100644 index c5ee75b..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotFrame.py +++ /dev/null @@ -1,1219 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This modules provides the rendering of plot titles, axes and grid. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -# TODO -# keep aspect ratio managed here? -# smarter dirty flag handling? - -import datetime as dt -import math -import weakref -import logging -from collections import namedtuple - -import numpy - -from ...._glutils import gl, Program -from ..._utils import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX -from .GLSupport import mat4Ortho -from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270 -from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10 -from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString -from ..._utils.dtime_ticklayout import timestamp - -_logger = logging.getLogger(__name__) - - -# PlotAxis #################################################################### - -class PlotAxis(object): - """Represents a 1D axis of the plot. - This class is intended to be used with :class:`GLPlotFrame`. - """ - - def __init__(self, plotFrame, - tickLength=(0., 0.), - foregroundColor=(0., 0., 0., 1.0), - labelAlign=CENTER, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=CENTER, - titleRotate=0, titleOffset=(0., 0.)): - self._ticks = None - - self._plotFrameRef = weakref.ref(plotFrame) - - self._isDateTime = False - self._timeZone = None - self._isLog = False - self._dataRange = 1., 100. - self._displayCoords = (0., 0.), (1., 0.) - self._title = '' - - self._tickLength = tickLength - self._foregroundColor = foregroundColor - self._labelAlign = labelAlign - self._labelVAlign = labelVAlign - self._titleAlign = titleAlign - self._titleVAlign = titleVAlign - self._titleRotate = titleRotate - self._titleOffset = titleOffset - - @property - def dataRange(self): - """The range of the data represented on the axis as a tuple - of 2 floats: (min, max).""" - return self._dataRange - - @dataRange.setter - def dataRange(self, dataRange): - assert len(dataRange) == 2 - assert dataRange[0] <= dataRange[1] - dataRange = float(dataRange[0]), float(dataRange[1]) - - if dataRange != self._dataRange: - self._dataRange = dataRange - self._dirtyTicks() - - @property - def isLog(self): - """Whether the axis is using a log10 scale or not as a bool.""" - return self._isLog - - @isLog.setter - def isLog(self, isLog): - isLog = bool(isLog) - if isLog != self._isLog: - self._isLog = isLog - self._dirtyTicks() - - @property - def timeZone(self): - """Returnss datetime.tzinfo that is used if this axis plots date times.""" - return self._timeZone - - @timeZone.setter - def timeZone(self, tz): - """Sets dateetime.tzinfo that is used if this axis plots date times.""" - self._timeZone = tz - self._dirtyTicks() - - @property - def isTimeSeries(self): - """Whether the axis is showing floats as datetime objects""" - return self._isDateTime - - @isTimeSeries.setter - def isTimeSeries(self, isTimeSeries): - isTimeSeries = bool(isTimeSeries) - if isTimeSeries != self._isDateTime: - self._isDateTime = isTimeSeries - self._dirtyTicks() - - @property - def displayCoords(self): - """The coordinates of the start and end points of the axis - in display space (i.e., in pixels) as a tuple of 2 tuples of - 2 floats: ((x0, y0), (x1, y1)). - """ - return self._displayCoords - - @displayCoords.setter - def displayCoords(self, displayCoords): - assert len(displayCoords) == 2 - assert len(displayCoords[0]) == 2 - assert len(displayCoords[1]) == 2 - displayCoords = tuple(displayCoords[0]), tuple(displayCoords[1]) - if displayCoords != self._displayCoords: - self._displayCoords = displayCoords - self._dirtyTicks() - - @property - def devicePixelRatio(self): - """Returns the ratio between qt pixels and device pixels.""" - plotFrame = self._plotFrameRef() - return plotFrame.devicePixelRatio if plotFrame is not None else 1. - - @property - def title(self): - """The text label associated with this axis as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirtyPlotFrame() - - @property - def titleOffset(self): - """Title offset in pixels (x: int, y: int)""" - return self._titleOffset - - @titleOffset.setter - def titleOffset(self, offset): - if offset != self._titleOffset: - self._titleOffset = offset - self._dirtyTicks() - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._foregroundColor = color - self._dirtyTicks() - - @property - def ticks(self): - """Ticks as tuples: ((x, y) in display, dataPos, textLabel).""" - if self._ticks is None: - self._ticks = tuple(self._ticksGenerator()) - return self._ticks - - def getVerticesAndLabels(self): - """Create the list of vertices for axis and associated text labels. - - :returns: A tuple: List of 2D line vertices, List of Text2D labels. - """ - vertices = list(self.displayCoords) # Add start and end points - labels = [] - tickLabelsSize = [0., 0.] - - xTickLength, yTickLength = self._tickLength - xTickLength *= self.devicePixelRatio - yTickLength *= self.devicePixelRatio - for (xPixel, yPixel), dataPos, text in self.ticks: - if text is None: - tickScale = 0.5 - else: - tickScale = 1. - - label = Text2D(text=text, - color=self._foregroundColor, - x=xPixel - xTickLength, - y=yPixel - yTickLength, - align=self._labelAlign, - valign=self._labelVAlign, - devicePixelRatio=self.devicePixelRatio) - - width, height = label.size - if width > tickLabelsSize[0]: - tickLabelsSize[0] = width - if height > tickLabelsSize[1]: - tickLabelsSize[1] = height - - labels.append(label) - - vertices.append((xPixel, yPixel)) - vertices.append((xPixel + tickScale * xTickLength, - yPixel + tickScale * yTickLength)) - - (x0, y0), (x1, y1) = self.displayCoords - xAxisCenter = 0.5 * (x0 + x1) - yAxisCenter = 0.5 * (y0 + y1) - - xOffset, yOffset = self.titleOffset - - # Adaptative title positioning: - # tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2) - # xOffset = -tickLabelsSize[0] * xTickLength / tickNorm - # xOffset -= 3 * xTickLength - # yOffset = -tickLabelsSize[1] * yTickLength / tickNorm - # yOffset -= 3 * yTickLength - - axisTitle = Text2D(text=self.title, - color=self._foregroundColor, - x=xAxisCenter + xOffset, - y=yAxisCenter + yOffset, - align=self._titleAlign, - valign=self._titleVAlign, - rotate=self._titleRotate, - devicePixelRatio=self.devicePixelRatio) - labels.append(axisTitle) - - return vertices, labels - - def _dirtyPlotFrame(self): - """Dirty parent GLPlotFrame""" - plotFrame = self._plotFrameRef() - if plotFrame is not None: - plotFrame._dirty() - - def _dirtyTicks(self): - """Mark ticks as dirty and notify listener (i.e., background).""" - self._ticks = None - self._dirtyPlotFrame() - - @staticmethod - def _frange(start, stop, step): - """range for float (including stop).""" - while start <= stop: - yield start - start += step - - def _ticksGenerator(self): - """Generator of ticks as tuples: - ((x, y) in display, dataPos, textLabel). - """ - dataMin, dataMax = self.dataRange - if self.isLog and dataMin <= 0.: - _logger.warning( - 'Getting ticks while isLog=True and dataRange[0]<=0.') - dataMin = 1. - if dataMax < dataMin: - dataMax = 1. - - if dataMin != dataMax: # data range is not null - (x0, y0), (x1, y1) = self.displayCoords - - if self.isLog: - - if self.isTimeSeries: - _logger.warning("Time series not implemented for log-scale") - - logMin, logMax = math.log10(dataMin), math.log10(dataMax) - tickMin, tickMax, step, _ = niceNumbersForLog10(logMin, logMax) - - xScale = (x1 - x0) / (logMax - logMin) - yScale = (y1 - y0) / (logMax - logMin) - - for logPos in self._frange(tickMin, tickMax, step): - if logMin <= logPos <= logMax: - dataPos = 10 ** logPos - xPixel = x0 + (logPos - logMin) * xScale - yPixel = y0 + (logPos - logMin) * yScale - text = '1e%+03d' % logPos - yield ((xPixel, yPixel), dataPos, text) - - if step == 1: - ticks = list(self._frange(tickMin, tickMax, step))[:-1] - for logPos in ticks: - dataOrigPos = 10 ** logPos - for index in range(2, 10): - dataPos = dataOrigPos * index - if dataMin <= dataPos <= dataMax: - logSubPos = math.log10(dataPos) - xPixel = x0 + (logSubPos - logMin) * xScale - yPixel = y0 + (logSubPos - logMin) * yScale - yield ((xPixel, yPixel), dataPos, None) - - else: - xScale = (x1 - x0) / (dataMax - dataMin) - yScale = (y1 - y0) / (dataMax - dataMin) - - nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio - - # Density of 1.3 label per 92 pixels - # i.e., 1.3 label per inch on a 92 dpi screen - tickDensity = 1.3 / 92 - - if not self.isTimeSeries: - tickMin, tickMax, step, nbFrac = niceNumbersAdaptative( - dataMin, dataMax, nbPixels, tickDensity) - - for dataPos in self._frange(tickMin, tickMax, step): - if dataMin <= dataPos <= dataMax: - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - if nbFrac == 0: - text = '%g' % dataPos - else: - text = ('%.' + str(nbFrac) + 'f') % dataPos - yield ((xPixel, yPixel), dataPos, text) - else: - # Time series - dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) - dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) - - tickDateTimes, spacing, unit = calcTicksAdaptive( - dtMin, dtMax, nbPixels, tickDensity) - - for tickDateTime in tickDateTimes: - if dtMin <= tickDateTime <= dtMax: - - dataPos = timestamp(tickDateTime) - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - fmtStr = bestFormatString(spacing, unit) - text = tickDateTime.strftime(fmtStr) - - yield ((xPixel, yPixel), dataPos, text) - - -# GLPlotFrame ################################################################# - -class GLPlotFrame(object): - """Base class for rendering a 2D frame surrounded by axes.""" - - _TICK_LENGTH_IN_PIXELS = 5 - _LINE_WIDTH = 1 - - _SHADERS = { - 'vertex': """ - attribute vec2 position; - uniform mat4 matrix; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragment': """ - uniform vec4 color; - uniform float tickFactor; /* = 1./tickLength or 0. for solid line */ - - void main(void) { - if (mod(tickFactor * (gl_FragCoord.x + gl_FragCoord.y), 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - } - - _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom')) - - # Margins used when plot frame is not displayed - _NoDisplayMargins = _Margins(0, 0, 0, 0) - - def __init__(self, marginRatios, foregroundColor, gridColor): - """ - :param List[float] marginRatios: - The ratios of margins around plot area for axis and labels. - (left, top, right, bottom) as float in [0., 1.] - :param foregroundColor: color used for the frame and labels. - :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0 - :param gridColor: color used for grid lines. - :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0 - """ - self._renderResources = None - - self.__marginRatios = marginRatios - self.__marginsCache = None - - self._foregroundColor = foregroundColor - self._gridColor = gridColor - - self.axes = [] # List of PlotAxis to be updated by subclasses - - self._grid = False - self._size = 0., 0. - self._title = '' - - self._devicePixelRatio = 1. - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return self._renderResources is None - - GRID_NONE = 0 - GRID_MAIN_TICKS = 1 - GRID_SUB_TICKS = 2 - GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS) - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._foregroundColor = color - for axis in self.axes: - axis.foregroundColor = color - self._dirty() - - @property - def gridColor(self): - """Color used for frame and labels""" - return self._gridColor - - @gridColor.setter - def gridColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "gridColor must have length 4, got {}".format(len(self._gridColor)) - if self._gridColor != color: - self._gridColor = color - self._dirty() - - @property - def marginRatios(self): - """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1]. - """ - return self.__marginRatios - - @marginRatios.setter - def marginRatios(self, ratios): - ratios = tuple(float(v) for v in ratios) - assert len(ratios) == 4 - for value in ratios: - assert 0. <= value <= 1. - assert ratios[0] + ratios[2] < 1. - assert ratios[1] + ratios[3] < 1. - - if self.__marginRatios != ratios: - self.__marginRatios = ratios - self.__marginsCache = None # Clear cached margins - self._dirty() - - @property - def margins(self): - """Margins in pixels around the plot.""" - if self.__marginsCache is None: - width, height = self.size - left, top, right, bottom = self.marginRatios - self.__marginsCache = self._Margins( - left=int(left*width), - right=int(right*width), - top=int(top*height), - bottom=int(bottom*height)) - return self.__marginsCache - - @property - def devicePixelRatio(self): - return self._devicePixelRatio - - @devicePixelRatio.setter - def devicePixelRatio(self, ratio): - if ratio != self._devicePixelRatio: - self._devicePixelRatio = ratio - self._dirty() - - @property - def grid(self): - """Grid display mode: - - 0: No grid. - - 1: Grid on main ticks. - - 2: Grid on sub-ticks for log scale axes. - - 3: Grid on main and sub ticks.""" - return self._grid - - @grid.setter - def grid(self, grid): - assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS, - self.GRID_SUB_TICKS, self.GRID_ALL_TICKS) - if grid != self._grid: - self._grid = grid - self._dirty() - - @property - def size(self): - """Size in device pixels of the plot area including margins.""" - return self._size - - @size.setter - def size(self, size): - assert len(size) == 2 - size = tuple(size) - if size != self._size: - self._size = size - self.__marginsCache = None # Clear cached margins - self._dirty() - - @property - def plotOrigin(self): - """Plot area origin (left, top) in widget coordinates in pixels.""" - return self.margins.left, self.margins.top - - @property - def plotSize(self): - """Plot area size (width, height) in pixels.""" - w, h = self.size - w -= self.margins.left + self.margins.right - h -= self.margins.top + self.margins.bottom - return w, h - - @property - def title(self): - """Main title as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirty() - - # In-place update - # if self._renderResources is not None: - # self._renderResources[-1][-1].text = title - - def _dirty(self): - # When Text2D require discard we need to handle it - self._renderResources = None - - def _buildGridVertices(self): - if self._grid == self.GRID_NONE: - return [] - - elif self._grid == self.GRID_MAIN_TICKS: - def test(text): - return text is not None - elif self._grid == self.GRID_SUB_TICKS: - def test(text): - return text is None - elif self._grid == self.GRID_ALL_TICKS: - def test(_): - return True - else: - logging.warning('Wrong grid mode: %d' % self._grid) - return [] - - return self._buildGridVerticesWithTest(test) - - def _buildGridVerticesWithTest(self, test): - """Override in subclass to generate grid vertices""" - return [] - - def _buildVerticesAndLabels(self): - # To fill with copy of axes lists - vertices = [] - labels = [] - - for axis in self.axes: - axisVertices, axisLabels = axis.getVerticesAndLabels() - vertices += axisVertices - labels += axisLabels - - vertices = numpy.array(vertices, dtype=numpy.float32) - - # Add main title - xTitle = (self.size[0] + self.margins.left - - self.margins.right) // 2 - yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS - labels.append(Text2D(text=self.title, - color=self._foregroundColor, - x=xTitle, - y=yTitle, - align=CENTER, - valign=BOTTOM, - devicePixelRatio=self.devicePixelRatio)) - - # grid - gridVertices = numpy.array(self._buildGridVertices(), - dtype=numpy.float32) - - self._renderResources = (vertices, gridVertices, labels) - - _program = Program( - _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position') - - def render(self): - if self.margins == self._NoDisplayMargins: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - for label in labels: - label.render(matProj) - - def renderGrid(self): - if self._grid == self.GRID_NONE: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], *self._gridColor) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, gridVertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices)) - - -# GLPlotFrame2D ############################################################### - -class GLPlotFrame2D(GLPlotFrame): - def __init__(self, marginRatios, foregroundColor, gridColor): - """ - :param List[float] marginRatios: - The ratios of margins around plot area for axis and labels. - (left, top, right, bottom) as float in [0., 1.] - :param foregroundColor: color used for the frame and labels. - :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0 - :param gridColor: color used for grid lines. - :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0 - - """ - super(GLPlotFrame2D, self).__init__(marginRatios, foregroundColor, gridColor) - self.axes.append(PlotAxis(self, - tickLength=(0., -5.), - foregroundColor=self._foregroundColor, - labelAlign=CENTER, labelVAlign=TOP, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=0)) - - self._x2AxisCoords = () - - self.axes.append(PlotAxis(self, - tickLength=(5., 0.), - foregroundColor=self._foregroundColor, - labelAlign=RIGHT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=BOTTOM, - titleRotate=ROTATE_270)) - - self._y2Axis = PlotAxis(self, - tickLength=(-5., 0.), - foregroundColor=self._foregroundColor, - labelAlign=LEFT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=ROTATE_270) - - self._isYAxisInverted = False - - self._dataRanges = { - 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)} - - self._baseVectors = (1., 0.), (0., 1.) - - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - def _dirty(self): - super(GLPlotFrame2D, self)._dirty() - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return (super(GLPlotFrame2D, self).isDirty or - self._transformedDataRanges is None or - self._transformedDataProjMat is None or - self._transformedDataY2ProjMat is None) - - @property - def xAxis(self): - return self.axes[0] - - @property - def yAxis(self): - return self.axes[1] - - @property - def y2Axis(self): - return self._y2Axis - - @property - def isY2Axis(self): - """Whether to display the left Y axis or not.""" - return len(self.axes) == 3 - - @isY2Axis.setter - def isY2Axis(self, isY2Axis): - if isY2Axis != self.isY2Axis: - if isY2Axis: - self.axes.append(self._y2Axis) - else: - self.axes = self.axes[:2] - - self._dirty() - - @property - def isYAxisInverted(self): - """Whether Y axes are inverted or not as a bool.""" - return self._isYAxisInverted - - @isYAxisInverted.setter - def isYAxisInverted(self, value): - value = bool(value) - if value != self._isYAxisInverted: - self._isYAxisInverted = value - self._dirty() - - DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.) - """Values of baseVectors for orthogonal axes.""" - - @property - def baseVectors(self): - """Coordinates of the X and Y axes in the orthogonal plot coords. - - Raises ValueError if corresponding matrix is singular. - - 2 tuples of 2 floats: (xx, xy), (yx, yy) - """ - return self._baseVectors - - @baseVectors.setter - def baseVectors(self, baseVectors): - self._dirty() - - (xx, xy), (yx, yy) = baseVectors - vectors = (float(xx), float(xy)), (float(yx), float(yy)) - - det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]) - if det == 0.: - raise ValueError("Singular matrix for base vectors: " + - str(vectors)) - - if vectors != self._baseVectors: - self._baseVectors = vectors - self._dirty() - - def _updateTitleOffset(self): - """Update axes title offset according to margins""" - margins = self.margins - self.xAxis.titleOffset = 0, margins.bottom // 2 - self.yAxis.titleOffset = -3 * margins.left // 4, 0 - self.y2Axis.titleOffset = 3 * margins.right // 4, 0 - - # Override size and marginRatios setters to update titleOffsets - @GLPlotFrame.size.setter - def size(self, size): - GLPlotFrame.size.fset(self, size) - self._updateTitleOffset() - - @GLPlotFrame.marginRatios.setter - def marginRatios(self, ratios): - GLPlotFrame.marginRatios.fset(self, ratios) - self._updateTitleOffset() - - @property - def dataRanges(self): - """Ranges of data visible in the plot on x, y and y2 axes. - - This is different to the axes range when axes are not orthogonal. - - Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - """ - return self._DataRanges(self._dataRanges['x'], - self._dataRanges['y'], - self._dataRanges['y2']) - - @staticmethod - def _clipToSafeRange(min_, max_, isLog): - # Clip range if needed - minLimit = FLOAT32_MINPOS if isLog else FLOAT32_SAFE_MIN - min_ = numpy.clip(min_, minLimit, FLOAT32_SAFE_MAX) - max_ = numpy.clip(max_, minLimit, FLOAT32_SAFE_MAX) - assert min_ < max_ - return min_, max_ - - def setDataRanges(self, x=None, y=None, y2=None): - """Set data range over each axes. - - The provided ranges are clipped to possible values - (i.e., 32 float range + positive range for log scale). - - :param x: (min, max) data range over X axis - :param y: (min, max) data range over Y axis - :param y2: (min, max) data range over Y2 axis - """ - if x is not None: - self._dataRanges['x'] = \ - self._clipToSafeRange(x[0], x[1], self.xAxis.isLog) - - if y is not None: - self._dataRanges['y'] = \ - self._clipToSafeRange(y[0], y[1], self.yAxis.isLog) - - if y2 is not None: - self._dataRanges['y2'] = \ - self._clipToSafeRange(y2[0], y2[1], self.y2Axis.isLog) - - self.xAxis.dataRange = self._dataRanges['x'] - self.yAxis.dataRange = self._dataRanges['y'] - self.y2Axis.dataRange = self._dataRanges['y2'] - - _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2')) - - @property - def transformedDataRanges(self): - """Bounds of the displayed area in transformed data coordinates - (i.e., log scale applied if any as well as skew) - - 3-tuple of 2-tuple (min, max) for each axis: x, y, y2. - """ - if self._transformedDataRanges is None: - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self.dataRanges - - if self.xAxis.isLog: - try: - xMin = math.log10(xMin) - except ValueError: - _logger.info('xMin: warning log10(%f)', xMin) - xMin = 0. - try: - xMax = math.log10(xMax) - except ValueError: - _logger.info('xMax: warning log10(%f)', xMax) - xMax = 0. - - if self.yAxis.isLog: - try: - yMin = math.log10(yMin) - except ValueError: - _logger.info('yMin: warning log10(%f)', yMin) - yMin = 0. - try: - yMax = math.log10(yMax) - except ValueError: - _logger.info('yMax: warning log10(%f)', yMax) - yMax = 0. - - try: - y2Min = math.log10(y2Min) - except ValueError: - _logger.info('yMin: warning log10(%f)', y2Min) - y2Min = 0. - try: - y2Max = math.log10(y2Max) - except ValueError: - _logger.info('yMax: warning log10(%f)', y2Max) - y2Max = 0. - - self._transformedDataRanges = self._DataRanges( - (xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - - return self._transformedDataRanges - - @property - def transformedDataProjMat(self): - """Orthographic projection matrix for rendering transformed data - - :type: numpy.matrix - """ - if self._transformedDataProjMat is None: - xMin, xMax = self.transformedDataRanges.x - yMin, yMax = self.transformedDataRanges.y - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1) - self._transformedDataProjMat = mat - - return self._transformedDataProjMat - - @property - def transformedDataY2ProjMat(self): - """Orthographic projection matrix for rendering transformed data - for the 2nd Y axis - - :type: numpy.matrix - """ - if self._transformedDataY2ProjMat is None: - xMin, xMax = self.transformedDataRanges.x - y2Min, y2Max = self.transformedDataRanges.y2 - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1) - self._transformedDataY2ProjMat = mat - - return self._transformedDataY2ProjMat - - def dataToPixel(self, x, y, axis='left'): - """Convert data coordinate to widget pixel coordinate. - """ - assert axis in ('left', 'right') - - trBounds = self.transformedDataRanges - - if self.xAxis.isLog: - if x < FLOAT32_MINPOS: - return None - xDataTr = math.log10(x) - else: - xDataTr = x - - if self.yAxis.isLog: - if y < FLOAT32_MINPOS: - return None - yDataTr = math.log10(y) - else: - yDataTr = y - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - coords = numpy.dot(skew_mat, numpy.array((xDataTr, yDataTr))) - xDataTr, yDataTr = coords - - plotWidth, plotHeight = self.plotSize - - xPixel = int(self.margins.left + - plotWidth * (xDataTr - trBounds.x[0]) / - (trBounds.x[1] - trBounds.x[0])) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - yOffset = (plotHeight * (yDataTr - usedAxis[0]) / - (usedAxis[1] - usedAxis[0])) - - if self.isYAxisInverted: - yPixel = int(self.margins.top + yOffset) - else: - yPixel = int(self.size[1] - self.margins.bottom - yOffset) - - return xPixel, yPixel - - def pixelToData(self, x, y, axis="left"): - """Convert pixel position to data coordinates. - - :param float x: X coord - :param float y: Y coord - :param str axis: Y axis to use in ('left', 'right') - :return: (x, y) position in data coords - """ - assert axis in ("left", "right") - - plotWidth, plotHeight = self.plotSize - - trBounds = self.transformedDataRanges - - xData = (x - self.margins.left + 0.5) / float(plotWidth) - xData = trBounds.x[0] + xData * (trBounds.x[1] - trBounds.x[0]) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - if self.isYAxisInverted: - yData = (y - self.margins.top + 0.5) / float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - else: - yData = self.size[1] - self.margins.bottom - y - 0.5 - yData /= float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - - # non-orthogonal axis - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - skew_mat = numpy.linalg.inv(skew_mat) - - coords = numpy.dot(skew_mat, numpy.array((xData, yData))) - xData, yData = coords - - if self.xAxis.isLog: - xData = pow(10, xData) - if self.yAxis.isLog: - yData = pow(10, yData) - - return xData, yData - - def _buildGridVerticesWithTest(self, test): - vertices = [] - - if self.baseVectors == self.DEFAULT_BASE_VECTORS: - for axis in self.axes: - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - vertices.append((xPixel, yPixel)) - if axis == self.xAxis: - vertices.append((xPixel, self.margins.top)) - elif axis == self.yAxis: - vertices.append((self.size[0] - self.margins.right, - yPixel)) - else: # axis == self.y2Axis - vertices.append((self.margins.left, yPixel)) - - else: - # Get plot corners in data coords - plotLeft, plotTop = self.plotOrigin - plotWidth, plotHeight = self.plotSize - - corners = [(plotLeft, plotTop), - (plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop)] - - for axis in self.axes: - if axis == self.xAxis: - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ((cornersInData[0], cornersInData[3]), # top - (cornersInData[1], cornersInData[0]), # left - (cornersInData[3], cornersInData[2])) # right - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(x0, x1) <= data < max(x0, x1): - yIntersect = (data - x0) * \ - (y1 - y0) / (x1 - x0) + y0 - - pixelPos = self.dataToPixel( - data, yIntersect) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - else: # y or y2 axes - if axis == self.yAxis: - axis_name = 'left' - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ( - (cornersInData[3], cornersInData[2]), # right - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - else: # axis == self.y2Axis - axis_name = 'right' - corners = numpy.array([self.pixelToData( - x, y, axis='right') for (x, y) in corners]) - borders = ( - (cornersInData[1], cornersInData[0]), # left - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(y0, y1) <= data < max(y0, y1): - xIntersect = (data - y0) * \ - (x1 - x0) / (y1 - y0) + x0 - - pixelPos = self.dataToPixel( - xIntersect, data, axis=axis_name) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - return vertices - - def _buildVerticesAndLabels(self): - width, height = self.size - - xCoords = (self.margins.left - 0.5, - width - self.margins.right + 0.5) - yCoords = (height - self.margins.bottom + 0.5, - self.margins.top - 0.5) - - self.axes[0].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[1], yCoords[0])) - - self._x2AxisCoords = ((xCoords[0], yCoords[1]), - (xCoords[1], yCoords[1])) - - if self.isYAxisInverted: - # Y axes are inverted, axes coordinates are inverted - yCoords = yCoords[1], yCoords[0] - - self.axes[1].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[0], yCoords[1])) - - self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]), - (xCoords[1], yCoords[1])) - - super(GLPlotFrame2D, self)._buildVerticesAndLabels() - - vertices, gridVertices, labels = self._renderResources - - # Adds vertices for borders without axis - extraVertices = [] - extraVertices += self._x2AxisCoords - if not self.isY2Axis: - extraVertices += self._y2Axis.displayCoords - - extraVertices = numpy.array( - extraVertices, copy=False, dtype=numpy.float32) - vertices = numpy.append(vertices, extraVertices, axis=0) - - self._renderResources = (vertices, gridVertices, labels) - - @property - def foregroundColor(self): - """Color used for frame and labels""" - return self._foregroundColor - - @foregroundColor.setter - def foregroundColor(self, color): - """Color used for frame and labels""" - assert len(color) == 4, \ - "foregroundColor must have length 4, got {}".format(len(self._foregroundColor)) - if self._foregroundColor != color: - self._y2Axis.foregroundColor = color - GLPlotFrame.foregroundColor.fset(self, color) # call parent property diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py deleted file mode 100644 index 3ad94b9..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotImage.py +++ /dev/null @@ -1,756 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2021 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 -from .GLPlotItem import GLPlotItem - - -class _GLPlotData2D(GLPlotItem): - def __init__(self, data, origin, scale): - super().__init__() - 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 (row,), (col,) - 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 - - -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 vec2 bounds_oneOverRange; - uniform vec2 bounds_originOverRange; - - 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 - - /* isnan declaration for compatibility with GLSL 1.20 */ - bool isnan(float value) { - return (value != value); - } - - uniform sampler2D data; - uniform sampler2D cmap_texture; - uniform int cmap_normalization; - uniform float cmap_parameter; - uniform float cmap_min; - uniform float cmap_oneOverRange; - uniform float alpha; - uniform vec4 nancolor; - - varying vec2 coords; - - %s - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - float data = texture2D(data, textureCoords()).r; - float value = data; - if (cmap_normalization == 1) { /*Logarithm mapping*/ - if (value > 0.) { - value = clamp(cmap_oneOverRange * - (oneOverLog10 * log(value) - cmap_min), - 0., 1.); - } else { - value = 0.; - } - } else if (cmap_normalization == 2) { /*Square root mapping*/ - if (value >= 0.) { - value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min), - 0., 1.); - } else { - value = 0.; - } - } else if (cmap_normalization == 3) { /*Gamma correction mapping*/ - value = pow( - clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.), - cmap_parameter); - } else if (cmap_normalization == 4) { /* arcsinh mapping */ - /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */ - value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.); - } else { /*Linear mapping and fallback*/ - value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.); - } - - if (isnan(data)) { - gl_FragColor = nancolor; - } else { - 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, - numpy.dtype(numpy.float16): gl.GL_R16F, - # Use normalized integer for unsigned int formats - numpy.dtype(numpy.uint16): gl.GL_R16, - numpy.dtype(numpy.uint8): gl.GL_R8, - } - - _linearProgram = Program(_SHADERS['linear']['vertex'], - _SHADERS['fragment'] % - _SHADERS['linear']['fragTransform'], - attrib0='position') - - _logProgram = Program(_SHADERS['log']['vertex'], - _SHADERS['fragment'] % - _SHADERS['log']['fragTransform'], - attrib0='position') - - SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh' - - def __init__(self, data, origin, scale, - colormap, normalization='linear', gamma=0., cmapRange=None, - alpha=1.0, nancolor=(1., 1., 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 str normalization: The colormap normalization. - One of: 'linear', 'log', 'sqrt', 'gamma' - ;param float gamma: The gamma parameter (for 'gamma' normalization) - :param cmapRange: The range of colormap or None for autoscale colormap - For logarithmic colormap, the range is in the untransformed data - TODO: check consistency with matplotlib - :type cmapRange: (float, float) or None - :param float alpha: Opacity from 0 (transparent) to 1 (opaque) - :param nancolor: RGBA color for Not-A-Number values - :type nancolor: 4-tuple of float in [0., 1.] - """ - assert data.dtype in self._INTERNAL_FORMATS - assert normalization in self.SUPPORTED_NORMALIZATIONS - - super(GLPlotColormap, self).__init__(data, origin, scale) - self.colormap = numpy.array(colormap, copy=False) - self.normalization = normalization - self.gamma = gamma - self._cmapRange = (1., 10.) # Colormap range - self.cmapRange = cmapRange # Update _cmapRange - self._alpha = numpy.clip(alpha, 0., 1.) - self._nancolor = numpy.clip(nancolor, 0., 1.) - - self._cmap_texture = None - self._texture = None - 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 - - def isInitialized(self): - return (self._cmap_texture is not None or - self._texture is not None) - - @property - def cmapRange(self): - if self.normalization == 'log': - assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0. - elif self.normalization == 'sqrt': - assert self._cmapRange[0] >= 0. and self._cmapRange[1] >= 0. - return self._cmapRange - - @cmapRange.setter - 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)) - self._cmap_texture.prepare() - - 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 - param = 0. - - 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.normalization == 'log': - dataMin = math.log10(dataMin) - dataMax = math.log10(dataMax) - normID = 1 - elif self.normalization == 'sqrt': - dataMin = math.sqrt(dataMin) - dataMax = math.sqrt(dataMax) - normID = 2 - elif self.normalization == 'gamma': - # Keep dataMin, dataMax as is - param = self.gamma - normID = 3 - elif self.normalization == 'arcsinh': - dataMin = numpy.arcsinh(dataMin) - dataMax = numpy.arcsinh(dataMax) - normID = 4 - else: # Linear and fallback - normID = 0 - - gl.glUniform1i(prog.uniforms['cmap_texture'], - self._cmap_texture.texUnit) - gl.glUniform1i(prog.uniforms['cmap_normalization'], normID) - gl.glUniform1f(prog.uniforms['cmap_parameter'], param) - gl.glUniform1f(prog.uniforms['cmap_min'], dataMin) - if dataMax > dataMin: - oneOverRange = 1. / (dataMax - dataMin) - else: - oneOverRange = 0. # Fall-back - gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange) - - gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor) - - self._cmap_texture.bind() - - def _renderLinear(self, context): - """Perform rendering when both axes have linear scales - - :param RenderContext context: Rendering information - """ - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(context.matrix, - mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - 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, context): - """Perform rendering when one axis has log scale - - :param RenderContext context: Rendering information - """ - xMin, yMin = self.xMin, self.yMin - if ((context.isXLog and xMin < FLOAT32_MINPOS) or - (context.isYLog and yMin < FLOAT32_MINPOS)): - # Do not render images that are partly or totally <= 0 - return - - 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, - context.matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.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, context): - """Perform rendering - - :param RenderContext context: Rendering information - """ - if any((context.isXLog, context.isYLog)): - self._renderLog10(context) - else: - self._renderLinear(context) - - # 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 vec2 bounds_oneOverRange; - uniform vec2 bounds_originOverRange; - 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), - numpy.dtype(numpy.uint16)) - - _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.isInitialized(): - self._texture.discard() - self._texture = None - self._textureIsDirty = False - - def isInitialized(self): - return self._texture is not None - - 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: - formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB' - format_ = getattr(gl, formatName) - - if self.data.dtype == numpy.uint16: - formatName += '16' # Use sized internal format for uint16 - internalFormat = getattr(gl, formatName) - - self._texture = Image(internalFormat, - self.data, - format_=format_, - texUnit=self._DATA_TEX_UNIT) - 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, context): - """Perform rendering with both axes having linear scales - - :param RenderContext context: Rendering information - """ - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(context.matrix, mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._texture.render(prog.attributes['position'], - prog.attributes['texCoords'], - self._DATA_TEX_UNIT) - - def _renderLog(self, context): - """Perform rendering with axes having log scale - - :param RenderContext context: Rendering information - """ - 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, - context.matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.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, context): - """Perform rendering - - :param RenderContext context: Rendering information - """ - if any((context.isXLog, context.isYLog)): - self._renderLog(context) - else: - self._renderLinear(context) diff --git a/silx/gui/plot/backends/glutils/GLPlotItem.py b/silx/gui/plot/backends/glutils/GLPlotItem.py deleted file mode 100644 index ae13091..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotItem.py +++ /dev/null @@ -1,99 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2020-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides a base class for PlotWidget OpenGL backend primitives -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "02/07/2020" - - -class RenderContext: - """Context with which to perform OpenGL rendering. - - :param numpy.ndarray matrix: 4x4 transform matrix to use for rendering - :param bool isXLog: Whether X axis is log scale or not - :param bool isYLog: Whether Y axis is log scale or not - :param float dpi: Number of device pixels per inch - """ - - def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96.): - self.matrix = matrix - """Current transformation matrix""" - - self.__isXLog = isXLog - self.__isYLog = isYLog - self.__dpi = dpi - - @property - def isXLog(self): - """True if X axis is using log scale""" - return self.__isXLog - - @property - def isYLog(self): - """True if Y axis is using log scale""" - return self.__isYLog - - @property - def dpi(self): - """Number of device pixels per inch""" - return self.__dpi - - -class GLPlotItem: - """Base class for primitives used in the PlotWidget OpenGL backend""" - - def __init__(self): - self.yaxis = 'left' - "YAxis this item is attached to (either 'left' or 'right')" - - def pick(self, x, y): - """Perform picking at given position. - - :param float x: X coordinate in plot data frame of reference - :param float y: Y coordinate in plot data frame of reference - :returns: - Result of picking as a list of indices or None if nothing picked - :rtype: Union[List[int],None] - """ - return None - - def render(self, context): - """Performs OpenGL rendering of the item. - - :param RenderContext context: Rendering context information - """ - pass - - def discard(self): - """Discards OpenGL resources this item has created.""" - pass - - def isInitialized(self) -> bool: - """Returns True if resources where initialized and requires `discard`. - """ - return True diff --git a/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/silx/gui/plot/backends/glutils/GLPlotTriangles.py deleted file mode 100644 index fbe9e02..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotTriangles.py +++ /dev/null @@ -1,197 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2019-2021 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 a set of 2D triangles -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import ctypes - -import numpy - -from .....math.combo import min_max -from .... import _glutils as glutils -from ...._glutils import gl -from .GLPlotItem import GLPlotItem - - -class GLPlotTriangles(GLPlotItem): - """Handle rendering of a set of colored triangles""" - - _PROGRAM = glutils.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.0, 1.0); - vColor = color; - } - """, - fragmentShader=""" - #version 120 - - uniform float alpha; - varying vec4 vColor; - - void main(void) { - gl_FragColor = vColor; - gl_FragColor.a *= alpha; - } - """, - attrib0='xPos') - - def __init__(self, x, y, color, triangles, alpha=1.): - """ - - :param numpy.ndarray x: X coordinates of triangle corners - :param numpy.ndarray y: Y coordinates of triangle corners - :param numpy.ndarray color: color for each point - :param numpy.ndarray triangles: (N, 3) array of indices of triangles - :param float alpha: Opacity in [0, 1] - """ - super().__init__() - # Check and convert input data - x = numpy.ravel(numpy.array(x, dtype=numpy.float32)) - y = numpy.ravel(numpy.array(y, dtype=numpy.float32)) - color = numpy.array(color, copy=False) - # Cast to uint32 - triangles = numpy.array(triangles, copy=False, dtype=numpy.uint32) - - assert x.size == y.size - assert x.size == len(color) - assert color.ndim == 2 and color.shape[1] in (3, 4) - if numpy.issubdtype(color.dtype, numpy.floating): - color = numpy.array(color, dtype=numpy.float32, copy=False) - elif numpy.issubdtype(color.dtype, numpy.integer): - color = numpy.array(color, dtype=numpy.uint8, copy=False) - else: - raise ValueError('Unsupported color type') - assert triangles.ndim == 2 and triangles.shape[1] == 3 - - self.__x_y_color = x, y, color - self.xMin, self.xMax = min_max(x, finite=True) - self.yMin, self.yMax = min_max(y, finite=True) - self.__triangles = triangles - self.__alpha = numpy.clip(float(alpha), 0., 1.) - self.__vbos = None - self.__indicesVbo = None - self.__picking_triangles = None - - def pick(self, x, y): - """Perform picking - - :param float x: X coordinates in plot data frame - :param float y: Y coordinates in plot data frame - :return: List of picked data point indices - :rtype: Union[List[int],None] - """ - if (x < self.xMin or x > self.xMax or - y < self.yMin or y > self.yMax): - return None - - xPts, yPts = self.__x_y_color[:2] - if self.__picking_triangles is None: - self.__picking_triangles = numpy.zeros( - self.__triangles.shape + (3,), dtype=numpy.float32) - self.__picking_triangles[:, :, 0] = xPts[self.__triangles] - self.__picking_triangles[:, :, 1] = yPts[self.__triangles] - - segment = numpy.array(((x, y, -1), (x, y, 1)), dtype=numpy.float32) - # Picked triangle indices - indices = glutils.segmentTrianglesIntersection( - segment, self.__picking_triangles)[0] - # Point indices - indices = numpy.unique(numpy.ravel(self.__triangles[indices])) - - # Sorted from furthest to closest point - dists = (xPts[indices] - x) ** 2 + (yPts[indices] - y) ** 2 - indices = indices[numpy.flip(numpy.argsort(dists), axis=0)] - - return tuple(indices) if len(indices) > 0 else None - - def discard(self): - """Release resources on the GPU""" - if self.isInitialized(): - self.__vbos[0].vbo.discard() - self.__vbos = None - self.__indicesVbo.discard() - self.__indicesVbo = None - - def isInitialized(self): - return self.__vbos is not None - - def prepare(self): - """Allocate resources on the GPU""" - if self.__vbos is None: - self.__vbos = glutils.vertexBuffer(self.__x_y_color) - # Normalization is need for color - self.__vbos[-1].normalization = True - - if self.__indicesVbo is None: - self.__indicesVbo = glutils.VertexBuffer( - numpy.ravel(self.__triangles), - usage=gl.GL_STATIC_DRAW, - target=gl.GL_ELEMENT_ARRAY_BUFFER) - - def render(self, context): - """Perform rendering - - :param RenderContext context: Rendering information - """ - self.prepare() - - if self.__vbos is None or self.__indicesVbo is None: - return # Nothing to display - - self._PROGRAM.use() - - gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], - 1, - gl.GL_TRUE, - context.matrix.astype(numpy.float32)) - - gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha) - - for index, name in enumerate(('xPos', 'yPos', 'color')): - attr = self._PROGRAM.attributes[name] - gl.glEnableVertexAttribArray(attr) - self.__vbos[index].setVertexAttrib(attr) - - with self.__indicesVbo: - gl.glDrawElements(gl.GL_TRIANGLES, - self.__triangles.size, - glutils.numpyToGLType(self.__triangles.dtype), - ctypes.c_void_p(0)) diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py deleted file mode 100644 index da6dffa..0000000 --- a/silx/gui/plot/backends/glutils/GLSupport.py +++ /dev/null @@ -1,158 +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 FilledShape2D(object): - _NO_HATCH = 0 - _HATCH_STEP = 20 - - def __init__(self, points, style='solid', color=(0., 0., 0., 1.)): - self.vertices = numpy.array(points, dtype=numpy.float32, copy=False) - 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.style = style - self.color = color - - def render(self, posAttrib, colorUnif, hatchStepUnif): - assert self.style in ('hatch', 'solid') - gl.glUniform4f(colorUnif, *self.color) - step = self._HATCH_STEP if self.style == 'hatch' else self._NO_HATCH - gl.glUniform1i(hatchStepUnif, step) - - # Prepare fill mask - 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) - - 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) - - -# 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 d6ae6fa..0000000 --- a/silx/gui/plot/backends/glutils/GLText.py +++ /dev/null @@ -1,287 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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 weakref - -import numpy - -from ...._glutils import font, gl, Context, 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 = weakref.WeakKeyDictionary() - """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, - devicePixelRatio= 1.): - self.devicePixelRatio = devicePixelRatio - 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, devicePixelRatio): - # Retrieve/initialize texture cache for current context - textureKey = text, devicePixelRatio - - context = Context.getCurrent() - if context not in self._textures: - self._textures[context] = _Cache( - callback=lambda key, value: value[0].discard()) - textures = self._textures[context] - - if textureKey not in textures: - image, offset = font.rasterText( - text, - font.getDefaultFontFamily(), - devicePixelRatio=self.devicePixelRatio) - if textureKey not in self._sizes: - self._sizes[textureKey] = image.shape[1], image.shape[0] - - texture = Texture( - gl.GL_RED, - data=image, - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)) - texture.prepare() - textures[textureKey] = texture, offset - - return textures[textureKey] - - @property - def text(self): - return self._text - - @property - def size(self): - textureKey = self.text, self.devicePixelRatio - if textureKey not in self._sizes: - image, offset = font.rasterText( - self.text, - font.getDefaultFontFamily(), - devicePixelRatio=self.devicePixelRatio) - self._sizes[textureKey] = image.shape[1], image.shape[0] - return self._sizes[textureKey] - - def getVertices(self, offset, shape): - height, width = shape - - 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, self.devicePixelRatio) - - 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 37fbdd0..0000000 --- a/silx/gui/plot/backends/glutils/GLTexture.py +++ /dev/null @@ -1,241 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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) - texture.prepare() - 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_, - 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) - texture.prepare() - 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) - texture.prepare() - # 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 5fb6853..0000000 --- a/silx/gui/plot/backends/glutils/PlotImageFile.py +++ /dev/null @@ -1,153 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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.tobytes() 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.tobytes()) - - 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 f87d7c1..0000000 --- a/silx/gui/plot/backends/glutils/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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 .GLPlotItem import GLPlotItem, RenderContext # noqa -from .GLPlotTriangles import GLPlotTriangles # noqa -from .GLSupport import * # noqa -from .GLText import * # noqa -from .GLTexture import * # noqa |