From 30946321348abc349fb4003dc39d0232dc19606c Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Fri, 7 Jun 2024 17:57:09 +0200 Subject: [PATCH] feat(utils.colors): general color validators --- bec_widgets/utils/colors.py | 211 ++++++++++++++++++ .../figure/plots/waveform/waveform_curve.py | 23 +- .../spiral_progress_bar.py | 11 +- 3 files changed, 221 insertions(+), 24 deletions(-) diff --git a/bec_widgets/utils/colors.py b/bec_widgets/utils/colors.py index f1d65cd4..488c8886 100644 --- a/bec_widgets/utils/colors.py +++ b/bec_widgets/utils/colors.py @@ -1,11 +1,14 @@ +import re from typing import Literal import numpy as np import pyqtgraph as pg +from pydantic_core import PydanticCustomError from qtpy.QtGui import QColor class Colors: + @staticmethod def golden_ratio(num: int) -> list: """Calculate the golden ratio for a given number of angles. @@ -63,3 +66,211 @@ class Colors: else: raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.") return colors + + @staticmethod + def validate_color(color: tuple | str) -> tuple | str: + """ + Validate the color input if it is HEX or RGBA compatible. Can be used in any pydantic model as a field validator. + + Args: + color(tuple|str): The color to be validated. Can be a tuple of RGBA values or a HEX string. + + Returns: + tuple|str: The validated color. + """ + CSS_COLOR_NAMES = { + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "cadetblue", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "green", + "greenyellow", + "grey", + "honeydew", + "hotpink", + "indianred", + "indigo", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "magenta", + "maroon", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "red", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen", + } + if isinstance(color, str): + hex_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$") + if hex_pattern.match(color): + return color + elif color.lower() in CSS_COLOR_NAMES: + return color + else: + raise PydanticCustomError( + "unsupported color", + "The color must be a valid HEX string or CSS Color.", + {"wrong_value": color}, + ) + elif isinstance(color, tuple): + if len(color) != 4: + raise PydanticCustomError( + "unsupported color", + "The color must be a tuple of 4 elements (R, G, B, A).", + {"wrong_value": color}, + ) + for value in color: + if not 0 <= value <= 255: + raise PydanticCustomError( + "unsupported color", + f"The color values must be between 0 and 255. Provide color {color}.", + {"wrong_value": color}, + ) + return color + + @staticmethod + def validate_color_map(color_map: str) -> str: + """ + Validate the colormap input if it is supported by pyqtgraph. Can be used in any pydantic model as a field validator. If validation fails it prints all available colormaps from pyqtgraph instance. + + Args: + color_map(str): The colormap to be validated. + + Returns: + str: The validated colormap. + """ + available_colormaps = pg.colormap.listMaps() + if color_map not in available_colormaps: + raise PydanticCustomError( + "unsupported colormap", + f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.", + {"wrong_value": color_map}, + ) + return color_map diff --git a/bec_widgets/widgets/figure/plots/waveform/waveform_curve.py b/bec_widgets/widgets/figure/plots/waveform/waveform_curve.py index 3d8979a5..14bf1315 100644 --- a/bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +++ b/bec_widgets/widgets/figure/plots/waveform/waveform_curve.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, field_validator from pydantic_core import PydanticCustomError from qtpy import QtCore -from bec_widgets.utils import BECConnector, ConnectionConfig +from bec_widgets.utils import BECConnector, ConnectionConfig, Colors if TYPE_CHECKING: from bec_widgets.widgets.figure.plots.waveform import BECWaveform1D @@ -37,9 +37,11 @@ class Signal(BaseModel): class CurveConfig(ConnectionConfig): parent_id: Optional[str] = Field(None, description="The parent plot of the curve.") label: Optional[str] = Field(None, description="The label of the curve.") - color: Optional[Any] = Field(None, description="The color of the curve.") + color: Optional[str | tuple] = Field(None, description="The color of the curve.") symbol: Optional[str] = Field("o", description="The symbol of the curve.") - symbol_color: Optional[str] = Field(None, description="The color of the symbol of the curve.") + symbol_color: Optional[str | tuple] = Field( + None, description="The color of the symbol of the curve." + ) symbol_size: Optional[int] = Field(5, description="The size of the symbol of the curve.") pen_width: Optional[int] = Field(2, description="The width of the pen of the curve.") pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field( @@ -50,19 +52,12 @@ class CurveConfig(ConnectionConfig): color_map_z: Optional[str] = Field( "plasma", description="The colormap of the curves z gradient.", validate_default=True ) + model_config: dict = {"validate_assignment": True} - @field_validator("color_map_z") - def validate_color_map(cls, v, values): - if v is not None and v != "": - available_colormaps = pg.colormap.listMaps() - if v not in available_colormaps: - raise PydanticCustomError( - "unsupported colormap", - f"Colormap '{v}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.", - {"wrong_value": v}, - ) - return v + _validate_color_map_z = field_validator("color_map_z")(Colors.validate_color_map) + _validate_color = field_validator("color")(Colors.validate_color) + _validate_symbol_color = field_validator("symbol_color")(Colors.validate_color) class BECCurve(BECConnector, pg.PlotDataItem): diff --git a/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py b/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py index 4dff9d2c..63a43af0 100644 --- a/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py +++ b/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py @@ -59,16 +59,7 @@ class SpiralProgressBarConfig(ConnectionConfig): ) return v - @field_validator("color_map") - def validate_color_map(cls, v, values): - if v is not None and v != "": - if v not in pg.colormap.listMaps(): - raise PydanticCustomError( - "unsupported colormap", - f"Colormap '{v}' not found in the current installation of pyqtgraph", - {"wrong_value": v}, - ) - return v + _validate_colormap = field_validator("color_map")(Colors.validate_color_map) class SpiralProgressBar(BECConnector, QWidget):