summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-xsilx/gui/plot/backends/BackendBase.py578
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py1544
-rwxr-xr-xsilx/gui/plot/backends/BackendOpenGL.py1420
-rw-r--r--silx/gui/plot/backends/__init__.py29
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py1375
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py1219
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py756
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotItem.py99
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py197
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py158
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py287
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py241
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py153
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py46
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