# /*##########################################################################
#
# Copyright (c) 2004-2023 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.
"""
from __future__ import annotations
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
from collections.abc import Callable
import weakref
from silx.gui.colors import RGBAColorType
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"
[docs]
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.0, 100.0
self.__yLimits = {"left": (1.0, 100.0), "right": (1.0, 100.0)}
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
[docs]
def addCurve(
self,
x,
y,
color,
gapcolor,
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 Union[str, None] gapcolor:
color used to fill dashed line gaps.
: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 linestyle: Type of line::
- ' ' or '' no line
- '-' solid line
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
- (offset, (dash pattern))
: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()
[docs]
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()
[docs]
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()
[docs]
def addShape(
self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
):
"""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 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
- (offset, (dash pattern))
:param float linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
:param str gapcolor: 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()
[docs]
def addMarker(
self,
x: float | None,
y: float | None,
text: str | None,
color: str,
symbol: str | None,
linestyle: str | tuple[float, tuple[float, ...] | None],
linewidth: float,
constraint: Callable[[float, float], tuple[float, float]] | None,
yaxis: str,
font: qt.QFont,
bgcolor: RGBAColorType | None,
) -> object:
"""Add a point, vertical line or horizontal line marker to the plot.
:param x: Horizontal position of the marker in graph coordinates.
If None, the marker is a horizontal line.
:param y: Vertical position of the marker in graph coordinates.
If None, the marker is a vertical line.
:param text: Text associated to the marker (or None for no text)
:param color: Color to be used for instance 'blue', 'b', '#FF0000'
:param bgcolor: Text background color to be used for instance 'blue', 'b', '#FF0000'
:param 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 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
- (offset, (dash pattern))
:param 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.
It takes the coordinates of the current cursor position in the plot
as input and that returns the filtered coordinates.
:param yaxis: The Y axis this marker belongs to in: 'left', 'right'
:param font: QFont to use to render text
:return: Handle used by the backend to univocally access the marker
"""
return object()
# Remove methods
[docs]
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
[docs]
def setGraphCursorShape(self, cursor):
"""Set the cursor shape.
To override in interactive backends.
:param str cursor: Name of the cursor shape or None
"""
pass
[docs]
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
- (offset, (dash pattern))
:type linestyle: None, one of the predefined styles or (offset, (dash pattern)).
"""
pass
[docs]
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())
)
[docs]
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
[docs]
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.
[docs]
def postRedisplay(self):
"""Trigger backend update and repaint."""
self.replot()
[docs]
def replot(self):
"""Redraw the plot."""
with self._plot._paintContext():
pass
[docs]
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
[docs]
def setGraphTitle(self, title):
"""Set the main title of the plot.
:param str title: Title associated to the plot
"""
pass
[docs]
def setGraphXLabel(self, label):
"""Set the X axis label.
:param str label: label associated to the plot bottom X axis
"""
pass
[docs]
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
[docs]
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
[docs]
def getGraphXLimits(self):
"""Get the graph X (bottom) limits.
:return: Minimum and maximum values of the X axis
"""
return self.__xLimits
[docs]
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
[docs]
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]
[docs]
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
[docs]
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
[docs]
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
[docs]
def isXAxisTimeSeries(self):
"""Return True if the X-axis scale shows datetime objects.
:rtype: bool
"""
return self.__xAxisTimeSeries
[docs]
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)
[docs]
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
[docs]
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
[docs]
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)
[docs]
def isYAxisInverted(self):
"""Return True if left Y axis is inverted, False otherwise."""
return self.__yAxisInverted
[docs]
def isYRightAxisVisible(self) -> bool:
"""Return True if the Y axis on the right side of the plot is visible"""
return False
[docs]
def isKeepDataAspectRatio(self):
"""Returns whether the plot is keeping data aspect ratio or not."""
return self.__keepDataAspectRatio
[docs]
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)
[docs]
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
[docs]
def dataToPixel(self, x, y, axis):
"""Convert a position in data space to a position in pixels
in the widget.
:param x: The X coordinate in data space.
:type x: float or sequence of float
:param y: The Y coordinate in data space.
:type y: float or sequence of float
: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()
[docs]
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()
[docs]
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()
[docs]
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
[docs]
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
[docs]
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