Source code for plotpy.styles.base

# -*- coding: utf-8 -*-

from __future__ import annotations

from copy import deepcopy
from math import atan2, pi, sqrt
from typing import TYPE_CHECKING

from guidata.dataset import (
    BeginGroup,
    BoolItem,
    ButtonItem,
    ColorItem,
    DataSet,
    DataSetGroup,
    EndGroup,
    FloatItem,
    ImageChoiceItem,
    IntItem,
    ObjectItem,
    StringItem,
    update_dataset,
)
from guidata.dataset.qtitemwidgets import DataSetWidget
from guidata.dataset.qtwidgets import DataSetEditLayout
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from qwt import QwtPlotCurve, QwtPlotMarker, QwtSymbol

from plotpy.config import _

if TYPE_CHECKING:
    import guidata.dataset

    from plotpy.interfaces import IBasePlotItem
    from plotpy.plot import BasePlot

LINESTYLES = {"-": "SolidLine", "--": "DashLine", ":": "DotLine", "-.": "DashDotLine"}
COLORS = {
    "r": "red",
    "g": "green",
    "b": "blue",
    "c": "cyan",
    "m": "magenta",
    "y": "yellow",
    "k": "black",
    "w": "white",
    "G": "gray",
}
MARKERS = {
    "+": QwtSymbol.Cross,
    "o": QwtSymbol.Ellipse,
    "*": QwtSymbol.Star1,
    ".": QwtSymbol(
        QwtSymbol.Ellipse, QG.QBrush(QC.Qt.black), QG.QPen(QC.Qt.black), QC.QSizeF(3, 3)
    ),
    "x": QwtSymbol.XCross,
    "s": QwtSymbol.Rect,
    "d": QwtSymbol.Diamond,
    "^": QwtSymbol.UTriangle,
    "v": QwtSymbol.DTriangle,
    ">": QwtSymbol.RTriangle,
    "<": QwtSymbol.LTriangle,
    "h": QwtSymbol.Star2,
}
MARKERSTYLES = {None: "NoLine", "-": "HLine", "|": "VLine", "+": "Cross"}


LINESTYLE_CHOICES = [
    ("SolidLine", _("Solid line"), "solid.png"),
    ("DashLine", _("Dashed line"), "dash.png"),
    ("DotLine", _("Dotted line"), "dot.png"),
    ("DashDotLine", _("Dash-dot line"), "dashdot.png"),
    ("DashDotDotLine", _("Dash-dot-dot line"), "dashdotdot.png"),
    ("NoPen", _("No line"), "none.png"),
]
MARKER_CHOICES = [
    ("Cross", _("Cross"), "cross.png"),
    ("Ellipse", _("Ellipse"), "ellipse.png"),
    ("Star1", _("Star"), "star.png"),
    ("XCross", _("X-Cross"), "xcross.png"),
    ("Rect", _("Square"), "square.png"),
    ("Diamond", _("Diamond"), "diamond.png"),
    ("UTriangle", _("Triangle"), "triangle_u.png"),
    ("DTriangle", _("Triangle"), "triangle_d.png"),
    ("RTriangle", _("Triangle"), "triangle_r.png"),
    ("LTriangle", _("Triangle"), "triangle_l.png"),
    ("Star2", _("Hexagon"), "hexagon.png"),
    ("NoSymbol", _("No symbol"), "none.png"),
]
CURVESTYLE_CHOICES = [
    ("Lines", _("Lines"), "lines.png"),
    ("Sticks", _("Sticks"), "sticks.png"),
    ("Steps", _("Steps"), "steps.png"),
    ("Dots", _("Dots"), "dots.png"),
    ("NoCurve", _("No curve"), "none.png"),
]

BRUSHSTYLE_CHOICES = [
    ("NoBrush", _("No brush pattern"), "nobrush.png"),
    ("SolidPattern", _("Uniform color"), "solidpattern.png"),
    ("Dense1Pattern", _("Extremely dense brush pattern"), "dense1pattern.png"),
    ("Dense2Pattern", _("Very dense brush pattern"), "dense2pattern.png"),
    ("Dense3Pattern", _("Somewhat dense brush pattern"), "dense3pattern.png"),
    ("Dense4Pattern", _("Half dense brush pattern"), "dense4pattern.png"),
    ("Dense5Pattern", _("Somewhat sparse brush pattern"), "dense5pattern.png"),
    ("Dense6Pattern", _("Very sparse brush pattern"), "dense6pattern.png"),
    ("Dense7Pattern", _("Extremely sparse brush pattern"), "dense7pattern.png"),
    ("HorPattern", _("Horizontal lines"), "horpattern.png"),
    ("VerPattern", _("Vertical lines"), "verpattern.png"),
    ("CrossPattern", _("Crossing horizontal and vertical lines"), "crosspattern.png"),
    ("BDiagPattern", _("Backward diagonal lines"), "bdiagpattern.png"),
    ("FDiagPattern", _("Forward diagonal lines"), "fdiagpattern.png"),
    ("DiagCrossPattern", _("Crossing diagonal lines"), "diagcrosspattern.png"),
    # (
    #     "LinearGradientPattern",
    #     _("Linear gradient (set using a dedicated QBrush constructor)"),
    #     "none.png",
    # ),
    # (
    #     "ConicalGradientPattern",
    #     _("Conical gradient (set using a dedicated QBrush constructor)"),
    #     "none.png",
    # ),
    # (
    #     "RadialGradientPattern",
    #     _("Radial gradient (set using a dedicated QBrush constructor)"),
    #     "none.png",
    # ),
    # ("TexturePattern", _("Custom pattern (see QBrush::setTexture())"), "none.png"),
]

MARKERSTYLE_CHOICES = [
    ("NoLine", _("None"), "none.png"),
    ("HLine", _("Horizontal"), "horiz_marker.png"),
    ("VLine", _("Vertical"), "vert_marker.png"),
    ("Cross", _("Cross"), "cross_marker.png"),
]


def build_reverse_map(lst, obj):
    """

    :param lst:
    :param obj:
    :return:
    """
    dict = {}
    for idx, _name, _icon in lst:
        val = getattr(obj, idx)
        dict[val] = idx
    return dict


MARKER_NAME = build_reverse_map(MARKER_CHOICES, QwtSymbol)
CURVESTYLE_NAME = build_reverse_map(CURVESTYLE_CHOICES, QwtPlotCurve)
LINESTYLE_NAME = build_reverse_map(LINESTYLE_CHOICES, QC.Qt)
BRUSHSTYLE_NAME = build_reverse_map(BRUSHSTYLE_CHOICES, QC.Qt)
MARKERSTYLE_NAME = build_reverse_map(MARKERSTYLE_CHOICES, QwtPlotMarker)


def style_generator(color_keys="bgrcmykG"):
    """Cycling through curve styles"""
    while True:
        for linestyle in sorted(LINESTYLES.keys()):
            for color in color_keys:
                yield color + linestyle


def update_style_attr(style: str, param) -> None:
    """Parse a MATLAB-like style string.

    Example: 'r--o' for red dashed line with circle markers

    Also accepts HTML colors like '#ff0000'.

    Args:
        style: style string
    """
    for marker in list(MARKERS.keys()):
        if marker in style:
            param.symbol.update_param(MARKERS[marker])
            break
    else:
        param.symbol.update_param(QwtSymbol.NoSymbol)
    for linestyle in list(LINESTYLES.keys()):
        if linestyle in style:
            param.line.style = LINESTYLES[linestyle]
            break
    else:
        param.line.style = "NoPen"
    color = "black"
    for style_str in style:
        if style_str.startswith("#"):  # HTML color
            color = style_str[:7]
            break
    for color_letter in list(COLORS.keys()):
        if color_letter in style:
            color = COLORS[color_letter]
            break
    param.line.color = color
    param.symbol.facecolor = color
    param.symbol.edgecolor = color


[docs] class ItemParameters: """Class handling QwtPlotItem-like parameters Args: multiselection (bool): if True, the class will handle """ MULTISEL_DATASETS = [] # Customizing tab display order: ENDING_PARAMETERS = ( "CurveParam", "ErrorBarParam", "ShapeParam", "LabelParam", "LegendParam", "GridParam", "AxesParam", ) def __init__(self, multiselection: bool = False): self.multiselection = multiselection self.paramdict: dict[str, guidata.dataset.DataSet] = {} self.items: set[IBasePlotItem] = set()
[docs] @classmethod def register_multiselection(cls, klass, klass_ms): """Register a DataSet couple: (DataSet, DataSet_for_MultiSelection)""" # Inserting element backwards because classes have to be registered # from children to parent (see 'add' method to fully understand why) cls.MULTISEL_DATASETS.insert(0, (klass, klass_ms))
def __add(self, key, item, param): self.paramdict[key] = param self.items.add(item)
[docs] def add( self, key: str, item: IBasePlotItem, param: guidata.dataset.DataSet, ) -> None: """ Add parameters for a given item Args: key: key to identify the item item: item to be customized param: parameters to be applied to the item """ if self.multiselection: for klass, klass_ms in self.MULTISEL_DATASETS: if isinstance(param, klass): title = param.get_title() if key in self.paramdict and not title.endswith("s"): title += "s" param_ms = klass_ms( title=title, comment=param.get_comment(), icon=param.get_icon() ) update_dataset(param_ms, param) self.__add(key, item, param_ms) return self.__add(key, item, param)
[docs] def get(self, key: str) -> guidata.dataset.DataSet: """ Get parameters for a given item Args: key: key to identify the item """ return deepcopy(self.paramdict.get(key))
[docs] def update(self, plot: BasePlot) -> None: """ Update plot items according to the parameters Args: plot: plot to be updated """ for item in self.items: item.set_item_parameters(self) plot.SIG_ITEM_PARAMETERS_CHANGED.emit(item) plot.replot() plot.SIG_ITEMS_CHANGED.emit(plot)
[docs] def edit(self, plot: BasePlot, title: str, icon: str) -> None: """ Edit parameters Args: plot: plot to be updated title: title of the dialog icon: icon of the dialog """ paramdict = self.paramdict.copy() ending_parameters = [] for key in self.ENDING_PARAMETERS: if key in paramdict: ending_parameters.append(paramdict.pop(key)) parameters = list(paramdict.values()) + ending_parameters dset = DataSetGroup(parameters, title=title.rstrip("."), icon=icon) if dset.edit(parent=plot, apply=lambda dset: self.update(plot)): self.update(plot)
# =================================================== # Common font parameters # =================================================== def _font_selection(param, item, value, parent): font = param.build_font() result, valid = QW.QFontDialog.getFont(font, parent) if valid: param.update_param(result)
[docs] class FontParam(DataSet): family = StringItem(_("Family"), default="default") _choose = ButtonItem(_("Choose font"), _font_selection, default=None).set_pos(col=1) size = IntItem(_("Size in point"), default=12) bold = BoolItem(_("Bold"), default=False).set_pos(col=1) italic = BoolItem(_("Italic"), default=False).set_pos(col=2)
[docs] def update_param(self, font): """ :param font: """ self.family = str(font.family()) self.size = font.pointSize() self.bold = bool(font.bold()) self.italic = bool(font.italic())
[docs] def build_font(self): """ :return: """ font = QG.QFont(self.family) font.setPointSize(self.size) font.setBold(self.bold) font.setItalic(self.italic) return font
class FontItemWidget(DataSetWidget): klass = FontParam class FontItem(ObjectItem): """Item holding a LineStyleParam""" klass = FontParam DataSetEditLayout.register(FontItem, FontItemWidget) # =================================================== # Common Qwt symbol parameters # ===================================================
[docs] class SymbolParam(DataSet): marker = ImageChoiceItem(_("Style"), MARKER_CHOICES, default="NoSymbol") size = IntItem(_("Size"), default=9) edgecolor = ColorItem(_("Border"), default="gray") edgewidth = FloatItem(_("Border width"), default=1.0, min=0.0) facecolor = ColorItem(_("Background color"), default="yellow") alpha = FloatItem(_("Background alpha"), default=1.0, min=0, max=1)
[docs] def update_param(self, symb): """ :param symb: :return: """ if not isinstance(symb, QwtSymbol): # check if this is still needed # raise RuntimeError assert isinstance(symb, QwtSymbol.Style) self.marker = MARKER_NAME[symb] return self.marker = MARKER_NAME[symb.style()] self.size = int(symb.size().width()) self.edgecolor = str(symb.pen().color().name()) self.edgewidth = float(symb.pen().widthF()) self.facecolor = str(symb.brush().color().name())
[docs] def build_symbol(self): """ :return: """ marker_type = getattr(QwtSymbol, self.marker) color = QG.QColor(self.facecolor) color.setAlphaF(self.alpha) pen = QG.QPen(QG.QColor(self.edgecolor)) pen.setWidthF(self.edgewidth) marker = QwtSymbol( marker_type, QG.QBrush(color), pen, QC.QSizeF(self.size, self.size), ) return marker
[docs] def update_symbol(self, obj): """ :param obj: """ obj.setSymbol(self.build_symbol())
class SymbolItemWidget(DataSetWidget): klass = SymbolParam class SymbolItem(ObjectItem): """Item holding a SymbolParam""" klass = SymbolParam DataSetEditLayout.register(SymbolItem, SymbolItemWidget) # =================================================== # Common line style parameters # ===================================================
[docs] class LineStyleParam(DataSet): style = ImageChoiceItem(_("Style"), LINESTYLE_CHOICES, default="SolidLine") color = ColorItem(_("Color"), default="black") width = FloatItem(_("Width"), default=1.0, min=0.0)
[docs] def update_param(self, pen): """ :param pen: """ self.width = pen.widthF() self.color = str(pen.color().name()) self.style = LINESTYLE_NAME[pen.style()]
[docs] def build_pen(self): """ :return: """ linecolor = QG.QColor(self.color) style = getattr(QC.Qt, self.style) pen = QG.QPen(linecolor, self.width, style) return pen
[docs] def set_style_from_matlab(self, linestyle): """Eventually convert MATLAB-like linestyle into Qt linestyle""" linestyle = LINESTYLES.get(linestyle, linestyle) # MATLAB-style if linestyle == "": # MATLAB-style linestyle = "NoPen" self.style = linestyle
class LineStyleItemWidget(DataSetWidget): klass = LineStyleParam class LineStyleItem(ObjectItem): """Item holding a LineStyleParam""" klass = LineStyleParam DataSetEditLayout.register(LineStyleItem, LineStyleItemWidget) # =================================================== # Common brush style parameters # ===================================================
[docs] class BrushStyleParam(DataSet): style = ImageChoiceItem(_("Style"), BRUSHSTYLE_CHOICES, default="SolidPattern") color = ColorItem(_("Color"), default="black") alpha = FloatItem(_("Alpha"), default=1.0) angle = FloatItem(_("Angle"), default=0.0, min=0) sx = FloatItem(_("sx"), default=1.0, min=0) sy = FloatItem(_("sy"), default=1.0, min=0)
[docs] def update_param(self, brush): """ :param brush: """ tr = brush.transform() pt = tr.map(QC.QPointF(1.0, 0.0)) self.sx = sqrt(pt.x() ** 2 + pt.y() ** 2) self.angle = 180 * atan2(pt.y(), pt.x()) / pi pt = tr.map(QC.QPointF(0.0, 1.0)) self.sy = sqrt(pt.x() ** 2 + pt.y() ** 2) col = brush.color() self.color = str(col.name()) self.alpha = col.alphaF() self.style = BRUSHSTYLE_NAME[brush.style()]
[docs] def build_brush(self): """ :return: """ color = QG.QColor(self.color) color.setAlphaF(self.alpha) brush = QG.QBrush(color, getattr(QC.Qt, self.style)) tr = QG.QTransform() tr = tr.scale(self.sx, self.sy) tr = tr.rotate(self.angle) brush.setTransform(tr) return brush
class BrushStyleItemWidget(DataSetWidget): klass = BrushStyleParam class BrushStyleItem(ObjectItem): """Item holding a LineStyleParam""" klass = BrushStyleParam DataSetEditLayout.register(BrushStyleItem, BrushStyleItemWidget) # =================================================== # QwtText parameters # ===================================================
[docs] class TextStyleParam(DataSet): font = FontItem(_("Font")) textcolor = ColorItem(_("Text color"), default="blue") background_color = ColorItem(_("Background color"), default="white") background_alpha = FloatItem(_("Background alpha"), default=0.5, min=0, max=1)
[docs] def update_param(self, obj): """obj: QwtText instance""" self.font.update_param(obj.font()) self.textcolor = obj.color().name() color = obj.backgroundBrush().color() self.background_color = color.name() self.background_alpha = color.alphaF()
[docs] def update_text(self, obj): """obj: QwtText instance""" obj.setColor(QG.QColor(self.textcolor)) color = QG.QColor(self.background_color) color.setAlphaF(self.background_alpha) obj.setBackgroundBrush(QG.QBrush(color)) font = self.font.build_font() obj.setFont(font)
class TextStyleItemWidget(DataSetWidget): klass = TextStyleParam class TextStyleItem(ObjectItem): """Item holding a TextStyleParam""" klass = TextStyleParam DataSetEditLayout.register(TextStyleItem, TextStyleItemWidget) # =================================================== # Grid parameters # ===================================================
[docs] class GridParam(DataSet): background = ColorItem(_("Background color"), default="white") maj = BeginGroup(_("Major grid")) maj_xenabled = BoolItem(_("X Axis"), default=True) maj_yenabled = BoolItem(_("Y Axis"), default=True).set_pos(col=1) maj_line = LineStyleItem(_("Line")) _maj = EndGroup("end group") min = BeginGroup(_("Minor grid")) min_xenabled = BoolItem(_("X Axis"), default=False) min_yenabled = BoolItem(_("Y Axis"), default=False).set_pos(col=1) min_line = LineStyleItem(_("Line")) _min = EndGroup("fin groupe")
[docs] def update_param(self, grid): """ :param grid: """ plot = grid.plot() if plot is not None: self.background = str(plot.canvasBackground().color().name()) self.maj_xenabled = grid.xEnabled() self.maj_yenabled = grid.yEnabled() self.maj_line.update_param(grid.majorPen()) self.min_xenabled = grid.xMinEnabled() self.min_yenabled = grid.yMinEnabled() self.min_line.update_param(grid.minorPen())
[docs] def update_grid(self, grid): """ :param grid: """ plot = grid.plot() if plot is not None: plot.blockSignals(True) # Avoid unwanted calls of update_param # triggered by the setter methods below plot.setCanvasBackground(QG.QColor(self.background)) grid.enableX(self.maj_xenabled) grid.enableY(self.maj_yenabled) grid.setPen(self.maj_line.build_pen()) grid.enableXMin(self.min_xenabled) grid.enableYMin(self.min_yenabled) grid.setMinorPen(self.min_line.build_pen()) grid.setTitle(self.get_title()) if plot is not None: plot.blockSignals(False)