# /*##########################################################################
#
# Copyright (c) 2017-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.
#
# ###########################################################################*/
"""This module provides the :class:`Curve` item of the :class:`Plot`.
"""
from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
import logging
import numpy
from ....utils.deprecation import deprecated_warning
from ... import colors
from .core import (
PointsBase,
LabelsMixIn,
ColorMixIn,
YAxisMixIn,
FillMixIn,
LineMixIn,
LineGapColorMixIn,
LineStyleType,
SymbolMixIn,
BaselineMixIn,
HighlightedMixIn,
_Style,
)
from silx._utils import NP_OPTIONAL_COPY
_logger = logging.getLogger(__name__)
[docs]
class CurveStyle(_Style):
"""Object storing the style of a curve.
Set a value to None to use the default
:param color: Color
:param linestyle: Style of the line
:param linewidth: Width of the line
:param symbol: Symbol for markers
:param symbolsize: Size of the markers
:param gapcolor: Color of gaps of dashed line
"""
def __init__(
self,
color: colors.RGBAColorType | None = None,
linestyle: LineStyleType | None = None,
linewidth: float | None = None,
symbol: str | None = None,
symbolsize: float | None = None,
gapcolor: colors.RGBAColorType | None = None,
):
if color is None:
self._color = None
else:
if isinstance(color, str):
color = colors.rgba(color)
else: # array-like expected
color = numpy.asarray(color)
if color.ndim == 1: # Array is 1D, this is a single color
color = colors.rgba(color)
self._color = color
if not LineMixIn.isValidLineStyle(linestyle):
raise ValueError(f"Not a valid line style: {linestyle}")
self._linestyle = linestyle
self._linewidth = None if linewidth is None else float(linewidth)
if symbol is not None:
assert symbol in SymbolMixIn.getSupportedSymbols()
self._symbol = symbol
self._symbolsize = None if symbolsize is None else float(symbolsize)
self._gapcolor = None if gapcolor is None else colors.rgba(gapcolor)
[docs]
def getColor(self, copy=True):
"""Returns the color or None if not set.
:param bool copy: True to get a copy (default),
False to get internal representation (do not modify!)
:rtype: Union[List[float],None]
"""
if isinstance(self._color, numpy.ndarray):
return numpy.array(self._color, copy=copy or NP_OPTIONAL_COPY)
else:
return self._color
def getLineGapColor(self):
"""Returns the color of dashed line gaps or None if not set.
:rtype: Union[List[float],None]
"""
return self._gapcolor
[docs]
def getLineStyle(self) -> LineStyleType | None:
"""Return the type of the line or None if not set.
Type of line::
- ' ' no line
- '-' solid line
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
- (offset, (dash pattern))
"""
return self._linestyle
[docs]
def getLineWidth(self):
"""Return the curve line width in pixels or None if not set.
:rtype: Union[float,None]
"""
return self._linewidth
[docs]
def getSymbol(self):
"""Return the point marker type.
Marker type::
- 'o' circle
- '.' point
- ',' pixel
- '+' cross
- 'x' x-cross
- 'd' diamond
- 's' square
:rtype: Union[str,None]
"""
return self._symbol
[docs]
def getSymbolSize(self):
"""Return the point marker size in points.
:rtype: Union[float,None]
"""
return self._symbolsize
def __eq__(self, other):
if isinstance(other, CurveStyle):
return (
numpy.array_equal(self.getColor(), other.getColor())
and self.getLineStyle() == other.getLineStyle()
and self.getLineWidth() == other.getLineWidth()
and self.getSymbol() == other.getSymbol()
and self.getSymbolSize() == other.getSymbolSize()
and self.getLineGapColor() == other.getLineGapColor()
)
else:
return False
[docs]
class Curve(
PointsBase,
ColorMixIn,
YAxisMixIn,
FillMixIn,
LabelsMixIn,
LineMixIn,
LineGapColorMixIn,
BaselineMixIn,
HighlightedMixIn,
):
"""Description of a curve"""
_DEFAULT_Z_LAYER = 1
"""Default overlay layer for curves"""
_DEFAULT_SELECTABLE = True
"""Default selectable state for curves"""
_DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
_DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
_DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color="black")
"""Default highlight style of the item"""
_DEFAULT_BASELINE = None
def __init__(self):
PointsBase.__init__(self)
ColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
FillMixIn.__init__(self)
LabelsMixIn.__init__(self)
LineMixIn.__init__(self)
LineGapColorMixIn.__init__(self)
BaselineMixIn.__init__(self)
HighlightedMixIn.__init__(self)
self._setBaseline(Curve._DEFAULT_BASELINE)
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
# Filter-out values <= 0
xFiltered, yFiltered, xerror, yerror = self.getData(copy=False, displayed=True)
if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
return None # No data to display, do not add renderer to backend
style = self.getCurrentStyle()
return backend.addCurve(
xFiltered,
yFiltered,
color=style.getColor(),
gapcolor=style.getLineGapColor(),
symbol=style.getSymbol(),
linestyle=style.getLineStyle(),
linewidth=style.getLineWidth(),
yaxis=self.getYAxis(),
xerror=xerror,
yerror=yerror,
fill=self.isFill(),
alpha=self.getAlpha(),
symbolsize=style.getSymbolSize(),
baseline=self.getBaseline(copy=False),
)
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
deprecated_warning(
"Attributes",
"__getitem__",
since_version="2.0.0",
replacement="Use Curve methods",
)
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
return self.getXData(copy=False)
elif item == 1:
return self.getYData(copy=False)
elif item == 2:
return self.getName()
elif item == 3:
info = self.getInfo(copy=False)
return {} if info is None else info
elif item == 4:
params = {
"info": self.getInfo(),
"color": self.getColor(),
"symbol": self.getSymbol(),
"linewidth": self.getLineWidth(),
"linestyle": self.getLineStyle(),
"xlabel": self.getXLabel(),
"ylabel": self.getYLabel(),
"yaxis": self.getYAxis(),
"xerror": self.getXErrorData(copy=False),
"yerror": self.getYErrorData(copy=False),
"z": self.getZValue(),
"selectable": self.isSelectable(),
"fill": self.isFill(),
}
return params
else:
raise IndexError("Index out of range: %s", str(item))
[docs]
def getCurrentStyle(self):
"""Returns the current curve style.
Curve style depends on curve highlighting
:rtype: CurveStyle
"""
if self.isHighlighted():
style = self.getHighlightedStyle()
color = style.getColor()
linestyle = style.getLineStyle()
linewidth = style.getLineWidth()
symbol = style.getSymbol()
symbolsize = style.getSymbolSize()
gapcolor = style.getLineGapColor()
return CurveStyle(
color=self.getColor() if color is None else color,
linestyle=self.getLineStyle() if linestyle is None else linestyle,
linewidth=self.getLineWidth() if linewidth is None else linewidth,
symbol=self.getSymbol() if symbol is None else symbol,
symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize,
gapcolor=self.getLineGapColor() if gapcolor is None else gapcolor,
)
else:
return CurveStyle(
color=self.getColor(),
linestyle=self.getLineStyle(),
linewidth=self.getLineWidth(),
symbol=self.getSymbol(),
symbolsize=self.getSymbolSize(),
gapcolor=self.getLineGapColor(),
)
[docs]
def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
"""Set the data of the curve.
:param numpy.ndarray x: The data corresponding to the x coordinates.
:param numpy.ndarray y: The data corresponding to the y coordinates.
:param xerror: Values with the uncertainties on the x values
: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 positive errors,
row 1 for negative errors.
:param yerror: Values with the uncertainties on the y values.
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param baseline: curve baseline
:type baseline: Union[None,float,numpy.ndarray]
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror, copy=copy)
self._setBaseline(baseline=baseline)