Source code for bokeh.colors.color

#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Provide a base class for representing color values.

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import colorsys
from abc import ABCMeta, abstractmethod
from math import sqrt
from re import match
from typing import TYPE_CHECKING, TypeAlias

# Bokeh imports
from ..core.serialization import AnyRep, Serializable, Serializer

if TYPE_CHECKING:
    from typing_extensions import Self

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'Color',
    'ColorLike',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

RGBTuple = tuple[int, int, int] | tuple[int, int, int, float]

[docs] class Color(Serializable, metaclass=ABCMeta): ''' A base class for representing color objects. ''' def __repr__(self) -> str: return self.to_css()
[docs] def to_serializable(self, serializer: Serializer) -> AnyRep: return self.to_css()
[docs] @staticmethod def clamp(value: float, maximum: float | None = None) -> float: ''' Clamp numeric values to be non-negative, an optionally, less than a given maximum. Args: value (float) : A number to clamp. maximum (float, optional) : A max bound to to clamp to. If None, there is no upper bound, and values are only clamped to be non-negative. (default: None) Returns: float ''' value = max(value, 0) if maximum is not None: return min(value, maximum) else: return value
[docs] @abstractmethod def copy(self) -> Self: ''' Copy this color. *Subclasses must implement this method.* ''' raise NotImplementedError
[docs] def darken(self, amount: float) -> Self: ''' Darken (reduce the luminance) of this color. *Subclasses must implement this method.* Args: amount (float) : Amount to reduce the luminance by (clamped above zero) Returns: Color ''' return self.lighten(-amount)
[docs] @classmethod @abstractmethod def from_hsl(cls, value: HSL) -> Self: ''' Create a new color by converting from an HSL color. *Subclasses must implement this method.* Args: value (HSL) : A color to convert from HSL Returns: Color ''' raise NotImplementedError
[docs] @classmethod @abstractmethod def from_rgb(cls, value: RGB) -> Self: ''' Create a new color by converting from an RGB color. *Subclasses must implement this method.* Args: value (:class:`~bokeh.colors.RGB`) : A color to convert from RGB Returns: Color ''' raise NotImplementedError
[docs] def lighten(self, amount: float) -> Self: ''' Lighten (increase the luminance) of this color. *Subclasses must implement this method.* Args: amount (float) : Amount to increase the luminance by (clamped above zero) Returns: Color ''' rgb = self.to_rgb() h, l, s = colorsys.rgb_to_hls(float(rgb.r)/255, float(rgb.g)/255, float(rgb.b)/255) new_l = self.clamp(l + amount, 1) r, g, b = colorsys.hls_to_rgb(h, new_l, s) rgb.r = round(r * 255) rgb.g = round(g * 255) rgb.b = round(b * 255) return self.from_rgb(rgb)
[docs] @abstractmethod def to_css(self) -> str: ''' Return a CSS representation of this color. *Subclasses must implement this method.* Returns: str ''' raise NotImplementedError
[docs] @abstractmethod def to_hsl(self) -> HSL: ''' Create a new HSL color by converting from this color. *Subclasses must implement this method.* Returns: HSL ''' raise NotImplementedError
[docs] @abstractmethod def to_rgb(self) -> RGB: ''' Create a new HSL color by converting from this color. *Subclasses must implement this method.* Returns: :class:`~bokeh.colors.RGB` ''' raise NotImplementedError
class RGB(Color): ''' Represent colors by specifying their Red, Green, and Blue channels. Alpha values may also optionally be provided. Otherwise, alpha values default to 1. ''' def __init__(self, r: int, g: int, b: int, a: float = 1.0) -> None: ''' Args: r (int) : The value for the red channel in [0, 255] g (int) : The value for the green channel in [0, 255] b (int) : The value for the blue channel in [0, 255] a (float, optional) : An alpha value for this color in [0, 1] (default: 1.0) ''' self.r = r self.g = g self.b = b self.a = a def copy(self) -> RGB: ''' Return a copy of this color value. Returns: :class:`~bokeh.colors.RGB` ''' return RGB(self.r, self.g, self.b, self.a) @classmethod def from_hsl(cls, value: HSL) -> RGB: ''' Create an RGB color from an HSL color value. Args: value (HSL) : The HSL color to convert. Returns: :class:`~bokeh.colors.RGB` ''' return value.to_rgb() @classmethod def from_hex_string(cls, hex_string: str) -> RGB: ''' Create an RGB color from a RGB(A) hex string. Args: hex_string (str) : String containing hex-encoded RGBA(A) values. Valid formats are '#rrggbb', '#rrggbbaa', '#rgb' and '#rgba'. Returns: :class:`~bokeh.colors.RGB` ''' if isinstance(hex_string, str): # Hex color as #rrggbbaa or #rrggbb if match(r"#([\da-fA-F]{2}){3,4}\Z", hex_string): r = int(hex_string[1:3], 16) g = int(hex_string[3:5], 16) b = int(hex_string[5:7], 16) a = int(hex_string[7:9], 16) / 255.0 if len(hex_string) > 7 else 1.0 return RGB(r, g, b, a) # Hex color as #rgb or #rgba if match(r"#[\da-fA-F]{3,4}\Z", hex_string): r = int(hex_string[1]*2, 16) g = int(hex_string[2]*2, 16) b = int(hex_string[3]*2, 16) a = int(hex_string[4]*2, 16) / 255.0 if len(hex_string) > 4 else 1.0 return RGB(r, g, b, a) raise ValueError(f"'{hex_string}' is not an RGB(A) hex color string") @classmethod def from_tuple(cls, value: RGBTuple) -> RGB: ''' Initialize ``RGB`` instance from a 3- or 4-tuple. ''' if len(value) == 3: r, g, b = value return RGB(r, g, b) else: r, g, b, a = value return RGB(r, g, b, a) @classmethod def from_rgb(cls, value: RGB) -> RGB: ''' Copy an RGB color from another RGB color value. Args: value (:class:`~bokeh.colors.RGB`) : The RGB color to copy. Returns: :class:`~bokeh.colors.RGB` ''' return value.copy() def to_css(self) -> str: ''' Generate the CSS representation of this RGB color. Returns: str, ``"rgb(...)"`` or ``"rgba(...)"`` ''' if self.a == 1.0: return f"rgb({self.r}, {self.g}, {self.b})" else: return f"rgba({self.r}, {self.g}, {self.b}, {self.a})" def to_hex(self) -> str: ''' Return a hex color string for this RGB(A) color. Any alpha value is only included in the output string if it is less than 1. Returns: str, ``"#RRGGBBAA"`` if alpha is less than 1 and ``"#RRGGBB"`` otherwise ''' if self.a < 1.0: return f"#{self.r:02x}{self.g:02x}{self.b:02x}{int(round(self.a*255)):02x}" else: return f"#{self.r:02x}{self.g:02x}{self.b:02x}" def to_hsl(self) -> HSL: ''' Return a corresponding HSL color for this RGB color. Returns: :class:`~bokeh.colors.HSL` ''' h, l, s = colorsys.rgb_to_hls(float(self.r)/255, float(self.g)/255, float(self.b)/255) return HSL(round(h*360), s, l, self.a) def to_rgb(self) -> RGB: ''' Return a RGB copy for this RGB color. Returns: :class:`~bokeh.colors.RGB` ''' return self.copy() @property def brightness(self) -> float: """ Perceived brightness of a color in [0, 1] range. """ # http://alienryderflex.com/hsp.html r, g, b = self.r, self.g, self.b return sqrt(0.299*r**2 + 0.587*g**2 + 0.114*b**2)/255 @property def luminance(self) -> float: """ Perceived luminance of a color in [0, 1] range. """ # https://en.wikipedia.org/wiki/Relative_luminance r, g, b = self.r, self.g, self.b return (0.2126*r**2.2 + 0.7152*g**2.2 + 0.0722*b**2.2) / 255**2.2 class HSL(Color): ''' Represent colors by specifying their Hue, Saturation, and lightness. Alpha values may also optionally be provided. Otherwise, alpha values default to 1. ''' def __init__(self, h: float, s: float, l: float, a: float = 1.0) -> None: ''' Args: h (int) : The Hue, in [0, 360] s (int) : The Saturation, in [0, 1] l (int) : The lightness, in [0, 1] a (float, optional) : An alpha value for this color in [0, 1] (default: 1.0) ''' self.h = h self.s = s self.l = l self.a = a def copy(self) -> HSL: ''' Return a copy of this color value. Returns: :class:`~bokeh.colors.HSL` ''' return HSL(self.h, self.s, self.l, self.a) def darken(self, amount: float) -> HSL: ''' Darken (reduce the luminance) of this color. Args: amount (float) : Amount to reduce the luminance by (clamped above zero) Returns: :class:`~bokeh.colors.HSL` ''' return self.lighten(-amount) @classmethod def from_hsl(cls, value: HSL) -> HSL: ''' Copy an HSL color from another HSL color value. Args: value (HSL) : The HSL color to copy. Returns: :class:`~bokeh.colors.hsl.HSL` ''' return value.copy() @classmethod def from_rgb(cls, value: RGB) -> HSL: ''' Create an HSL color from an RGB color value. Args: value (:class:`~bokeh.colors.RGB`) : The RGB color to convert. Returns: :class:`~bokeh.colors.HSL` ''' return value.to_hsl() def to_css(self) -> str: ''' Generate the CSS representation of this HSL color. Returns: str, ``"hsl(...)"`` or ``"hsla(...)"`` ''' if self.a == 1.0: return f"hsl({self.h}, {self.s*100}%, {self.l*100}%)" else: return f"hsla({self.h}, {self.s*100}%, {self.l*100}%, {self.a})" def to_hsl(self) -> HSL: ''' Return a HSL copy for this HSL color. Returns: :class:`~bokeh.colors.HSL` ''' return self.copy() def to_rgb(self) -> RGB: ''' Return a corresponding :class:`~bokeh.colors.RGB` color for this HSL color. Returns: :class:`~bokeh.colors.RGB` ''' r, g, b = colorsys.hls_to_rgb(float(self.h)/360, self.l, self.s) return RGB(round(r*255), round(g*255), round(b*255), self.a) def lighten(self, amount: float) -> HSL: ''' Lighten (increase the luminance) of this color. Args: amount (float) : Amount to increase the luminance by (clamped above zero) Returns: :class:`~bokeh.colors.HSL` ''' hsl = self.copy() hsl.l = self.clamp(hsl.l + amount, 1) return self.from_hsl(hsl) ColorLike: TypeAlias = str | Color | RGBTuple #----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------