Coverage for src/colorspace/palettes.py: 95%
884 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
3class palette:
4 """Custom Color Palette
6 Allows for the construction of custom (named) color palettes with a fixed
7 set of colors based on hex color inputs (or named matplotlib colors).
9 Args:
10 colors (str, list, colorspace.colorlib.colorobject, LinearSegmentedColormap):
11 One or multiple colors which will make up the custom palette, or a
12 `matplotlib.colors.LinearSegmentedColormap`.
13 name (str): Name of this custom palette. Defaults to `"user_palette"`.
14 Used for object representation/visualization.
15 n (int): int (`>1`), number of colors drawn from an `hclpalette` object.
16 Only taken into account if the object provided on `colors` inherits
17 from `colorspace.palettes.hclpalette`.
19 Returns:
20 An object of class :py:class:`colorspace.palettes.palette`.
22 Example:
24 >>> from colorspace.palettes import palette
25 >>> colors = ["#070707", "#690056", "#C30E62", "#ED8353", "#FDF5EB"]
26 >>> custom_pal = palette(colors, "test palette")
27 >>> custom_pal
28 >>>
29 >>> #: Creating custom palettes based on different input
30 >>> # types (str, list, colorobject)
31 >>> from colorspace.colorlib import hexcols
32 >>> from colorspace import palette
33 >>> hexcols = hexcols(colors)
34 >>>
35 >>> # Creating a series of custom palette objects
36 >>> pal1 = palette("#ff0033") # unnamed
37 >>> pal2 = palette("#ff0033", name = "Custom Palette")
38 >>> pal3 = palette(colors, name = "Custom Palette #3")
39 >>> pal4 = palette(hexcols, name = "Custom Palette #4")
40 >>> print(pal1)
41 >>> #:
42 >>> print(pal2)
43 >>> #:
44 >>> print(pal3)
45 >>> #:
46 >>> print(pal4)
47 >>>
48 >>> #: Palette Swatch Plot
49 >>> from colorspace import swatchplot
50 >>> swatchplot([pal3, pal4], figsize = (5.5, 2.0));
52 Raises:
53 TypeError: If `n` is not int.
54 ValueError: If `n` is `< 2`.
55 TypeError: If `name` is neither `str` nor `None`.
56 """
58 def __init__(self, colors, name = None, n = 7):
60 # Sanity check for input
61 from colorspace.colorlib import colorobject
62 from colorspace.palettes import hclpalette
63 from colorspace import check_hex_colors, palette
65 if not isinstance(n, int):
66 raise TypeError("argument `n` must be int")
67 elif n <= 1:
68 raise ValueError("argument `n` must be > 1")
70 # Trying to load matplotlib to allow 'colors' to be a
71 # LinearSegmentedColormap
72 try:
73 from matplotlib.colors import LinearSegmentedColormap
74 matplotlib_loaded = True
75 except:
76 matplotlib_loaded = False
78 # This is a palette object? Well ...
79 if isinstance(colors, hclpalette):
80 colors = colors.colors(n)
81 # Already a palette object? Simply extract the colors from it.
82 elif isinstance(colors, palette):
83 colors = colors.colors()
84 # If the input inherits from colorspace.colorlib.colorobject we
85 # draw the colors as a hex list.
86 elif isinstance(colors, colorobject):
87 colors = colors.colors()
88 # Matplotlib loaded and 'colors' is a LinearSegmentedColormap
89 elif matplotlib_loaded and isinstance(colors, LinearSegmentedColormap):
90 colors = self._LinearSegmentedColormap_to_colors(colors, n)
92 # Now check if all our colors are valid hex colors
93 self._colors = check_hex_colors(colors)
95 if not isinstance(name, (type(None), str)):
96 raise TypeError("argument `name` must be None or a str")
98 self._name = name
101 def _LinearSegmentedColormap_to_colors(self, x, n):
102 from matplotlib.colors import LinearSegmentedColormap
103 from numpy import linspace
104 from colorspace.colorlib import sRGB
106 assert isinstance(x, LinearSegmentedColormap)
107 assert isinstance(n, int) and n > 1
109 # Evaluate at
110 at = linspace(0.0, 1.0, n)
111 # Get sRGB Coordinates
112 rgb = x(at).transpose()
114 # Create sRGB colorobject, return hex color list
115 return sRGB(R = rgb[0], G = rgb[1], B = rgb[2]).colors()
118 def __len__(self):
119 """Number of Colors
121 Returns:
122 int: Number of colors.
123 """
124 return len(self._colors)
126 def __repr__(self):
127 name = self.name()
128 str = "Palette Name: {:s}\n".format("None" if name is None else name) + \
129 " Type: Custom palette\n" + \
130 " Number of colors: {:d}\n".format(len(self.colors()))
131 return str
133 def rename(self, name):
134 """Rename Custom Palette
136 Allows to set, remplace, or remove the name of a palette.
138 Args:
139 name (None, str): new name for the palette.
141 Raises:
142 ValueError: If input 'name' is not of type str.
144 Examples:
146 >>> from colorspace import palette
147 >>> #: Starting from an unnamed palette
148 >>> pal = palette(["#11C638", "#E2E2E2", "#EF9708"])
149 >>> pal.name() # Returns None
150 >>>
151 >>> #: Naming the palette
152 >>> pal.rename("Custom palette")
153 >>> pal.name()
154 >>>
155 >>> #: Rename
156 >>> pal.rename("Modified palette name")
157 >>> pal.name()
158 >>>
159 >>> #: Unname (replace current name with None)
160 >>> pal.rename(None)
161 >>> pal.name() # Returns None
163 """
164 if not isinstance(name, (type(None), str)):
165 raise ValueError("argument `name` must be None or a str")
166 self._name = name
168 def name(self):
169 """Get Palette Name
171 Returns:
172 Returns `None` if the palette is unnamed, else
173 the name of the palette as `str`.
175 Examples:
176 >>> from colorspace import palette
177 >>> # Unnamed palette
178 >>> pal = palette(["#11C638", "#E2E2E2", "#EF9708"])
179 >>> pal.name()
180 >>> #:
181 >>> type(pal.name())
182 >>> #: Named palette
183 >>> pal = palette(["#11C638", "#E2E2E2", "#EF9708"],
184 >>> name = "My Custom Palette")
185 >>> pal.name()
186 """
187 return self._name
189 def colors(self, *args, **kwargs):
190 """Get Palette Colors
192 Returns the colors of the current palette as a list
193 of hex colors (`str`).
195 Args:
196 *args: Ignored.
197 **kwargs: Ignored.
199 Returns:
200 list: List of all colors of the palette.
202 Examples:
203 >>> from colorspace import palette
204 >>> pal = palette(["#11C638", "#E2E2E2", "#EF9708"],
205 >>> name = "My Custom Palette")
206 >>> pal.colors()
207 """
208 return self._colors
210 def swatchplot(self, **kwargs):
211 """Palette Swatch Plot
213 Interfacing the main :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`
214 function. Plotting the spectrum of the current color palette.
216 Args:
217 **kwargs: forwarded to :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`.
218 Note that `show_names` will always be set to `False`.
220 Return:
221 Returns what :py:func:`colorspace.swatchplot.swatchplot` returns.
223 Example:
225 >>> from colorspace import palette
226 >>> pal = palette(["#FCFFC9", "#E8C167", "#D67500", "#913640", "#1D0B14"],
227 >>> name = "Custom Palette")
228 >>> pal.swatchplot()
229 >>> pal.swatchplot(figsize = (5, 1))
230 """
232 from .swatchplot import swatchplot
233 if "show_names" in kwargs.keys():
234 del kwargs["show_names"]
235 return swatchplot(pals = self.colors(), show_names = False, **kwargs)
237 def specplot(self, *args, **kwargs):
238 """Color Spectrum Plot
240 Interfacing the :py:func:`colorspace.specplot.specplot` function.
241 Plotting the spectrum of the current color palette.
243 Args:
244 *args: Forwarded to :py:func:`colorspace.specplot.specplot`.
245 **kwargs: Forwarded to :py:func:`colorspace.specplot.specplot`.
247 Return:
248 Returns what :py:func:`colorspace.specplot.specplot` returns.
250 Example:
252 >>> from colorspace import palette, diverging_hcl
253 >>> # Default diverging HCL palette
254 >>> pal = palette(diverging_hcl().colors(7))
255 >>> pal.specplot()
256 >>> pal.specplot(rgb = False)
257 """
259 from .specplot import specplot
260 return specplot(self.colors(), *args, **kwargs)
263 def hclplot(self, **kwargs):
264 """Palette Plot in HCL Space
266 Internally calls :py:func:`hclplot <colorspace.hclplot.hclplot>`,
267 additional arguments to this main function can be forwarded via the
268 `**kwargs` argument.
270 Args:
271 **kwargs: Additional named arguments forwarded to
272 :py:func:`hclplot <colorspace.hclplot.hclplot>`.
274 Return:
275 Returns what :py:func:`colorspace.hclplot.hclplot` returns.
277 Example:
279 >>> from colorspace import palette, diverging_hcl
280 >>> pal = palette(diverging_hcl().colors(7))
281 >>> pal.hclplot()
282 """
284 from .hclplot import hclplot
285 return hclplot(x = self.colors(), **kwargs)
288 def cmap(self, continuous = True):
289 """Create Matplotlib Compatible Color Map
291 Converts the current palette into a
292 `matplotlib.colors.LinearSegmentedColormap` color map based on the
293 colors provided creating this palette object. If `continuous = True`
294 a series of `256` unique colors will be created using linear
295 interpolation in the standard RGB color space. If `continuous = False`
296 the resulting color map is solely based on the number of colors of
297 the palette which yields a non-continuous color map with step-functions
298 in R, G, and B (see Example).
300 Args:
301 continuous (bool): If `True` (default) the resulting colormap
302 will contain 256 colors, linearely interpolated in between
303 the colors of the palette. If `False`, only the `N` colors
304 of the palette are used (see Examples).
306 Return:
307 matplotlib.colors.LinearSegmentedColormap: Colormap to be used with
308 matplotlib.
310 Example:
312 >>> from colorspace import diverging_hcl, palette, specplot
313 >>> pal = diverging_hcl()
314 >>> pal = palette(pal(5), name = "Diverging Palette with 5 Colors")
315 >>>
316 >>> # Continuous colormap
317 >>> cmap1 = pal.cmap(continuous = True)
318 >>> cmap1.N # Internal number of colors
319 >>>
320 >>> #: Non-continuous version of the colormap
321 >>> cmap2 = pal.cmap(continuous = False)
322 >>> cmap2.N # Internal number of colors
323 >>>
324 >>> #: Using helper function for demonstration
325 >>> specplot(cmap1, rgb = True, figsize = (8, 6));
326 >>> #:
327 >>> specplot(cmap2, rgb = True, figsize = (8, 6));
329 Raises:
330 TypeError: If `continuous` is not bool
331 """
333 from matplotlib.colors import LinearSegmentedColormap
335 if not isinstance(continuous, bool):
336 raise TypeError("argument `continuous` must be bool")
338 # Create colormap
339 n = 256 if continuous else len(self.colors())
340 cmap = LinearSegmentedColormap.from_list(self.name(), self.colors(), n)
341 return cmap
345# -------------------------------------------------------------------
346# -------------------------------------------------------------------
347class defaultpalette:
348 """Pre-defined HCL Color Palettes
350 Object for handling the pre-defined HCL-based color palettes, not intended
351 to be used by the users.
353 Args:
354 type (str): Type of palette.
355 method (str): Name of the method which has to be called to retrieve colors
356 (e.g., :py:class:`colorspace.palettes.diverging_hcl`).
357 name (str): Name of the color palette.
358 settings (dict): Dictionary containing the parameter settings.
360 Returns:
361 Object of class `colorspace.palettes.defaultpalette`.
362 """
364 def __init__(self, type, method, name, settings):
366 self._type_ = type
367 self._name_ = name
368 self._method_ = method
369 self._settings_ = settings
371 # Default representation of defaultpalette objects.
372 def __repr__(self):
373 """Standard Representation
375 Prints the current settings on stdout.
376 Development method."""
378 res = []
379 res.append("Palette Name: {:s}".format(self.name()))
380 res.append(" Type: {:s}".format(self.type()))
381 res.append(" Inspired by: {:s}".format(self.get("desc")))
382 #for key,val in self._settings_.items():
383 keys = list(self.get_settings().keys())
384 keys.sort()
385 for key in keys:
386 if key in ["desc"]: continue
387 val = self.get(key)
388 if isinstance(val, bool): val = " True" if val else "False"
389 elif isinstance(val, int): val = "{:5d}".format(val)
390 elif isinstance(val, float): val = "{:5.1f}".format(val)
391 elif callable(val): val = "{:s}".format(str(val))
392 res.append(" {:10s} {:s}".format(key,val))
394 return "\n".join(res)
396 def __call__(self, n = 11):
397 """Get Colors
399 Wrapper function for :py:func:`colors`.
401 Args:
402 n (int): Number of colors, defaults to 7.
404 Returns:
405 list: List of hex colors.
406 """
407 return self.colors(n)
409 def method(self):
410 """Get Construction Method
412 Returns method used to create this palette.
414 Returns:
415 Returns the method (`str`, name of the function to be called
416 to create the palette) of the palette.
417 """
418 return self._method_
420 def type(self):
421 """Get Palette Type
423 Get type of the color palette.
425 Returns:
426 Returns the type (`str`) of the palette.
427 """
428 return self._type_
430 def name(self):
431 """Get Palette Name
433 Get name of color palette.
435 Returns:
436 str: Returns the name of the palette.
437 """
438 return self._name_
440 def rename(self, name):
441 """Rename Palette
443 Allows to rename the palette.
445 Args:
446 name (str): New palette name.
447 """
448 self._name_ = name
450 def get(self, what):
451 """Get Specific Palette Settings
453 Allows to load the settings of the palette for the different parameters
454 (e.g., `h1`, `h2`, ...). Returns `None` if the parameter does not
455 exist. Another method (:py:func:`set`) allows to set the parameters.
457 Args:
458 what (str): Name of the parameter which should be extracted and
459 returned from the settings of this color palette.
461 Returns:
462 Returns `None` if the parameter `what` cannot be found,
463 else the value of the parameter `what` is returned.
464 """
465 if what in self._settings_.keys():
466 return self._settings_[what]
467 else:
468 return None
471 def set(self, lambda_allowed = False, **kwargs):
472 """Set Specific Palette Settings
474 Allows to set/overwrite color palette parameters (e.g., `h1`, `h2`,
475 ...). Another method (:py:func:`get`) allows to retrieve the
476 parameters.
478 Args:
479 lambda_allowed (bool): Defaults to `False`, for qualitative palettes this
480 is set `True` for `h2`.
481 **kwargs: A set of named arguments (`key = value` pairs) where the key
482 defines the parameter which should be overruled, value the
483 corresponding value. Allowed value types are bool, int, and float.
484 """
485 if not isinstance(lambda_allowed, bool):
486 ValueError("argument `lambda_allowed` must be bool")
488 for key,val in kwargs.items():
489 # Float or integer? Default case; passing forward
490 if isinstance(val, (float, int)):
491 pass
492 # If key == h2 and val is callable, we do allow this (for qualitative palettes)
493 # if lambda_allowed is set True; passing foward as well
494 elif key == "h2" and lambda_allowed and callable(val):
495 pass
496 # Else check current type specification and append
497 # if possible (and convert to the new type).
498 elif not isinstance(val, (int, float, bool)):
499 raise ValueError(f"argument `{key}` to {self.__class__.__name__}" + \
500 f" is of type {type(val)}; only bool, int, and float allowed.")
501 elif isinstance(val, bool):
502 val = 1 if val else 0
503 else:
504 raise Exception(f"whoops, no rule yet to handle {key} = {val}")
506 # Not yet a parameter in our dictionary? Add as float
507 if not key in self._settings_.keys():
508 self._settings_[key] = float(val)
509 # If already existing we convert the new value into the existing type.
510 elif isinstance(self._settings_[key], int):
511 self._settings_[key] = int(val)
512 # If setitng exists and is float, int, or a lambda function: replace
513 # If val is itself a callable function, take it as is. Else convert to float.
514 elif isinstance(self._settings_[key], (int, float)) or callable(self._settings_[key]):
515 self._settings_[key] = val if callable(val) else float(val)
516 else:
517 raise Exception(f"whoops, some code needed here in {self.__class__.__name__}.set")
520 def get_settings(self):
521 """Get All Palette Settings
523 Allows to get the current settings of the palette object.
524 To retrieve single parameters use :py:func:`get`.
526 Returns:
527 Returns a `dict` object with all parameter specification of this
528 palette.
529 """
530 return self._settings_
532 def colors(self, n = 11):
533 """Get Colors
535 Load a set of `n` colors from this palette. This method evaluates
536 the `method` argument to generate a set of hex colors which will be
537 returned. Please note that it is possible that none-values will be
538 returned if the fixup-setting is set to `False` (see
539 :py:class:`colorlib.hexcols`).
541 Args:
542 n (int): Number of colors to be returned, defaults to 11.
544 Returns:
545 list: Returns a list of str with `n` colors from the palette.
546 """
548 # Dynamically load color function
549 mod = __import__("colorspace")
550 cfun = getattr(mod, self._method_)
552 # Calling color method with arguments of this object.
553 from copy import copy
554 args = copy(self.get_settings())
556 pal = cfun(**args)
557 return pal.colors(n, fixup = True)
560# -------------------------------------------------------------------
561# -------------------------------------------------------------------
562class hclpalettes:
563 """Prepare Pre-defined HCL Palettes
565 Prepares the pre-specified hclpalettes. Reads the config files and creates
566 a set of `defaultpalette` objects.
568 See also: :py:func:`divergingx_palettes <colorspace.hcl_palettes.divergingx_palettes>`.
570 Args:
571 files (None, str list): If `None` (default) the default color palette
572 configuration from within the package will be loaded. A path to a custom
573 config file (str) or a list of paths can be provided to read custom
574 palettes.
575 files_regex (None, str): Additional regular expression to filter files.
576 Only used if `files = None`.
578 Return:
579 hclpalettes: Collection of predefined hcl color palettes.
581 Examples:
583 >>> from colorspace import hclpalettes
584 >>> hclpals = hclpalettes()
585 >>> hclpals
586 >>> #: Palette swatch plots with 5 colors each
587 >>> hclpals.plot(n = 5);
588 >>> #: Palette swatch plots with 11 colors each
589 >>> hclpals.plot(n = 11);
590 """
591 def __init__(self, files = None, files_regex = None):
593 from os.path import dirname, join, isfile
595 if not isinstance(files, (type(None), str, list)):
596 raise TypeError("argument `files` must either be None, str, or list of str")
597 if isinstance(files, str): files = [files]
598 if isinstance(files, list):
599 for file in files:
600 if not isinstance(file, str):
601 raise TypeError("not all elements in `files` are of type str")
603 if files is None:
604 import glob
605 resource_package = dirname(__file__)
606 tmp = glob.glob(join(resource_package, "palconfig", "*.conf"))
607 # Ensure files_regex is of appropriate type
608 if not isinstance(files_regex, (str, type(None))):
609 raise TypeError("argument `filex_regex` must be None or str")
610 if files_regex:
611 from re import match
612 files = []
613 for f in tmp:
614 if match(files_regex, f): files.append(f)
615 else:
616 files = tmp
618 # Input 'files' specified:
619 if not len(files) > 0:
620 raise ValueError(f"no palette config files provided ({self.__class__.__name__})")
621 for file in files:
622 if not isfile(file):
623 raise FileNotFoundError(f"file \"{file}\" does not exist")
626 # Else trying to load palettes and append thenm to _palettes_
627 self._palettes_ = {}
628 for file in files:
629 [palette_type, pals] = self._load_palette_config_(file)
630 if pals: self._palettes_[palette_type] = pals # append
632 # A poor attempt to order the palettes somehow
633 x = list(self._palettes_.keys())
634 x.sort(reverse = True)
635 tmp = {}
637 for rec in x: tmp[rec] = self._palettes_[rec]
638 self._palettes_ = tmp
641 def __repr__(self):
642 """Standard Representation
644 Standard representation of the object."""
645 res = ["HCL palettes"]
647 for type_ in self.get_palette_types():
648 res.append("") # Blank line
649 res.append("Type: {:s}".format(type_))
650 nchar = 0
651 for pal in self.get_palettes(type_):
652 # Initialize new line
653 if nchar == 0:
654 tmp = "Names: {:s}".format(pal.name())
655 nchar = len(tmp)
656 elif (len(pal.name()) + nchar) > 60:
657 res.append(tmp)
658 tmp = " {:s}".format(pal.name())
659 nchar = len(tmp)
660 # Append
661 else:
662 tmp += ", {:s}".format(pal.name())
663 nchar += len(pal.name()) + 2
664 res.append(tmp)
666 return "\n".join(res)
669 def get_palette_types(self):
670 """Get Palette Types
672 Get all palette types.
674 Returns:
675 list: Returns a `list` of str with the names of all palette types
676 or groups.
677 """
679 return list(self._palettes_.keys())
681 def get_palettes(self, type_ = None, exact = False):
682 """Get Type-Specific Palettes
684 Get all palettes of a specific type.
686 Args:
687 type_ (None, str): (Partial) Name of the palettes which should be returned.
688 String matching is used; partial matches are allowed.
689 If set to `None` (default) all palettes will be returned. Names
690 have to match but are not case sensitive, defaults to None.
691 exact (bool): If `False` (default) partial matching is used. If `True`,
692 `type_` must be an exact match (case sensitive).
694 Examples:
695 >>> # Initialize hclpalettes object
696 >>> from colorspace import hclpalettes
697 >>> hclpals = hclpalettes()
698 >>>
699 >>> # Get all Diverging palettes
700 >>> pals1 = hclpals.get_palettes("Diverging")
701 >>> len(pals1)
702 >>>
703 >>> #: Get 'Advanced: Diverging' palettes, also includes
704 >>> # 'Advanced: DivergingX' (partial match).
705 >>> pals2 = hclpals.get_palettes("Advanced: Diverging")
706 >>> len(pals2)
707 >>>
708 >>> #: Only get 'Advanced: Diverging' (exact match)
709 >>> pals3 = hclpals.get_palettes("Advanced: Diverging", exact = True)
710 >>> len(pals3)
711 >>> #:
712 >>> pals3
714 Returns:
715 Returns a `list` containing `defaultpalette` objects objects.
717 Raises:
718 TypeError: If `type_` is not str or None.
719 TypeError: If `exact` is not bool.
720 ValueError: If no matching palette is found.
721 """
722 if not isinstance(type_, (str, type(None))):
723 raise TypeError("argument `type_` must be str or None")
724 if not isinstance(exact, bool):
725 raise TypeError("argument `exact` must be bool.")
727 # Return all available palettes
728 if not type_:
729 res = []
730 for key,pals in self._palettes_.items(): res += pals
732 # Else find matching palettes (given type_). Either exact matches or
733 # partial matches depending on `exact`.
734 else:
735 from re import compile, IGNORECASE, escape
736 if not exact:
737 pattern = compile(f".*?{escape(type_)}.*?", IGNORECASE)
738 else:
739 pattern = compile(f"^{escape(type_)}$")
741 # Searching trough available palettes
742 res = []
743 for t in self._palettes_.keys():
744 if pattern.match(t):
745 res += self._palettes_[t]
747 # No palettes found? Raise ValueError
748 if len(res) == 0:
749 raise ValueError(f"no palettes for type \"{type_}\" ({'exact' if exact else 'partial'} match)")
751 # Else return list with palettes
752 return res
754 def get_palette(self, name):
755 """Get Palette by Name
757 Get a palette with a specific name.
759 Args:
760 name (str): Name of the color palette which should be returned. Not
761 case sensitive; blanks are ignored (removed).
763 Returns:
764 Returns an object of class `defaultpalette` if a palette with
765 the name as specified can be found. Else an error will be dropped.
766 """
768 # Try to find the palette with the name 'name'
769 take_pal = None
770 for type_,pals in self._palettes_.items():
771 # Looping over palettes
772 for pal in pals:
773 if pal.name().upper().replace(" ", "") == name.upper().replace(" ", ""):
774 take_pal = pal
775 break;
776 # If already found: break outer loop
777 if take_pal: break;
779 # If none: not found
780 if take_pal is None:
781 raise ValueError(f"palette \"{name}\" not found")
783 # Else return list with palettes
784 return take_pal
787 # Helper method to load the palette config files.
788 def _load_palette_config_(self, file):
790 import re
791 import sys
792 from configparser import ConfigParser
794 CNF = ConfigParser()
795 CNF.read(file)
797 # Reading type (or name)
798 try:
799 palette_type = CNF.get("main", "type")
800 palette_method = CNF.get("main", "method")
801 except Exception as e:
802 raise Exception(f"misspecification in palconfig file = \"{file}\": {str(e)}")
804 # The dictionary which will be returned.
805 pals = []
807 # Looping over all sections looking for palette specifications.
808 for sec in CNF.sections():
809 mtch = re.match("^palette\\s+(.*)$", sec)
810 if not mtch: continue
812 # Extracting palette name from section name
813 name = mtch.group(1).strip()
815 # Loading all available setting elements.
816 # "desc": interpreted as character
817 # "p1/p1": interpreted as float
818 # "fixup": interpreted as bool
819 # rest: interpreted as int
820 settings = {}
821 for key,val in CNF.items(sec):
822 key = key.lower()
823 if key in ["desc"]:
824 settings[key] = val
825 elif key in ["fixup"]:
826 settings[key] = True if int(val) else False
827 elif key in ["p1","p2", "p3", "p4"]:
828 settings[key] = float(val)
829 elif key in ["h1", "h2"]:
830 if re.match("^-?[0-9\\.]+$", val):
831 settings[key] = int(val)
832 else:
833 # Try to evaluate this as a lambda function.
834 try:
835 val = eval(val)
836 if not callable(val): raise Exception
837 except:
838 raise ValueError(f"element '{key}' for palette '{sec}' neither an int nor proper lambda function")
839 # Append lambda function to the settings
840 settings[key] = val
842 else:
843 settings[key] = int(val)
845 pals.append(defaultpalette(palette_type, palette_method, name, settings))
847 # Return dictionary with palettes
848 if len(pals) == 0:
849 return [None, None]
850 else:
851 return [palette_type, pals]
854 def length(self):
855 """Get Number of Palettes
857 Get length of palette.
859 Returns:
860 int: Integer, number of palettes in the object.
861 """
863 npals = 0
864 for type_ in self.get_palette_types():
865 for pal in self.get_palettes(type_): npals += 1
867 return npals
870 def plot(self, n = 5):
871 """Palette Swatch Plot
873 Interfacing the main :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`
874 function. Plotting the spectrum of the current color palette.
876 Args:
877 n (int): Number of colors, defaults to 7.
878 """
879 if not isinstance(n, int): raise TypeError("argument `n` must be int")
880 if not n > 0: raise ValueError("argument `n` must be positive")
882 from .swatchplot import swatchplot
883 swatchplot(self, n = n)
886# -------------------------------------------------------------------
887# -------------------------------------------------------------------
888class hclpalette:
889 """HCL Palette Superclass
891 Hy, I am the base class. Is extended by the different HCL based color
892 palettes such as the classes :py:class:`diverging_hcl`, :py:class:`qualitative_hcl`,
893 :py:class:`rainbow_hcl`, :py:class:`sequential_hcl`, and maybe even more in the future.
894 """
896 # Default call: return n hex colors
897 def __call__(self, *args, **kwargs):
898 """__call__(*args, **kwargs)
900 Call interface, calls objects `colors(...)` method.
902 Args:
903 *args: Unnamd arguments forwarded to the color method.
904 **kwargs: Named arguments forwarded to the color method.
906 Returns:
907 See colors method.
908 """
909 return self.colors(*args, **kwargs)
911 def specplot(self, n = 180, *args, **kwargs):
912 """Color Spectrum Plot
914 Interfacing the :py:func:`colorspace.specplot.specplot` function.
915 Plotting the spectrum of the current color palette.
917 Args:
918 n (int): Number of colors, defaults to 180.
919 *args: Forwarded to :py:func:`colorspace.specplot.specplot`.
920 **kwargs: Forwarded to :py:func:`colorspace.specplot.specplot`.
922 Return:
923 Returns what :py:func:`colorspace.specplot.specplot` returns.
925 Example:
927 >>> # Default diverging HCL palette
928 >>> from colorspace import diverging_hcl
929 >>> pal = diverging_hcl()
930 >>> pal.specplot()
931 >>> #:
932 >>> pal.specplot(rgb = True)
933 >>>
934 >>> #: Default sequential HCL palette
935 >>> from colorspace import sequential_hcl
936 >>> pal = sequential_hcl()
937 >>> pal.specplot(figsize = (8, 4))
938 >>>
939 >>> #: Default qualitative HCL palette
940 >>> from colorspace import qualitative_hcl
941 >>> pal = qualitative_hcl()
942 >>> pal.specplot(figsize = (8, 4), hcl = False, rgb = True)
943 """
945 from .specplot import specplot
946 return specplot(self.colors(n), *args, **kwargs)
948 def swatchplot(self, n = 7, **kwargs):
949 """Palette Swatch Plot
951 Interfacing the main :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`
952 function. Plotting the spectrum of the current color palette.
954 Args:
955 n (int): Number of colors, defaults to 7.
956 **kwargs: forwarded to :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`.
958 Return:
959 Returns what :py:func:`colorspace.swatchplot.swatchplot` returns.
961 Example:
963 >>> # Exemplarily for diverging_hcl, works for
964 >>> # all other HCL palettes as well.
965 >>> from colorspace import diverging_hcl
966 >>> pal = diverging_hcl()
967 >>> pal.swatchplot(figsize = (8, 2))
968 >>> #: Handing over a series of additional arguments
969 >>> # forwarded to swatchplot()
970 >>> pal.swatchplot(n = 21, figsize = (8, 2),
971 >>> show_names = False, cvd = "deutan")
972 >>> #:
973 >>> pal.swatchplot(n = 21, figsize = (8, 2),
974 >>> show_names = False,
975 >>> cvd = ["protan", "deutan", "tritan", "desaturate"])
976 """
978 from .swatchplot import swatchplot
979 return swatchplot(self.colors(n), **kwargs)
982 def hclplot(self, n = 7, **kwargs):
983 """Palette Plot in HCL Space
985 Internally calls :py:func:`hclplot <colorspace.hclplot.hclplot>`,
986 additional arguments to this main function can be forwarded via the
987 `**kwargs` argument.
989 Args:
990 n (int): Number of colors, defaults to 7.
991 **kwargs: Additional named arguments forwarded to
992 :py:func:`hclplot <colorspace.hclplot.hclplot>`.
994 Return:
995 Returns what :py:func:`colorspace.hclplot.hclplot` returns.
997 Example:
999 >>> from colorspace import diverging_hcl
1000 >>> pal = diverging_hcl()
1001 >>> pal.hclplot()
1002 >>> pal.hclplot(n = 11)
1003 """
1005 from .hclplot import hclplot
1006 return hclplot(x = self.colors(n), **kwargs)
1008 def name(self):
1009 """Get Palette Name
1011 Get name (generic) of color palette.
1013 Returns:
1014 str: Returns the name of the palette.
1016 Examples:
1017 >>> from colorspace import *
1018 >>> pal1 = diverging_hcl()
1019 >>> pal1.name()
1020 >>> #:
1021 >>> pal2 = sequential_hcl("ag_Sunset")
1022 >>> pal2.name()
1023 >>> #:
1024 >>> pal3 = heat_hcl()
1025 >>> pal3.name()
1026 >>> #:
1027 >>> pal4 = sequential_hcl("Rocket")
1028 >>> pal4.name()
1029 """
1030 return self._name
1032 def get(self, key):
1033 """Get Specific Palette Setting
1035 Returns one specific item of the palette settings,
1036 e.g., the current value for `h1` or `l2`.
1037 If not existing a `None` will be returned.
1039 Args:
1040 key (str): Name of the setting to be returned.
1042 Returns:
1043 None if `key` does ont exist, else the current value will be
1044 returned.
1046 Example:
1048 >>> # Exemplarily for rainbow_hcl (works for the
1049 >>> # other HCL palettes as well)
1050 >>> from colorspace import rainbow_hcl
1051 >>> a = rainbow_hcl()
1052 >>> a.get("h1")
1053 >>> #:
1054 >>> a.get("c1")
1055 >>> #:
1056 >>> a.get("l1")
1057 >>> #:
1058 >>> a.get("not_defined")
1059 """
1060 if not key in self.settings.keys():
1061 return None
1062 return self.settings[key]
1064 def show_settings(self):
1065 """Show Palette Settings
1067 Shows the current settings (table like print to stdout). Should more be
1068 seen as a development method than a very useful thing.
1070 Example:
1072 >>> from colorspace.palettes import rainbow_hcl
1073 >>> a = rainbow_hcl(10)
1074 >>> a.show_settings()
1075 """
1077 def get(key):
1078 val = self.get(key)
1079 if val is None:
1080 return f"{key:7s} ---"
1081 elif callable(val):
1082 return f"{key:7s} <lambda>"
1083 elif isinstance(val, bool):
1084 return f"{key:7s} {str(val):>8s}"
1085 elif isinstance(val, int):
1086 return f"{key:7s} {val:8d}"
1087 else:
1088 return f"{key:7s} {val:8.1f}"
1090 from .palettes import divergingx_hcl
1092 if not isinstance(self, divergingx_hcl):
1093 keys = ["h1", "h2", "c1", "cmax", "c2", "l1", "l2", "p1", "p2", "fixup"]
1094 else:
1095 keys = ["h1", "h2", "h3", "c1", "cmax1", "c2", "cmax2", "c3",
1096 "l1", "l2", "l3", "p1", "p2", "p3", "p4", "fixup"]
1098 print(f"Class: {self.__class__.__name__}")
1099 for k in keys: print(get(k))
1102 # Better input handling
1103 def _checkinput_(self, dtype, length_min = None, length_max = None,
1104 recycle = False, nansallowed = False, **kwargs):
1105 """Used to check/convert/extend input arguments to the palette functions.
1107 Args:
1108 dtype (object): E.g. int or float, the type in which the inputs
1109 should be converted.
1110 length_min (None, int): Optional. Minimum length of the input data.
1111 If not fulfilled and `recycle` is set to True it expands the
1112 input to `length_min`. Defaults to None, see also `length_max`.
1113 lenth_max (None, int): Optional. Maximum length of the input data.
1114 If longer, the script will stop. If not set (default is `None`)
1115 only the `length_min` will be checked.
1116 recycle (bool): if set to `True` the user inputs will be recycled
1117 to match the expected number of inputs, defaults to False.
1118 nansallowed (bool): if False an error will be raised if the final
1119 arguments contain `numpy.nan` values. Else `numpy.nan`s are
1120 passed trough and will be returned, defaults to False.
1121 **kwargs: List of named arguments, the ones to be checked. If only
1122 one is given the function returns the values of this specific
1123 input. If multiple arguments are handed over a dict will be
1124 returned with the names corresponding to the user input.
1126 Returns:
1127 If `kwargs` is of length one the values of this specific variable
1128 will be returned. If multiple `kwargs` arguments are set a dict is
1129 returned. Note that `None` will simply stay `None`. The function
1130 raises errors if the user inputs do not match the required
1131 specifications.
1132 """
1134 # Support function
1135 def fun(key, value, dtype, length_min, length_max, recycle, nansallowed):
1137 from numpy import vstack, ndarray, asarray, isnan, nan, any, atleast_1d
1139 if not nansallowed and any(isnan(value)):
1140 raise ValueError(f"nan's not allowed in `{key}`")
1141 elif isinstance(value, ndarray) and len(value) < length_min:
1142 raise ValueError(f"argument `{key}` too short (< {length_min})")
1144 # If None
1145 if value == None: return value
1147 # Converting the data
1148 try:
1149 value = asarray([value], dtype = dtype).flatten()
1150 except Exception as e:
1151 raise ValueError(f"incorrect input on argument `{key}`: {str(e)}")
1153 # Vector of length 0?
1154 if len(value) == 0:
1155 raise ValueError(f"argument `{key}` of length 0")
1157 # Not enough input values, check if we are allowed to
1158 # recycle.
1159 if length_min and len(value) < length_min:
1160 # Input was too short: check if we are allowed to
1161 # recycle the value or not.
1162 if not recycle:
1163 raise ValueError(f"wrong length of input \"{key}\", expected min {length_min} elements, " + \
1164 f"got {len(value)} when calling {self.__class__.__name__}")
1165 else:
1166 value = vstack([value] * length_min).flatten()[0:length_min]
1167 elif length_min and not length_max and len(value) > length_min:
1168 value = value[0:length_min]
1170 # Check if the input exceeds length_max if set
1171 if length_max and len(value) > length_max:
1172 raise ValueError(f"wrong length of input \"{key}\", expected max {length_max} elements, " + \
1173 f"got {len(value)} when calling {self.__class__.__name__}")
1174 # Cropping data if too much elements are given.
1175 elif length_max and len(value) > length_max:
1176 value = value[0:length_max]
1178 # Checking nan's
1179 if not nansallowed and any(isnan(value)):
1180 raise ValueError(f"arguments for \"{key}\" to function calling {self.__class__.__name__}" + \
1181 "contain nan values: not allowed")
1183 # Return single value if length is set to 1.
1184 if len(value) == 1: value = value[0]
1186 return atleast_1d(value) # Return 1d array
1188 # Looping over all kwargs
1189 for key,value in kwargs.items():
1190 if value is None: raise ValueError(f"argument `{key}` cannot be None")
1191 kwargs[key] = fun(key, value, dtype, length_min, length_max, recycle, nansallowed)
1193 # If only one kwarg was given: return values, else return dict.
1194 if len(kwargs) == 1:
1195 return kwargs[list(kwargs.keys())[0]]
1196 else:
1197 return kwargs
1200 # Return matplotlib.colors.LinearSegmentedColormap
1201 def cmap(self, n = 256, name = "custom_hcl_cmap"):
1202 """Create Matplotlib Compatible Color Map
1204 Allows to retrieve a matplotlib LinearSegmentedColormap color map.
1205 Clasically LinearSegmentedColormaps allow to retrieve a set of `N`
1206 colors from a set of `n` colors where `N >> n`. The matplotlib
1207 simply linearely interpolates between all `n` colors to extend
1208 the number of colors to `N`.
1210 In case of `hclpalette` objects this is not necessary as
1211 `hclpalette` objects allow to retrieve `N` colors directly
1212 along well-specified Hue-Chroma-Luminance paths. Thus, this method
1213 returns a matplotlib color map with `n = N` colors. The linear
1214 interpolation between the colors (as typically done by
1215 LinearSegmentedColormap) is not necessary. However, for convenience
1216 cmaps have been implemented such that you can easily use hcl based
1217 palettes in your existing workflow.
1219 Args:
1220 n (int): Number of colors the cmap should be based on; default is `n = 256`
1221 name (str): Name of the custom color map. Default is `custom_hcl_cmap`
1223 Example:
1225 >>> # Create LinearSegmentedColormap from diverging_hcl() palette.
1226 >>> # By default, 256 distinct colors are used across the palette.
1227 >>> from colorspace import diverging_hcl, specplot
1228 >>> pal = diverging_hcl()
1229 >>> cmap1 = pal.cmap()
1230 >>> cmap1.N
1231 >>>
1232 >>> #: Same as above, but only using 5 distinct colors.
1233 >>> cmap2 = pal.cmap(n = 5)
1234 >>> cmap2.N
1235 >>>
1236 >>> #: Plotting HCL and sRGB spectrum for both cmaps
1237 >>> specplot(cmap1, rgb = True, figsize = (8, 6));
1238 >>> #:
1239 >>> specplot(cmap2, rgb = True, figsize = (8, 6));
1241 Returns:
1242 Returns a `LinearSegmentedColormap` (cmap) to be used
1243 with the matplotlib library.
1245 Raises:
1246 TypeError: If `n` is not int
1247 ValueError: If `n` is lower than 2
1248 """
1249 import matplotlib
1250 from matplotlib.colors import LinearSegmentedColormap
1251 from numpy import linspace, round, fmin, fmax
1253 if not isinstance(n, int):
1254 raise TypeError("argument `n` must be int")
1255 elif n < 2:
1256 raise ValueError("argument `n` must be >= 2")
1258 cols = self.colors(n)
1259 return LinearSegmentedColormap.from_list(name, cols, n)
1262 def _set_rev(self, rev):
1263 """Helper function: Store 'rev' argument
1265 Args:
1266 rev (bool): Should the palette be reversed?
1268 Raises:
1269 TypeError: If argument `rev` is not bool.
1270 """
1271 if not isinstance(rev, bool):
1272 raise TypeError("argument `rev` must e bool")
1273 self._rev = rev # Just store it
1275 def _get_alpha_array(self, alpha, n):
1276 """Get numpy.ndarray for alpha values
1278 The .color() method allowes to specify an additonal alpha
1279 channel, a value between 0. (fully opaque) to 1. (fully transparent)
1280 which can be provided in different ways.
1282 Args:
1283 alpha (None, float, list, or numpy.ndarray): Can be `None`
1284 (default), a single float, a list, or a numpy array. If a list or
1285 array is provided it must be of length 1 or of length `n` and be
1286 convertible to float, providing values between `0.0` (full opacity)
1287 and `1.0` (full transparency)
1288 n (int): Number of colors (must be > 0).
1289 Raises:
1290 TypeError: If `n` is not int.
1291 ValueError: If `n` is not > 0.
1292 TypeError: If `alpha` is not among the allowed types.
1293 ValueError: If `alpha` is a numpy.array of length > 1 but not of length n.
1295 Return:
1296 None, numpy.ndarray: Returns None (if input alpha is None) or a numpy
1297 numeric numpy array of length 'n'.
1298 """
1299 import numpy as np
1301 if not isinstance(n, int):
1302 raise TypeError("argument `n` must be int")
1303 elif n <= 0:
1304 raise ValueError("argument `n` (int) must larger than 0")
1306 # checking alpha
1307 if not isinstance(alpha, (type(None), float, list, np.ndarray)):
1308 raise TypeError("argument `alpha` not among the allowed types")
1309 elif isinstance(alpha, (list, np.ndarray)) and len(alpha) == 0:
1310 raise ValueError("argument `alpha` is of length 0 (not allowed)")
1312 # If alpha is None, we can return immediately
1313 if alpha is None: return None
1315 # If alpha is a numpy array of length > 0, but not equal to n, error.
1316 if isinstance(alpha, np.ndarray) and len(alpha) > 1 and not len(alpha) == n:
1317 raise ValueError("if `alpha` is a numpy array, it must be of the same length as `n`")
1318 elif isinstance(alpha, list) and not len(alpha) == n:
1319 raise ValueError("if `alpha` is a list, it must be of the same length as `n`")
1320 elif isinstance(alpha, float):
1321 alpha = np.repeat(alpha, n)
1322 elif len(alpha) == 1:
1323 try:
1324 alpha = np.repeat(alpha[0], n)
1325 alpha = alpha.dtype("float")
1326 except Exception as e:
1327 raise Exception(f"problems converting alpha: {e}")
1328 else:
1329 try:
1330 alpha = np.asarray(alpha, dtype = "float")
1331 except Exception as e:
1332 raise Exception(f"problems converting alpha: {e}")
1334 # Now we know we have a float array
1335 if np.any(alpha < 0) or np.any(alpha > 1):
1336 raise ValueError("values `alpha` must be in the range of [0.0, 1.0]")
1338 # Returning numpy.ndarray
1339 return alpha
1341 def _chroma_trajectory(self, i, p1, c1, c2, cmax):
1342 """Helper function: Calculate linear or triangle trajectory for chroma dimension.
1344 Args:
1345 i (numpy array; float): Position across the palette, a sequence
1346 of values between 1 and 0. For diverging palettes this function
1347 is called twice, once for 1 to 0.5, and once for <0.5 t0 0.
1348 p1 (float): Power parameter p1.
1349 c1 (float): Chroma value of the left end of the color palette.
1350 c2 (float): Chroma value of the right end of the color palette.
1351 cmax (float, None, np.nan): Max choma value.
1353 Returns:
1354 numpy array: Linear trajectory for the chroma color dimension.
1355 """
1356 from numpy import isnan
1358 def _linear_trajectory(i, c1, c2):
1359 return c2 - (c2 - c1) * i
1361 def _triangle_trajectory(i, j, c1, c2, cmax):
1362 from numpy import where, abs, linspace
1363 res = where(i <= j,
1364 c2 - (c2 - cmax) * i / j,
1365 cmax - (cmax - c1) * abs((i - j) / (1 - j)))
1366 #print(res)
1367 return res
1369 if cmax is None or isnan(cmax):
1370 C = _linear_trajectory(i**p1, c1, c2)
1371 else:
1372 # Calculate the position of the triangle point
1373 j = 1. / (1. + abs(cmax - c1) / abs(cmax - c2))
1374 if not j is None and (j <= 0. or j >= 1.): j = None
1376 if j is None: C = _linear_trajectory(i**p1, c1, c2)
1377 else: C = _triangle_trajectory(i**p1, j, c1, c2, cmax)
1379 return C
1382 def _get_seqhcl(self, i, ha, hb, ca, cb, la, lb, pa, pb, cmax):
1383 """Get Sequential Palette Colors
1385 Get 'one side' of a sequential palette. This is also used
1386 by `divergingx_hcl` which is, per construction, nothing else
1387 than a combination of two flexible sequential palettes meeting
1388 in the center.
1390 Args:
1391 i (numpy.ndarray): Sequence of floats in `[0, 1]` where to draw the
1392 values from. Typically `linspace(1, 0, n)`, but for some palettes
1393 (diverging palettes with an even number of colors) they do not meet at 0.
1394 ha (float): Hue on end "A".
1395 hb (float): Hue on end "B".
1396 ca (float): Chroma on end "A".
1397 cb (float): Chroma on end "B".
1398 la (float): Luminance on end "A".
1399 lb (float): Luminance on end "B".
1400 pa (loat): Power parameter 1.
1401 pb (loat): Power parameter 2.
1402 cmax (float, None, np.nan): Max chroma.
1404 Return:
1405 list: List of `H`, `C`, and `L` coordinates.
1406 """
1407 from numpy import power
1409 # Hue and Luminance
1410 H = hb - (hb - ha) * i
1411 L = lb - (lb - la) * power(i, pb)
1413 # Calculate the trajectory for the chroma dimension
1414 C = self._chroma_trajectory(i, pa, ca, cb, cmax)
1416 return [H, C, L]
1419# -------------------------------------------------------------------
1420# -------------------------------------------------------------------
1421class qualitative_hcl(hclpalette):
1422 """Qualitative HCL Color Palettes
1424 The HCL (hue-chroma-luminance) color model is a perceptual color model
1425 obtained by using polar coordinates in CIELUV space
1426 (i.e., :py:class:`polarLUV <colorspace.colorlib.polarLUV>`),
1427 where steps of equal size correspond to approximately equal perceptual
1428 changes in color. By taking polar coordinates the resulting three
1429 dimensions capture the three perceptual axes very well: hue is the type of
1430 color, chroma the colorfulness compared to the corresponding gray, and
1431 luminance the brightness. This makes it relatively easy to create balanced
1432 palettes through trajectories in this HCL space. In contrast, in the more
1433 commonly-used ‘HSV’ (hue-saturation-value) model (a simple transformation
1434 of ‘RGB’), the three axes are confounded so that luminance changes along
1435 with the hue leading to very unbalanced palettes.
1437 `qualitative_hcl` distinguishes the underlying categories by a
1438 sequence of hues while keeping both chroma and luminance constant to give
1439 each color in the resulting palette the same perceptual weight. Thus, `h`
1440 should be a pair of hues (or equivalently `h1` and `h2` can be used) with
1441 the starting and ending hue of the palette. Then, an equidistant sequence
1442 between these hues is employed, by default spanning the full color wheel
1443 (i.e, the full 360 degrees). Chroma `c` (or equivalently `c1`) and
1444 luminance `l` (or equivalently `l1`) are constants. If `h` is str it will
1445 overwrite the `palette` argument. In this case, pre-specified palette
1446 settings will be loaded but are allowed to be overwritten by the user. At
1447 any time the user can overwrite any of the settings. If `h` is str it will
1448 overwrite the `palette` argument. In this case, pre-specified palette
1449 settings will be loaded but are allowed to be overwritten by the user. At
1450 any time the user can overwrite any of the settings.
1452 By default, `qualitative_hcl` returns an object of class `hclpalette` which
1453 allows to draw a number of colors (`n`) uniformly distributed around the
1454 circle (`[0, 360 * (n - 1) / n]`) controlled via the `h` (Hue) argument. As
1455 the number of colors is not yet defined, the upper hue limit (`h[1]`, `h2`)
1456 is defined via lambda function.
1458 See also: :py:class:`sequential_hcl`, :py:class:`diverging_hcl`,
1459 :py:class:`divergingx_hcl`, :py:class:`rainbow_hcl`, :py:class:`heat_hcl`,
1460 :py:class:`terrain_hcl`, :py:class:`diverging_hsv`, and
1461 :py:class:`rainbow`.
1463 Args:
1464 h (list, str): Hue values defining the 'color' or name of pre-defined
1465 palette (`str`) or a list of two numeric values (float/int) defining
1466 the hue on the two ends of the palette. If str, it acts as the
1467 input argument `palette`.
1468 Elements in list can also be lambda functions with one single input
1469 argument `n` (number of colors; see default value).
1470 c (int, float): Chroma value (colorfullness), a single numeric value.
1471 l (int, float): luminance value (lightness), a single numeric value.
1472 fixup (bool): Only used when converting the HCL colors to hex. Should RGB
1473 values outside the defined RGB color space be corrected?
1474 palette (None, str): Can be used to load a default diverging color
1475 qpalette specification. If the palette does not exist an exception will be
1476 raised. Else the settings of the palette as defined will be used to create
1477 qthe color palette.
1478 rev (bool): Should the color map be reversed? Default `False`.
1479 **kwargs: Additional named arguments to overwrite the palette settings.
1480 Allowed: `h1`, `h2`, `c1`, `l1`.
1482 Returns:
1483 qualitative_hcl: Initialize new object. Raises exceptions if the parameters are
1484 misspecified. Note that the object is callable, the default object call can
1485 be used to return hex colors (identical to the `.colors()` method), see
1486 examples.
1488 Example:
1490 >>> from colorspace import qualitative_hcl
1491 >>> a = qualitative_hcl()
1492 >>> a.colors(10)
1493 >>> #:
1494 >>> b = qualitative_hcl("Warm")
1495 >>> b.colors(10)
1496 >>> #:
1497 >>> b.swatchplot(show_names = False, figsize = (5.5, 0.5));
1498 >>> #: The standard call of the object also returns hex colors
1499 >>> qualitative_hcl("Warm")(10)
1500 >>>
1501 >>> #: Example where `h` is a list of two lambda functions
1502 >>> from colorspace import hexcols
1503 >>> pal = qualitative_hcl([lambda n: 100. * (n - 1) / n,
1504 >>> lambda n, h1: 300. * (n - 1) / n + h1], c = 30)
1505 >>> cols = hexcols(pal.colors(5))
1506 >>> cols
1507 >>> #:
1508 >>> cols.to("HCL")
1509 >>> cols
1512 Raises:
1513 TypeError: If `h` is neither str nor list of length 2.
1514 TypeError: If `h` is list of length 2, the elements must be int, float, or
1515 lambda functions.
1516 ValueError: If `c` and/or `l` contain unexpected values.
1517 ValueError: If `h` is str or `palette` is set, but a pre-defined palette
1518 with this name does not exist.
1519 """
1521 _allowed_parameters = ["h1", "h2", "c1", "l1"]
1522 _name = "Qualitative"
1524 def __init__(self, h = [0, lambda n: 360. * (n - 1.) / n], c = 80, l = 60,
1525 fixup = True, palette = None, rev = False, **kwargs):
1527 self._set_rev(rev)
1528 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
1529 if not isinstance(palette, (str, type(None))):
1530 raise TypeError("argument `palette` must be None or str")
1532 # If a str is given on "h": exchange with "palette".
1533 if isinstance(h, str):
1534 palette = h
1535 h = [0, lambda n: 360. * (n - 1.) / n] # Temporary override
1536 # Else it must be list of length 2
1537 elif not isinstance(h, list) or not len(h) == 2:
1538 raise ValueError("argument `h` must be str or list of length 2")
1539 # Check list elements for allowed types
1540 else:
1541 for rec in h:
1542 if callable(rec): pass
1543 elif isinstance(rec, (float, int)): pass
1544 else:
1545 raise TypeError("unexpected type in list on argument `h`")
1547 # _checkinput_ parameters (in the correct order):
1548 # - dtype, length_min, length_max, recycle, nansallowed, **kwargs
1549 try:
1550 c = self._checkinput_(int, 1, 1, False, c = c)
1551 l = self._checkinput_(int, 1, 1, False, l = l)
1552 except Exception as e:
1553 raise ValueError(str(e))
1555 # If user selected a named palette: load palette settings
1556 if isinstance(palette, str):
1557 from numpy import where
1558 pals = hclpalettes().get_palettes("Qualitative")
1559 idx = where([x.name().upper().replace(" ", "") == palette.upper().replace(" ", "") for x in pals])[0]
1560 if len(idx) == 0:
1561 raise ValueError(f"palette {palette} is not a valid qualitative palette. " + \
1562 f"Choose one of: {', '.join([x.name() for x in pals])}")
1563 pal = pals[idx[0]]
1565 # Allow to overrule few things
1566 for key,value in kwargs.items():
1567 if key in self._allowed_parameters: pal.set(**{key: value}, lambda_allowed = True)
1569 # Extending h2 if h1 = h2 (h2 None)
1570 if pal.get("h2") == None or pal.get("h1") == pal.get("h2"):
1571 pal.set(h2 = pal.get("h1") + 360)
1572 if pal.get("h2") > 360:
1573 pal.set(h1 = pal.get("h1") - 360)
1574 pal.set(h2 = pal.get("h2") - 360)
1576 # Getting settings
1577 settings = pal.get_settings()
1578 else:
1579 # User settings
1580 settings = {}
1581 settings["h1"] = h[0]
1582 settings["h2"] = h[1]
1583 settings["c1"] = c[0] # qualitative palette, constant
1584 settings["l1"] = l[0] # qualitative palette, constant
1585 settings["fixup"] = fixup
1586 settings["rev"] = rev
1588 # Allow to overule few things
1589 for key,value in kwargs.items():
1590 if key in ["h1", "h2", "c1", "l1"]: settings[key] = value
1592 # If keyword arguments are set:
1593 # overwrite the settings if possible.
1594 if kwargs:
1595 for key,val in kwargs.items():
1596 if not key in self._allowed_parameters + ["desc", "gui"]:
1597 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
1598 settings[key] = val
1600 # Save settings
1601 self.settings = settings
1604 def colors(self, n = 11, fixup = None, alpha = None, **kwargs):
1605 """Get Colors
1607 Returns the colors of the current color palette.
1609 Args:
1610 n (int): Number of colors which should be returned, defaults to 11.
1611 fixup (None, bool): should sRGB colors be corrected if they lie outside
1612 the defined color space? If `None` the `fixup` parameter from the
1613 object will be used. Can be set to `True` or `False` to explicitly
1614 control the fixup here.
1615 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
1616 (alpha channel) to the colors. Can be a single float, a list, or a
1617 numpy array. If a list or array is provided it must be of length 1 or
1618 of length `n` and be convertible to float, providing values
1619 between `0.0` (full opacity) and `1.0` (full transparency)
1620 **kwargs: Currently allows for `rev = True` to reverse the colors.
1622 Returns:
1623 list: Returns a list of str with `n` colors from the
1624 color palette.
1626 Examples:
1627 >>> from colorspace import qualitative_hcl, rainbow_hcl
1628 >>> qualitative_hcl("Dark 3").colors()
1629 >>> #: Five colors with constant alpha of 0.3
1630 >>> qualitative_hcl().colors(5, alpha = 0.3)
1631 >>> #: Three colors with variying alpha
1632 >>> qualitative_hcl().colors(3, alpha = [0.2, 0.8, 0.3])
1633 >>>
1634 >>> #: Same for rainbow_hcl which is a special
1635 >>> # version of the qualitative HCL color palette
1636 >>> rainbow_hcl().colors(4)
1638 """
1640 from numpy import repeat, linspace, asarray
1641 from numpy import vstack, transpose
1642 from .colorlib import HCL
1644 alpha = self._get_alpha_array(alpha, n)
1645 fixup = fixup if isinstance(fixup, bool) else self.settings["fixup"]
1647 # If either h1 or h2 is a lambda function: evaluate now.
1648 h1 = self.get("h1")(n) if callable(self.get("h1")) else self.get("h1")
1649 if callable(self.get("h2")):
1650 fn = self.get("h2")
1651 # Distinguish between lambda functions with one argument (n) or two (n + h1)
1652 h1 = self.get("h1")(n) if callable(self.get("h1")) else self.get("h1")
1653 h2 = fn(n) if fn.__code__.co_argcount == 1 else fn(n, h1)
1654 else:
1655 h2 = self.get("h2")
1657 # Calculate the coordinates for our HCL color(s)
1658 L = repeat(self.get("l1"), n)
1659 C = repeat(self.get("c1"), n)
1660 H = linspace(h1, h2, n)
1662 # Create new HCL color object
1663 HCL = HCL(H, C, L, alpha)
1665 # Reversing colors
1666 rev = self._rev
1667 if "rev" in kwargs.keys(): rev = kwargs["rev"]
1669 # Return hex colors
1670 return HCL.colors(fixup = fixup, rev = rev)
1673# -------------------------------------------------------------------
1674# The rainbow class extends the qualitative_hcl class.
1675# -------------------------------------------------------------------
1676class rainbow_hcl(qualitative_hcl):
1677 """HCL Based Rainbow Palette
1679 `rainbow_hcl` computes a rainbow of colors via :py:class:`qualitative_hcl`
1680 defined by different hues given a single value of each chroma and
1681 luminance. It corresponds to `rainbow` which computes a rainbow in
1682 HSV space.
1684 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
1685 :py:class:`diverging_hcl`, :py:class:`divergingx_hcl`,
1686 :py:class:`heat_hcl`, :py:class:`terrain_hcl`, :py:class:`diverging_hsv`,
1687 and :py:class:`rainbow`.
1689 Args:
1690 c (float, int): Chroma (colorfullness) of the color map `[0-100+]`.
1691 l (float, int): Luminance (lightness) of the color map `[0-100]`.
1692 start (float, int, lambda): Hue at which the rainbow should start or lambda function
1693 with one argument. Defaults to 0.
1694 end (float, int, lambda): Hue (int) at which the rainbow should end or lambda function
1695 with one argument. By default a lambda function evaluated when
1696 drawing colors (`360 * (n - 1) / n`).
1697 fixup (bool): Only used when converting the HCL colors to hex. Should
1698 RGB values outside the defined RGB color space be corrected?
1699 rev (bool): Should the color map be reversed? Default `False`.
1700 *args: Currently unused.
1701 **kwargs: Additional named arguments to overwrite the palette settings.
1702 Allowed: `h1`, `h2`, `c1`, `l1`, `l2`, `p1`.
1704 Returns:
1705 Initialize new object, no return. Raises a set of errors if the parameters
1706 are misspecified. Note that the object is callable, the default object call
1707 can be used to return hex colors (identical to the `.colors()` method),
1708 see examples.
1710 Example:
1712 >>> from colorspace import rainbow_hcl
1713 >>> pal = rainbow_hcl()
1714 >>> pal.colors(10)
1715 >>> #:
1716 >>> pal.swatchplot(show_names = False, figsize = (5.5, 0.5));
1717 >>> #: The standard call of the object also returns hex colors. Thus,
1718 >>> # you can make your code slimmer by calling
1719 >>> rainbow_hcl()(10)
1720 >>>
1721 >>> #: Testing lambda function for both, start and end
1722 >>> pal = rainbow_hcl(start = lambda n: (n - 1) / n,
1723 >>> end = lambda n: 360 - (n - 1) / n)
1724 >>> pal.swatchplot(n = 5, show_names = False, figsize = (5.5, 0.5))
1725 >>> #:
1726 >>> pal.swatchplot(n = 10, show_names = False, figsize = (5.5, 0.5))
1727 """
1729 _allowed_parameters = ["h1", "h2", "c1", "l1"]
1730 _name = "Rainbow HCL"
1732 def __init__(self, c = 50, l = 70, start = 0, end = lambda n: 360 * (n - 1) / n,
1733 fixup = True, rev = False, *args, **kwargs):
1735 self._set_rev(rev)
1736 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
1738 # _checkinput_ parameters (in the correct order):
1739 # dtype, length = None, recycle = False, nansallowed = False, **kwargs
1740 try:
1741 c = self._checkinput_(int, 1, 1, False, c = c)
1742 l = self._checkinput_(int, 1, 1, False, l = l)
1743 except Exception as e:
1744 raise ValueError(str(e))
1746 # Checking start and end. If int, use _checkinput_, if callable make
1747 # sure it is a lambda function with one single input argument.
1748 if isinstance(start, (float, int)):
1749 start = self._checkinput_(int, 1, False, False, start = start)
1750 elif callable(start):
1751 if not start.__code__.co_argcount == 1:
1752 raise Exception("if `start` is a lambda function it must have only one argument")
1753 else:
1754 raise TypeError("argument `start` must be int or lambda function")
1756 if isinstance(end, int):
1757 end = self._checkinput_(int, 1, False, False, end = end)
1758 elif callable(end):
1759 if not end.__code__.co_argcount == 1:
1760 raise Exception("if `end` is a lambda function it must have only one argument")
1761 else:
1762 raise TypeError("argument `end` must be int or lambda function")
1764 # Save settins
1765 self.settings = {"h1": start if callable(start) else int(start[0]),
1766 "h2": end if callable(end) else int(end[0]),
1767 "c1": int(c[0]),
1768 "l1": int(l[0]),
1769 "fixup": bool(fixup)}
1771 # If keyword arguments are set:
1772 # overwrite the settings if possible.
1773 if kwargs:
1774 for key,val in kwargs.items():
1775 if not key in self._allowed_parameters + ["desc", "gui"]:
1776 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
1777 self.settings[key] = val
1779 self.settings["rev"] = self._rev
1781# -------------------------------------------------------------------
1782# -------------------------------------------------------------------
1783class diverging_hcl(hclpalette):
1784 """Diverging HCL Color Palettes
1786 The HCL (hue-chroma-luminance) color model is a perceptual color model
1787 obtained by using polar coordinates in CIELUV space
1788 (i.e., :py:class:`polarLUV <colorspace.colorlib.polarLUV>`),
1789 where steps of equal size correspond to approximately equal perceptual
1790 changes in color. By taking polar coordinates the resulting three
1791 dimensions capture the three perceptual axes very well: hue is the type of
1792 color, chroma the colorfulness compared to the corresponding gray, and
1793 luminance the brightness. This makes it relatively easy to create balanced
1794 palettes through trajectories in this HCL space. In contrast, in the more
1795 commonly-used ‘HSV’ (hue-saturation-value) model (a simple transformation
1796 of ‘RGB’), the three axes are confounded so that luminance changes along
1797 with the hue leading to very unbalanced palettes.
1799 `diverging_hcl` codes the underlying numeric values by a
1800 triangular luminance sequence with different hues in the left and
1801 in the right arm of the palette. Thus, it can be seen as a
1802 combination of two sequential palettes with some restrictions: (a)
1803 a single hue is used for each arm of the palette, (b) chroma and
1804 luminance trajectory are balanced between the two arms, (c) the
1805 neutral central value has zero chroma. To specify such a palette a
1806 vector of two hues `h` (or equivalently `h1` and `h2`), either a
1807 single chroma value `c` (or `c1`) or a vector of two chroma values
1808 `c` (or `c1` and `cmax`), a vector of two luminances `l` (or `l1`
1809 and `l2`), and power parameter(s) `power` (or `p1` and `p2`) are
1810 used. For more flexible diverging palettes without the
1811 restrictrictions above (and consequently more parameters)
1812 `divergingx_hcl` is available. For backward compatibility,
1813 `diverge_hcl` is a copy of `diverging_hcl`.
1815 If `h` is str it will overwrite the `palette` argument. In this case,
1816 pre-specified palette settings will be loaded but are allowed to be
1817 overwritten by the user. At any time the user can overwrite any of the
1818 settings. By default, `diverging_hcl` returns an object of class
1819 `hclpalette` identical to the pre-defined `"Blue-Red"` palette.
1821 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
1822 :py:class:`divergingx_hcl`, :py:class:`rainbow_hcl`, :py:class:`heat_hcl`,
1823 :py:class:`terrain_hcl`, :py:class:`diverging_hsv`, and
1824 :py:class:`rainbow`.
1826 Args:
1827 h (list, float, int): Hue values (color), diverging color palettes should
1828 have different hues for both ends of the palette. If only one value is
1829 present it will be recycled ending up in a diverging color palette with the
1830 same colors on both ends. If more than two values are provided the first
1831 two will be used while the rest is ignored. If input `h` is a str
1832 this argument acts like the `palette` argument (see `palette` input
1833 parameter).
1834 c (float, int, list): Chroma value (colorfullness), a single numeric value. If two
1835 values are provided the first will be taken as `c1`, the second as `cmax`.
1836 l (float, int, list): luminance values (lightness). The first value is for
1837 the two ends of the color palette, the second one for the neutral center
1838 point. If only one value is given this value will be recycled.
1839 power (float, int, list): Power parameter for non-linear behaviour of the color
1840 palette.
1841 fixup (bool): Only used when converting the HCL colors to hex. Should RGB
1842 values outside the defined RGB color space be corrected?
1843 palette (str): Can be used to load a default diverging color palette
1844 specification. If the palette does not exist an exception will be raised.
1845 Else the settings of the palette as defined will be used to create the
1846 color palette.
1847 rev (bool): Should the color map be reversed.
1848 *args: Currently unused.
1849 **kwargs: Additional named arguments to overwrite the palette settings.
1850 Allowed: `h1`, `h2`, `c1`, `cmax`, `l1`, `l2`, `p1`, `p2`.
1852 Returns:
1853 Initialize new object, no return. Raises a set of errors if the parameters
1854 are misspecified. Note that the object is callable, the default object call
1855 can be used to return hex colors (identical to the `.colors()` method),
1856 see examples.
1858 Example:
1860 >>> from colorspace import diverging_hcl
1861 >>> a = diverging_hcl()
1862 >>> a.colors(10)
1863 >>> #: Different color palette by name
1864 >>> b = diverging_hcl("Tropic")
1865 >>> b.colors(10)
1866 >>> #:
1867 >>> b.swatchplot(show_names = False, figsize = (5.5, 0.5));
1868 >>> #: The standard call of the object also returns hex colors
1869 >>> diverging_hcl("Tropic")(10)
1870 """
1872 _allowed_parameters = ["h1", "h2", "c1", "cmax", "l1", "l2", "p1", "p2"]
1873 _name = "Diverging HCL"
1875 def __init__(self, h = [260, 0], c = 80, l = [30, 90],
1876 power = 1.5, fixup = True, palette = None, rev = False,
1877 *args, **kwargs):
1879 self._set_rev(rev)
1880 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
1881 if not isinstance(palette, (str, type(None))):
1882 raise TypeError("argument `palette` must be None or str")
1884 if isinstance(h, str):
1885 palette = h
1886 h = -999 # Temporarliy setting to dummy value
1887 if isinstance(power, int) or isinstance(power, float):
1888 power = [power]
1890 # _checkinput_ parameters (in the correct order):
1891 # - dtype, length_min, length_max, recycle, nansallowed, **kwargs
1892 try:
1893 h = self._checkinput_(int, 2, 2, True, False, h = h)
1894 c = self._checkinput_(int, 1, 2, False, False, c = c)
1895 l = self._checkinput_(int, 2, 2, True, False, l = l)
1896 power = self._checkinput_(float, 1, 2, True, False, power = power)
1897 except Exception as e:
1898 raise ValueError(str(e))
1900 # If user selected a named palette: load palette settings
1901 if isinstance(palette, str):
1902 from numpy import where
1903 pals = hclpalettes().get_palettes("Diverging")
1904 idx = where([x.name().upper().replace(" ", "") == \
1905 palette.upper().replace(" ", "") for x in pals])[0]
1906 if len(idx) == 0:
1907 raise ValueError(f"palette {palette} is not a valid diverging palette. " + \
1908 f"Choose one of: {', '.join([x.name() for x in pals])}")
1909 pal = pals[idx[0]]
1911 # Allow to overule few things
1912 for key,value in kwargs.items():
1913 if key in self._allowed_parameters: pal.set(**{key: value})
1915 # Extending h2 if h1 == h2 or h2 is None
1916 if pal.get("h2") == None or pal.get("h1") == pal.get("h2"):
1917 pal.set(h2 = pal.get("h1") + 360)
1918 if pal.get("h2") > 360:
1919 pal.set(h1 = pal.get("h1") - 360)
1920 pal.set(h2 = pal.get("h2") - 360)
1922 # Getting settings
1923 settings = pal.get_settings()
1924 else:
1925 settings = {}
1927 from numpy import ndarray
1929 # User settings
1930 settings["h1"] = h[0]
1931 settings["h2"] = h[1]
1932 settings["c1"] = float(c[0])
1933 if len(c) == 2:
1934 settings["cmax"] = float(c[1])
1935 settings["l1"] = l[0]
1936 settings["l2"] = l[1]
1937 settings["p1"] = float(power[0])
1938 if len(power) == 2:
1939 settings["p2"] = float(power[1])
1940 settings["fixup"] = fixup
1941 settings["rev"] = rev
1943 # If keyword arguments are set:
1944 # overwrite the settings if possible.
1945 if kwargs:
1946 for key,val in kwargs.items():
1947 if not key in self._allowed_parameters + ["desc", "gui"]:
1948 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
1949 settings[key] = val
1951 # Save settings
1952 self.settings = settings
1955 # Return hex colors
1956 def colors(self, n = 11, fixup = None, alpha = None, **kwargs):
1957 """Get Colors
1959 Returns the colors of the current color palette.
1961 Args:
1962 n (int): Number of colors which should be returned.
1963 fixup (None, bool): Should sRGB colors be corrected if they lie
1964 outside the defined color space? If `None` the `fixup`
1965 parameter from the object will be used. Can be set to `True` or
1966 `False` to explicitly control the fixup here.
1967 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
1968 (alpha channel) to the colors. Can be a single float, a list, or a
1969 numpy array. If a list or array is provided it must be of length 1 or
1970 of length `n` and be convertible to float, providing values
1971 between `0.0` (full opacity) and `1.0` (full transparency)
1972 **kwargs: Currently allows for `rev = True` to reverse the colors.
1974 Returns:
1975 list: Returns a list of str with `n` colors from the
1976 color palette.
1979 Examples:
1980 >>> from colorspace import diverging_hcl, hexcols
1981 >>> diverging_hcl().colors()
1982 >>> #: Different diverging palette
1983 >>> cols = diverging_hcl("Green-Orange").colors(5)
1984 >>> cols
1985 >>> #:
1986 >>> hexcols(cols)
1987 >>> #: Five colors with constant alpha of 0.3
1988 >>> diverging_hcl().colors(5, alpha = 0.3)
1989 >>> #: Three colors with variying alpha
1990 >>> diverging_hcl().colors(3, alpha = [0.2, 0.8, 0.3])
1992 """
1994 from numpy import abs, ceil, linspace, power, repeat, arange, fmax, delete
1995 from numpy import asarray, ndarray, ndenumerate, concatenate, flip
1996 from numpy import vstack, transpose, repeat
1997 from .colorlib import HCL
1999 alpha = self._get_alpha_array(alpha, n)
2000 fixup = fixup if isinstance(fixup, bool) else self.settings["fixup"]
2002 # Calculate H/C/L
2003 p1 = self.get("p1")
2004 p2 = p1 if self.get("p2") is None else self.get("p2")
2005 c1 = self.get("c1")
2006 c2 = 0 if self.get("c2") is None else self.get("c2")
2007 cmax = None if not self.get("cmax") else self.get("cmax")
2008 l1 = self.get("l1")
2009 l2 = l1 if self.get("l2") is None else self.get("l2")
2010 h1 = self.get("h1")
2011 h2 = h1 if self.get("h2") is None else self.get("h2")
2013 # If n == 1 we do as we have 3 colors, but then only return the middle one
2014 tmp_n = n if n > 1 else 3
2016 # Calculate H/C/L
2017 rval = linspace(1., -1., tmp_n)
2019 L = l2 - (l2 - l1) * power(abs(rval), p2)
2020 H = ndarray(tmp_n, dtype = "float")
2021 for i,val in ndenumerate(rval): H[i] = h1 if val > 0 else h2
2023 # Calculate the trajectory for the chroma dimension
2024 i = fmax(0, arange(1., -1e-10, -2. / (tmp_n - 1.)))
2025 C = self._chroma_trajectory(i, p1, c1, c2, cmax)
2026 C = fmax(0., concatenate((C, flip(C))))
2028 # Non-even number of colors? We need to remove one.
2029 if tmp_n % 2 == 1: C = delete(C, int(ceil(tmp_n / 2.)))
2031 # Create new HCL color object
2032 HCL = HCL(H, C, L, alpha)
2034 # Reversing colors
2035 rev = self._rev
2036 if "rev" in kwargs.keys(): rev = kwargs["rev"]
2038 # Return hex colors
2039 cols = HCL.colors(fixup = fixup, rev = rev)
2040 return [cols[1]] if n == 1 else cols
2045# -------------------------------------------------------------------
2046# -------------------------------------------------------------------
2047class divergingx_hcl(hclpalette):
2048 """Diverging X HCL Color Palettes
2050 More flexible version of the `diverging_hcl` class. A diverging X
2051 palette basically consists of two multi-hue sequential palettes.
2053 The `divergingx_hcl` function simply calls :py:class:`sequential_hcl` twice
2054 with a prespecified set of hue, chroma, and luminance parameters. This is
2055 similar to :py:class:`diverging_hcl` but allows for more flexibility:
2056 :py:class:`diverging_hcl` employs two _single-hue_ sequential palettes,
2057 always uses zero chroma for the neutral/central color, and restricts the
2058 chroma/luminance path to be the same in both "arms" of the palette. In
2059 contrast, `divergingx_hcl` relaxes this to two full _multi-hue_ palettes
2060 that can thus go through a non-gray neutral color (typically light yellow).
2061 Consequently, the chroma/luminance paths can be rather unbalanced between
2062 the two arms.
2064 With this additional flexibility various diverging palettes
2065 suggested by <https://ColorBrewer2.org/> and CARTO
2066 (<https://carto.com/carto-colors/>), can be emulated along with
2067 the Zissou 1 palette from 'wesanderson', Cividis from 'viridis',
2068 and Roma from 'scico'.
2070 * Available CARTO palettes: ArmyRose, Earth, Fall, Geyser, TealRose,
2071 Temps, and Tropic (available in :py:class:`diverging_hcl`).
2072 * Available ColorBrewer.org palettes: PuOr, RdBu, RdGy, PiYG, PRGn,
2073 BrBG, RdYlBu, RdYlGn, Spectral.
2075 If `h` is str it will overwrite the `palette` argument. In this case,
2076 pre-specified palette settings will be loaded but are allowed to be
2077 overwritten by the user. At any time the user can overwrite any of
2078 the settings.
2080 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
2081 :py:class:`diverging_hcl`, :py:class:`rainbow_hcl`, :py:class:`heat_hcl`,
2082 :py:class:`terrain_hcl`, :py:class:`diverging_hsv`, and
2083 :py:class:`rainbow`.
2085 Args:
2086 h (list): Hue values (color), list of three numerics. Divergingx color
2087 palettes should have different hues for both ends and the center of the
2088 palette. For this class three values must be provided. If input `h` is
2089 a str this argument acts like the `palette` argument (see `palette`
2090 input parameter).
2091 c (list): Chroma value (colorfullness), list of floats. In case two
2092 values are provided the firt is taken as `c1` and `c3` while the second
2093 one is used for `c2` (center value). When three values are provided
2094 they are used as `c1`, `c2`, and `c3` (see also `cmax`).
2095 l (list): Luminance values (lightness), list of float/int. In case two
2096 values are provided the firt is taken as `l1` and `l3` while the second
2097 one is used for `l2` (center value). When three are provided
2098 they are used as `l1`, `l2`, and `l3` respectively.
2099 power (list): Power parameters for non-linear behaviour of the color
2100 palette, list of floats.
2101 If two values are provided `power[0]` will be used for `p1` and `p4`
2102 while `power[1]` is used for `p2` and `p3` (symmetric). A list of length
2103 four allows to specify `p1`, `p2`, `p3`, and `p4` individually. List
2104 of length three acts like a list of length two, the last element is ignored.
2105 cmax (None, float, int, list, numpy.ndarray): Maximum chroma for
2106 triangular trajectory. Unused if set `Non`. If one value is provided it
2107 is used for `cmax1`, if two values are provided they are used as
2108 `cmax1` and `cmax2`, respectively.
2109 fixup (bool): Only used when converting the HCL colors to hex. Should RGB
2110 values outside the defined RGB color space be corrected?
2111 palette (str): Can be used to load a default diverging color palette
2112 specification. If the palette does not exist an exception will be raised.
2113 Else the settings of the palette as defined will be used to create the
2114 color palette.
2115 rev (bool): Should the color map be reversed.
2116 *args: Currently unused.
2117 **kwargs: Additional named arguments to overwrite the palette settings.
2118 Allowed: `h1`, `h2`, `h3`, `c1`, `cmax1`, `c2`, `cmax2` `c3`,
2119 `l1`, `l2`, `l3`, `p1`, `p2`, `p3`, `p4`.
2121 Returns:
2122 Initialize new object, no return. Raises a set of errors if the parameters
2123 are misspecified. Note that the object is callable, the default object call
2124 can be used to return hex colors (identical to the `.colors()` method),
2125 see examples.
2127 Example:
2129 >>> from colorspace import divergingx_hcl
2130 >>> pal1 = divergingx_hcl()
2131 >>> pal1.colors(5)
2132 >>> #:
2133 >>> pal1.swatchplot(show_names = False, figsize = (5.5, 0.5));
2134 >>>
2135 >>> #: Different color palette by name
2136 >>> pal2 = divergingx_hcl("ArmyRose")
2137 >>> pal2.colors(7)
2138 >>> #:
2139 >>> pal2.swatchplot(show_names = False, figsize = (5.5, 0.5));
2140 >>>
2141 >>> #: The standard call of the object also returns hex colors
2142 >>> divergingx_hcl("Fall")(3)
2143 >>>
2144 >>> #: Manual palette with user settings. The following diverginx
2145 >>> # palette goes from h = 180 (left) to h = 100 (center) and h = 20 (right).
2146 >>> # Croma is c = 30 (left), c = 5 (center), and c = 30 (right).
2147 >>> # In addition, both 'arms' have a maximum chroma of cmax = 70
2148 >>> # in the center of each of the two arms.
2149 >>> pal3 = divergingx_hcl(h = [180, 100, 20],
2150 >>> c = [30, 5, 30],
2151 >>> cmax = [70, 70])
2152 >>> pal3.specplot();
2153 >>> #: Drawing 5 colors from the custom palette.
2154 >>> pal3(3)
2155 >>>
2156 >>> #: Available default palettes (divergingx_hcl palettes)
2157 >>> from colorspace import divergingx_hcl, swatchplot, palette
2158 >>>
2159 >>> carto = ["ArmyRose", "Earth", "Fall",
2160 >>> "Geyser", "TealRose", "Temps"]
2161 >>> brewer = ["PuOr", "RdBu", "RdGy", "PiYG", "PRGn",
2162 >>> "BrBG", "RdYlBu", "RdYlGn", "Spectral"]
2163 >>> others = ["Zissou 1", "Cividis", "Roma"]
2164 >>>
2165 >>> # Create named palettes for swatchplot
2166 >>> col_carto = [palette(divergingx_hcl(x)(11), name = x) for x in carto]
2167 >>> col_brewer = [palette(divergingx_hcl(x)(11), name = x) for x in carto]
2168 >>> col_others = [palette(divergingx_hcl(x)(11), name = x) for x in others]
2169 >>>
2170 >>> # Visualize available divergingx palettes
2171 >>> swatchplot({"Carto": col_carto,
2172 >>> "Brewer": col_brewer,
2173 >>> "Others": col_others},
2174 >>> figsize = (5.5, 6));
2175 >>>
2176 >>> #: Checking settings of a specific palette
2177 >>> pal4 = divergingx_hcl("PRGn")
2178 >>> pal4.show_settings()
2180 Raises:
2181 TypeError: If `fixup` is not bool.
2182 TypeError: If `palette` is not `None` or str.
2183 TypeError: If `cmax` not `Non`, float, int, list, numpy.ndarray.
2184 ValueError: If `cmax` is list of length `<1` or `>2`.
2185 ValueError: If `h`, `c`, `l`, `power`, `cmax` contain unexpected types or values.
2186 ValueError: If `palette` is string, but palette with this name cannot be found.
2187 Exception: If `h3` is not specified.
2188 ValueError: If `**kwargs` are provides which are not among the allowed ones.
2189 """
2191 _allowed_parameters = ["h1", "h2", "h3",
2192 "c1", "cmax1", "c2", "cmax2", "c3",
2193 "l1", "l2", "l3", "p1", "p2", "p3", "p4"]
2194 _name = "DivergingX HCL"
2196 def __init__(self, h = [192, 77, 21], c = [40, 35, 100], l = [50, 95, 50], \
2197 power = [1.0, 1.0, 1.2, 1.0], cmax = 20, \
2198 fixup = True, palette = None, rev = False, *args, **kwargs):
2200 import numpy as np
2202 self._set_rev(rev)
2203 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
2204 if not isinstance(palette, (str, type(None))):
2205 raise TypeError("argument `palette` must be None or str")
2207 if isinstance(h, str):
2208 palette = h
2209 h = [-999.] * 3 # Temporarliy setting to dummy value
2210 if isinstance(power, int) or isinstance(power, float):
2211 power = [power]
2213 # Handling cmax
2214 if not isinstance(cmax, (type(None), float, int, list, np.ndarray)):
2215 raise TypeError("argument `cmax` must be None, float, int, or list")
2216 elif isinstance(cmax, (list, np.ndarray)) and (len(cmax) < 1 or len(cmax) > 2):
2217 raise ValueError("if `cmax` is a list or numpy.ndarray it must be of lengt 1 or 2")
2218 # If cmax is a numpy array: Reduce to list
2219 if isinstance(cmax, np.ndarray): cmax = list(cmax)
2221 if isinstance(cmax, list) and len(cmax) == 1:
2222 cmax = cmax + [None]
2223 elif cmax is None:
2224 cmax = [None, None] # Both set to None
2225 elif isinstance(cmax, (float, int)):
2226 cmax = [cmax, None] # Use 'cmax' as 'cmax1'
2228 # _checkinput_ parameters (in the correct order):
2229 # dtype, length = None, recycle = False, nansallowed = False, **kwargs
2230 try:
2231 h = self._checkinput_(int, 3, 3, False, False, h = h)
2232 c = self._checkinput_(int, 2, 3, False, False, c = c)
2233 l = self._checkinput_(int, 2, 3, False, False, l = l)
2234 power = self._checkinput_(float, 2, 4, False, False, power = power)
2235 cmax = self._checkinput_(float, 2, 2, False, True, cmax = cmax)
2236 except Exception as e:
2237 raise ValueError(str(e))
2239 if len(c) == 2: c = [c[0], c[1], c[0]]
2240 if len(l) == 2: l = [l[0], l[1], l[0]]
2241 if len(power) < 4: power = [power[0], power[1], power[1], power[0]]
2243 # If user selected a named palette: load palette settings
2244 if isinstance(palette, str):
2245 from .hcl_palettes import divergingx_palettes
2246 from numpy import where
2247 pals = divergingx_palettes().get_palettes("Divergingx")
2248 idx = where([x.name().upper().replace(" ", "") == palette.upper().replace(" ", "") for x in pals])[0]
2249 if len(idx) == 0:
2250 raise ValueError(f"palette {palette} is not a valid divergingx palette. " + \
2251 f"Choose one of: {', '.join([x.name() for x in pals])}")
2252 pal = pals[idx[0]]
2253 del pals, idx
2255 # Allow to overule few things
2256 for key,value in kwargs.items():
2257 if key in self._allowed_parameters:
2258 pal.set(**{key: value})
2260 # Extending h2 if h1 = h2 (h2 None)
2261 def isNone(x): return isinstance(x, type(None))
2262 if isNone(pal.get("p1")): pal.set(p1 = 1.0)
2263 # Second coordinate
2264 if isNone(pal.get("h2")): pal.set(h2 = pal.get("h1"))
2265 if isNone(pal.get("c2")): pal.set(c2 = 0.0)
2266 if isNone(pal.get("l2")): pal.set(l2 = pal.get("l1"))
2267 if isNone(pal.get("p2")): pal.set(p2 = pal.get("p1"))
2268 ## third coordinate
2269 if isNone(pal.get("h3")):
2270 raise Exception("third hue coordinate (h3) must be specified")
2271 if isNone(pal.get("c3")): pal.set(c3 = pal.get("c1"))
2272 if isNone(pal.get("l3")): pal.set(l3 = pal.get("l1"))
2273 if isNone(pal.get("p3")): pal.set(p3 = pal.get("p1"))
2274 if isNone(pal.get("p4")): pal.set(p4 = pal.get("p2"))
2276 # Getting settings
2277 settings = pal.get_settings()
2278 else:
2279 settings = {}
2281 from numpy import ndarray
2283 # User settings
2284 for i in range(3): settings["h{:d}".format(i + 1)] = h[i]
2285 for i in range(3): settings["c{:d}".format(i + 1)] = c[i]
2286 for i in range(3): settings["l{:d}".format(i + 1)] = l[i]
2287 for i in range(4): settings["p{:d}".format(i + 1)] = power[i]
2288 for i in range(2): settings["cmax{:d}".format(i + 1)] = cmax[i]
2289 settings["fixup"] = fixup
2290 settings["rev"] = rev
2292 # If keyword arguments are set:
2293 # overwrite the settings if possible.
2294 if kwargs:
2295 for key,val in kwargs.items():
2296 if not key in self._allowed_parameters + ["desc", "gui"]:
2297 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
2298 settings[key] = val
2300 # Save settings
2301 self.settings = settings
2304 # Return hex colors
2305 def colors(self, n = 11, fixup = None, alpha = None, **kwargs):
2306 """Get Colors
2308 Returns the colors of the current color palette.
2310 Args:
2311 n (int): Number of colors which should be returned.
2312 fixup (None, bool): Should sRGB colors be corrected if they lie
2313 outside the defined color space? If `None` the `fixup`
2314 parameter from the object will be used. Can be set to `True` or
2315 `False` to explicitly control the fixup here.
2316 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
2317 (alpha channel) to the colors. Can be a single float, a list, or a
2318 numpy array. If a list or array is provided it must be of length 1 or
2319 of length `n` and be convertible to float, providing values
2320 between `0.0` (full opacity) and `1.0` (full transparency)
2321 **kwargs: Currently allows for `rev = True` to reverse the colors.
2324 Returns:
2325 list: Returns a list of str with `n` colors from the
2326 color palette.
2328 """
2330 import numpy as np
2331 from .colorlib import HCL
2333 alpha = self._get_alpha_array(alpha, n)
2334 fixup = fixup if isinstance(fixup, bool) else self.settings["fixup"]
2336 # If n == 1 we do as we have 3 colors, but then only return the middle one
2337 tmp_n = n if n > 1 else 3
2339 # Calculate where to evaluate the trajectories
2340 # n2 is half the number of colors (n on either side of the palette)
2341 n2 = int(np.ceil(tmp_n / 2))
2342 rval = np.linspace(1., 0., n2) if n % 2 == 1 else np.linspace(1., 1. / (2 * n2 - 1), n2)
2344 # Calculate H/C/L coordinates for both sides (called 'a' and 'b' not to get
2345 # confused with the numbering of the parameters).
2346 Ha, Ca, La = self._get_seqhcl(rval, ha = self.get("h1"), hb = self.get("h2"),
2347 ca = self.get("c1"), cb = self.get("c2"),
2348 la = self.get("l1"), lb = self.get("l2"),
2349 pa = self.get("p1"), pb = self.get("p2"),
2350 cmax = self.get("cmax1"))
2351 Hb, Cb, Lb = self._get_seqhcl(rval, ha = self.get("h3"), hb = self.get("h2"),
2352 ca = self.get("c3"), cb = self.get("c2"),
2353 la = self.get("l3"), lb = self.get("l2"),
2354 pa = self.get("p3"), pb = self.get("p4"),
2355 cmax = self.get("cmax2"))
2357 # In case the user requested an odd number of colors we need to
2358 # cut away one of one of the two sides (remove it from 'b').
2359 if tmp_n % 2 == 1:
2360 Hb = Hb[:-1]
2361 Cb = Cb[:-1]
2362 Lb = Lb[:-1]
2364 # Concatenate the two sides
2365 H = np.concatenate((Ha, Hb[::-1]))
2366 C = np.concatenate((Ca, Cb[::-1]))
2367 L = np.concatenate((La, Lb[::-1]))
2369 # Create new HCL color object
2370 HCL = HCL(H, C, L, alpha)
2372 # Reversing colors
2373 rev = self._rev
2374 if "rev" in kwargs.keys(): rev = kwargs["rev"]
2376 # Return hex colors
2377 cols = HCL.colors(fixup = fixup, rev = rev)
2378 return [cols[1]] if n == 1 else cols
2383# -------------------------------------------------------------------
2384# -------------------------------------------------------------------
2385class sequential_hcl(hclpalette):
2386 """Sequential HCL Color Palettes
2388 The HCL (hue-chroma-luminance) color model is a perceptual color model
2389 obtained by using polar coordinates in CIELUV space
2390 (i.e., :py:class:`polarLUV <colorspace.colorlib.polarLUV>`),
2391 where steps of equal size correspond to approximately equal perceptual
2392 changes in color. By taking polar coordinates the resulting three
2393 dimensions capture the three perceptual axes very well: hue is the type of
2394 color, chroma the colorfulness compared to the corresponding gray, and
2395 luminance the brightness. This makes it relatively easy to create balanced
2396 palettes through trajectories in this HCL space. In contrast, in the more
2397 commonly-used ‘HSV’ (hue-saturation-value) model (a simple transformation
2398 of ‘RGB’), the three axes are confounded so that luminance changes along
2399 with the hue leading to very unbalanced palettes.
2401 `qualitative_hcl` distinguishes the underlying categories by a sequence of
2402 hues while keeping both chroma and luminance constant to give each color in
2403 the resulting palette the same perceptual weight. Thus, `h` should be a
2404 pair of hues (or equivalently `h1` and `h2` can be used) with the starting
2405 and ending hue of the palette. Then, an equidistant sequence between these
2406 hues is employed, by default spanning the full color wheel (i.e, the full
2407 360 degrees). Chroma `c` (or equivalently `c1`) and luminance `l` (or
2408 equivalently `l1`) are constants. If `h` is str it will overwrite the
2409 `palette` argument. In this case, pre-specified palette settings will be
2410 loaded but are allowed to be overwritten by the user. At any time the user
2411 can overwrite any of the settings.
2413 By default, `sequential_hcl` returns an object of class `hclpalette`
2414 identical to the pre-defined `"Blues 2"` palette.
2416 `h1` and `h2` both allow for lambda functions to create uniformly distributed
2417 hues around the (full) circle (360 degrees).
2419 * `h1`: can be a lambda function with one single argument `n` (number of colors).
2420 * `h2`: can be a lambda function with one or two arguments. If only one, `n`
2421 (number of colors) will be handed over when evaluated. If two, the first
2422 one is expected to be `n` (number of colors), as second argument the
2423 value `h1` will be used.
2425 See also: :py:class:`qualitative_hcl`, :py:class:`diverging_hcl`,
2426 :py:class:`divergingx_hcl`, :py:class:`rainbow_hcl`, :py:class:`heat_hcl`,
2427 :py:class:`terrain_hcl`, :py:class:`diverging_hsv`, and
2428 :py:class:`rainbow`.
2430 Args:
2431 h (float, int, list, str): Hue values (color). If only one value is given the value
2432 is recycled which yields a single-hue sequential color palette. If
2433 input `h` is a str this argument acts like the `palette` argument
2434 (see `palette` input parameter).
2435 c (float, int, list): Chroma values (colorfullness), int or float
2436 (linear to zero), list of two numerics (linear in interval), =
2437 or three numerics (advanced; `[c1, cmax, c2]`).
2438 l (float, int, list): Luminance values (luminance). If float or int,
2439 the element will be recycled, or a list of two numerics
2440 (defining `[l1, l2]`).
2441 power (float, int, list): Power parameter for non-linear behaviour
2442 of the color palette. Single float or int, or a list of numerics
2443 (defining `[p1, p2]`).
2444 fixup (bool): Only used when converting the HCL colors to hex. Should
2445 RGB values outside the defined RGB color space be corrected?
2446 palette (str): Can be used to load a default diverging color palette
2447 specification. If the palette does not exist an exception will be
2448 raised. Else the settings of the palette as defined will be used to
2449 create the color palette.
2450 rev (bool): Should the color map be reversed.
2451 *args: Currently unused.
2452 **kwargs: Additional named arguments to overwrite the palette settings.
2453 Allowed: `h1`, `h2`, `c1`, `c2`, `cmax`, `l1`, `l2`, `p1`, `p2`.
2455 Returns:
2456 Initialize new object, no return. Raises a set of errors if the parameters
2457 are misspecified. Note that the object is callable, the default object call
2458 can be used to return hex colors (identical to the `.colors()` method),
2459 see examples.
2461 Example:
2463 >>> from colorspace import sequential_hcl
2464 >>> a = sequential_hcl()
2465 >>> a.colors(10)
2466 >>> #: Different color palette by name
2467 >>> b = sequential_hcl("Peach")
2468 >>> b.colors(10)
2469 >>> #:
2470 >>> b.swatchplot(show_names = False, figsize = (5.5, 0.5));
2471 >>> #: The standard call of the object also returns hex colors
2472 >>> sequential_hcl("Peach")(10)
2473 """
2475 # Allowed to overwrite via **kwargs
2476 _allowed_parameters = ["h1", "h2", "c1", "c2", "cmax", "l1", "l2", "p1", "p2"]
2477 _name = "Sequential HCL"
2479 def __init__(self, h = 260, c = 80, l = [30, 90],
2480 power = 1.5, fixup = True, palette = None, rev = False,
2481 *args, **kwargs):
2483 self._set_rev(rev)
2484 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
2485 if not isinstance(palette, (str, type(None))):
2486 raise TypeError("argument `palette` must be None or str")
2488 # If input "h" is a str: exchange with "palette"
2489 if isinstance(h, str):
2490 palette = h
2491 h = -999 # Temporarliy setting to dummy value
2493 # _checkinput_ parameters (in the correct order):
2494 # dtype, length_min = None, length_max = None,
2495 # recycle = False, nansallowed = False, **kwargs
2496 try:
2497 h = self._checkinput_(int, 1, 2, True, False, h = h)
2498 c = self._checkinput_(int, 1, 3, True, False, c = c)
2499 l = self._checkinput_(int, 2, 2, True, False, l = l)
2500 power = self._checkinput_(float, 2, 2, True, False, power = power)
2501 except Exception as e:
2502 raise ValueError(str(e))
2504 # If user selected a named palette: load palette settings
2505 if isinstance(palette, str):
2506 from numpy import where
2507 pals = hclpalettes().get_palettes("Sequential")
2508 idx = where([x.name().upper().replace(" ", "") == palette.upper().replace(" ", "") for x in pals])[0]
2509 if len(idx) == 0:
2510 raise ValueError(f"palette {palette} is not a valid sequential palette. " + \
2511 f"Choose one of: {', '.join([x.name() for x in pals])}")
2512 pal = pals[idx[0]]
2514 def isNone(x): return isinstance(x, type(None))
2516 if isNone(pal.get("h2")): pal.set(h2 = pal.get("h1"))
2518 # Allow to overule few things
2519 for key,value in kwargs.items():
2520 if key in self._allowed_parameters: pal.set(**{key: value})
2522 # Getting settings
2523 settings = pal.get_settings()
2524 else:
2525 # User settings
2526 settings = {}
2527 settings["h1"] = h[0]
2528 settings["h2"] = None if len(h) == 1 else h[1]
2529 if len(c) == 3:
2530 settings["c1"] = c[0]
2531 settings["cmax"] = c[1]
2532 settings["c2"] = c[2]
2533 elif len(c) == 2:
2534 settings["c1"] = c[0]
2535 settings["c2"] = c[1]
2536 else:
2537 settings["c1"] = c[0]
2538 settings["c2"] = 0
2539 settings["l1"] = l[0]
2540 settings["l2"] = l[1]
2541 settings["p1"] = power[0]
2542 settings["p2"] = power[1]
2543 settings["fixup"] = fixup
2544 settings["rev"] = rev
2546 # If keyword arguments are set:
2547 # overwrite the settings if possible.
2548 if kwargs:
2549 for key,val in kwargs.items():
2550 if not key in self._allowed_parameters + ["desc", "gui"]:
2551 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
2552 settings[key] = val
2554 if isinstance(settings["h2"], type(None)): settings["h2"] = settings["h1"]
2556 # Save settings
2557 self.settings = settings
2560 # Return hex colors
2561 def colors(self, n = 11, fixup = None, alpha = None, **kwargs):
2562 """Get Colors
2564 Returns the colors of the current color palette.
2566 Args:
2567 n (int): Number of colors which should be returned.
2568 fixup (None, bool): Should sRGB colors be corrected if they lie
2569 outside the defined color space? If `None` the `fixup`
2570 parameter from the object will be used. Can be set to `True` or
2571 `False` to explicitly control the fixup here.
2572 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
2573 (alpha channel) to the colors. Can be a single float, a list, or a
2574 numpy array. If a list or array is provided it must be of length 1 or
2575 of length `n` and be convertible to float, providing values
2576 between `0.0` (full opacity) and `1.0` (full transparency)
2577 **kwargs: Currently allows for `rev = True` to reverse the colors.
2579 Returns:
2580 list: Returns a list of str with `n` colors from the
2581 color palette.
2583 Examples:
2584 >>> from colorspace import sequential_hcl, hexcols
2585 >>> sequential_hcl().colors()
2586 >>> #: Different sequential palette
2587 >>> cols = sequential_hcl("Rocket").colors(5)
2588 >>> cols
2589 >>> #:
2590 >>> hexcols(cols)
2591 >>> #: Five colors with constant alpha of 0.3
2592 >>> sequential_hcl().colors(5, alpha = 0.3)
2593 >>> #: Three colors with variying alpha
2594 >>> sequential_hcl().colors(3, alpha = [0.2, 0.8, 0.3])
2596 """
2598 from numpy import linspace
2599 from .colorlib import HCL
2601 alpha = self._get_alpha_array(alpha, n)
2602 fixup = fixup if isinstance(fixup, bool) else self.settings["fixup"]
2604 # Calculate H/C/L
2605 p1 = self.get("p1")
2606 p2 = p1 if self.get("p2") is None else self.get("p2")
2607 c1 = self.get("c1")
2608 c2 = 0 if self.get("c2") is None else self.get("c2")
2609 cmax = None if not self.get("cmax") else self.get("cmax")
2610 l1 = self.get("l1")
2611 l2 = l1 if self.get("l2") is None else self.get("l2")
2612 h1 = self.get("h1")
2613 h2 = h1 if self.get("h2") is None else self.get("h2")
2615 # Get colors and create new HCL color object
2616 [H, C, L] = self._get_seqhcl(linspace(1., 0., n), h1, h2, c1, c2, l1, l2, p1, p2, cmax)
2617 HCL = HCL(H, C, L, alpha)
2619 # Reversing colors
2620 rev = self._rev
2621 if "rev" in kwargs.keys(): rev = kwargs["rev"]
2623 # Return hex colors
2624 return HCL.colors(fixup = fixup, rev = rev)
2627# -------------------------------------------------------------------
2628# The rainbow class extends the qualitative_hcl class.
2629# -------------------------------------------------------------------
2630class heat_hcl(sequential_hcl):
2631 """HCL Based Heat Color Palette
2633 `heat_hcl` is an implementation of the base _R_ 'heat.colors' palette but
2634 constructed in HCL space based on a call to :py:class:`sequential_hcl`.
2636 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
2637 :py:class:`diverging_hcl`, :py:class:`divergingx_hcl`,
2638 :py:class:`rainbow_hcl`, :py:class:`terrain_hcl`,
2639 :py:class:`diverging_hsv`, and :py:class:`rainbow`.
2641 Args:
2642 h (list of int): Hue parameters (h1/h2).
2643 c (list of int): Chroma parameters (c1/c2).
2644 l (int): Luminance parameters (l1/l2).
2645 power (list of float): Power parameters (p1/p2).
2646 fixup (bool): Only used when converting the HCL colors to hex. Should
2647 RGB values outside the defined RGB color space be corrected?
2648 rev (bool): Should the color map be reversed.
2649 *args: Currently unused.
2650 **kwargs: Additional arguments to overwrite the h/c/l settings.
2651 Allowed: `h1`, `h2`, `c1`, `c2`, `l1`, `l2`, `p1`, `p2`.
2653 Returns:
2654 Initialize new object, no return. Raises a set of errors if the parameters
2655 are misspecified. Note that the object is callable, the default object call
2656 can be used to return hex colors (identical to the `.colors()` method),
2657 see examples.
2659 Example:
2661 >>> from colorspace.palettes import heat_hcl
2662 >>> pal = heat_hcl()
2663 >>> pal.colors(3)
2664 >>> #:
2665 >>> pal.swatchplot(show_names = False, figsize = (5.5, 0.5));
2666 >>> #: The standard call of the object also returns hex colors
2667 >>> heat_hcl()(10)
2668 """
2671 _allowed_parameters = ["h1", "h2", "c1", "c2", "l1", "l2", "p1", "p2"]
2672 _name = "Heat HCL"
2674 def __init__(self, h = [0, 90], c = [100, 30], l = [50, 90], power = [1./5., 1.],
2675 fixup = True, rev = False, *args, **kwargs):
2677 self._set_rev(rev)
2678 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
2680 # _checkinput_ parameters (in the correct order):
2681 # - dtype, length_min, length_max, recycle, nansallowed, **kwargs
2682 try:
2683 h = self._checkinput_(int, 2, 2, False, h = h)
2684 c = self._checkinput_(int, 2, 2, False, c = c)
2685 l = self._checkinput_(int, 2, 2, False, l = l)
2686 power = self._checkinput_(float, 2, 2, False, power = power)
2687 except Exception as e:
2688 raise ValueError(str(e))
2690 # Save settins
2691 self.settings = {"h1": int(h[0]), "h2": int(h[1]),
2692 "c1": int(c[0]), "c2": int(c[1]),
2693 "l1": int(l[0]), "l2": int(l[1]),
2694 "p1": power[0], "p2": power[1],
2695 "fixup": bool(fixup)}
2697 # If keyword arguments are set:
2698 # overwrite the settings if possible.
2699 if kwargs:
2700 for key,val in kwargs.items():
2701 if not key in self._allowed_parameters + ["desc", "gui"]:
2702 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
2703 self.settings[key] = val
2705 self.settings["rev"] = self._rev
2708# -------------------------------------------------------------------
2709# The rainbow class extends the qualitative_hcl class.
2710# -------------------------------------------------------------------
2711class terrain_hcl(sequential_hcl):
2712 """HCL Based Terrain Color Palette
2714 `terrain_hcl` is an implementation of the base _R_ 'terrain.colors' palette but
2715 constructed in HCL space based on a call to :py:class:`sequential_hcl`.
2717 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
2718 :py:class:`diverging_hcl`, :py:class:`divergingx_hcl`,
2719 :py:class:`rainbow_hcl`, :py:class:`heat_hcl`, :py:class:`diverging_hsv`,
2720 and :py:class:`rainbow`.
2722 Args:
2723 h (list of int): Hue parameters (h1/h2).
2724 c (list of int): Chroma parameters (c1/c2).
2725 l (int): Luminance parameters (l1/l2).
2726 power (list of float): Power parameters (p1/p2).
2727 fixup (bool): Only used when converting the HCL colors to hex. Should
2728 RGB values outside the defined RGB color space be corrected?
2729 rev (bool): Should the color map be reversed.
2730 *args: unused.
2731 **kwargs: Additional arguments to overwrite the h/c/l settings.
2732 Allowed: `h1`, `h2`, `c1`, `c2`, `l1`, `l2`, `p1`, `p2`.
2734 Returns:
2735 Initialize new object, no return. Raises a set of errors if the parameters
2736 are misspecified. Note that the object is callable, the default object call
2737 can be used to return hex colors (identical to the `.colors()` method),
2738 see examples.
2740 Example:
2742 >>> from colorspace import terrain_hcl
2743 >>> pal = terrain_hcl()
2744 >>> pal.colors(10)
2745 >>> #:
2746 >>> pal.swatchplot(show_names = False, figsize = (5.5, 0.5));
2747 >>> #: The standard call of the object also returns hex colors
2748 >>> terrain_hcl()(10)
2749 """
2751 _allowed_parameters = ["h1", "h2", "c1", "c2", "l1", "l2", "p1", "p2"]
2752 _name = "Terrain HCL"
2754 def __init__(self, h = [130, 0], c = [80, 0], l = [60, 95], power = [1./10., 1.],
2755 fixup = True, rev = False, *args, **kwargs):
2757 self._set_rev(rev)
2758 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
2760 # _checkinput_ parameters (in the correct order):
2761 # dtype, length = None, recycle = False, nansallowed = False, **kwargs
2762 try:
2763 h = self._checkinput_(int, 2, 2, False, h = h)
2764 c = self._checkinput_(int, 2, 2, False, c = c)
2765 l = self._checkinput_(int, 2, 2, False, l = l)
2766 power = self._checkinput_(float, 2, 2, False, power = power)
2767 except Exception as e:
2768 raise ValueError(str(e))
2770 # Save settins
2771 self.settings = {"h1": int(h[0]), "h2": int(h[1]),
2772 "c1": int(c[0]), "c2": int(c[1]),
2773 "l1": int(l[0]), "l2": int(l[1]),
2774 "p1": power[0], "p2": power[1],
2775 "fixup": bool(fixup)}
2777 # If keyword arguments are set:
2778 # overwrite the settings if possible.
2779 if kwargs:
2780 for key,val in kwargs.items():
2781 if not key in self._allowed_parameters + ["desc", "gui"]:
2782 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
2783 self.settings[key] = val
2785 self.settings["rev"] = self._rev
2788class diverging_hsv(hclpalette):
2789 """Diverging HSV Color Palettes
2791 `diverging_hsv` provides an HSV-based version of :py:class:`diverging_hcl`.
2792 Its purpose is mainly didactic to show that HSV-based diverging palettes
2793 are less appealing, more difficult to read and more flashy than HCL-based
2794 diverging palettes.
2796 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
2797 :py:class:`diverging_hcl`, :py:class:`divergingx_hcl`,
2798 :py:class:`rainbow_hcl`, :py:class:`heat_hcl`, :py:class:`terrain_hcl`, and
2799 :py:class:`rainbow`.
2801 Args:
2802 h (list of numerics): Hue values, diverging color palettes should have
2803 different hues for both ends of the palette. If only one value is present
2804 it will be recycled ending up in a diverging color palette with the same
2805 colors on both ends. If more than two values are provided the first two
2806 will be used while the rest is ignored. If input `h` is a str this
2807 argument acts like the `palette` argument (see `palette` input
2808 parameter).
2809 s (float, int): Saturation value for the two ends of the palette.
2810 v (float, int): Value (the HSV value) of the colors.
2811 power (numeric): Power parameter for non-linear behaviour of the color
2812 palette.
2813 fixup (bool): Only used when converting the HCL colors to hex. Should
2814 RGB values outside the defined RGB color space be corrected?
2815 rev (bool): Should the color map be reversed.
2816 *args: Unused.
2817 **kwargs: Additional arguments to overwrite the h/c/l settings.
2818 Allowed: `h1`, `h2`, `s`, `v`.
2820 Returns:
2821 Initialize new object, no return. Raises a set of errors if the parameters
2822 are misspecified. Note that the object is callable, the default object call
2823 can be used to return hex colors (identical to the `.colors()` method),
2824 see examples.
2826 Example:
2828 >>> from colorspace import diverging_hsv
2829 >>> pal = diverging_hsv()
2830 >>> pal.colors(10)
2831 >>> #:
2832 >>> pal.swatchplot(show_names = False, figsize = (5.5, 0.5));
2833 >>> #: The standard call of the object also returns hex colors
2834 >>> diverging_hsv()(10)
2835 >>> #: Manually modified palette from 'cyan' to 'orange'
2836 >>> diverging_hsv(h = [180, 30]).swatchplot(
2837 >>> n = 7, show_names = False, figsize = (5.5, 0.5))
2838 >>> #: Additionally, lower saturation on the two ends
2839 >>> diverging_hsv(h = [180, 30], s = 0.4).swatchplot(
2840 >>> n = 7, show_names = False, figsize = (5.5, 0.5))
2841 >>> #: Lowering the value
2842 >>> diverging_hsv(h = [180, 30], s = 0.4, v = 0.75).swatchplot(
2843 >>> n = 7, show_names = False, figsize = (5.5, 0.5))
2844 """
2846 _allowed_parameters = ["h1", "h2", "s", "v"]
2847 _name = "Diverging HSV"
2849 def __init__(self, h = [240, 0], s = 1., v = 1., power = 1.,
2850 fixup = True, rev = False, *args, **kwargs):
2852 self._set_rev(rev)
2853 if not isinstance(fixup, bool): raise TypeError("argument `fixup` must be bool")
2855 # Doing all the sanity checks.
2856 if not isinstance(s, (float, int)):
2857 raise TypeError("argument 's' must be float or int")
2858 s = float(s)
2859 if s < 0. or s > 1.: raise ValueError("argument 's' must be in [0., 1.]")
2861 if not isinstance(v, (float, int)):
2862 raise TypeError("argument 'v' must be float or int")
2863 v = float(v)
2864 if v < 0. or v > 1.: raise ValueError("argument 'v' must be in [0., 1.]")
2866 # _checkinput_ parameters (in the correct order):
2867 # dtype, length = None, recycle = False, nansallowed = False, **kwargs
2868 try:
2869 h = self._checkinput_(int, 2, 2, False, h = h)
2870 power = self._checkinput_(float, 1, 1, False, power = power)[0]
2871 except Exception as e:
2872 raise ValueError(str(e))
2874 # Save settins
2875 self.settings = {"h1": int(h[0]), "h2": int(h[1]),
2876 "s": s, "v": v,
2877 "power": power, "fixup": fixup}
2879 # If keyword arguments are set:
2880 # overwrite the settings if possible.
2881 if kwargs:
2882 for key,val in kwargs.items():
2883 if not key in self._allowed_parameters + ["desc", "gui"]:
2884 raise ValueError(f"argument `{key}` not allowed for {type(self).__name__}")
2885 self.settings[key] = val
2887 self.settings["rev"] = self._rev
2891 # Return hex colors
2892 def colors(self, n = 11, fixup = None, alpha = None, **kwargs):
2893 """Get Colors
2895 Returns the colors of the current color palette.
2897 Args:
2898 n (int): Number of colors which should be returned.
2899 fixup (None, bool): Should sRGB colors be corrected if they lie
2900 outside the defined color space? If `None` the `fixup`
2901 parameter from the object will be used. Can be set to `True` or
2902 `False` to explicitly control the fixup here.
2903 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
2904 (alpha channel) to the colors. Can be a single float, a list, or a
2905 numpy array. If a list or array is provided it must be of length 1 or
2906 of length `n` and be convertible to float, providing values
2907 between `0.0` (full opacity) and `1.0` (full transparency)
2908 **kwargs: Currently allows for `rev = True` to reverse the colors.
2910 Returns:
2911 list: Returns a list of str with `n` colors from the
2912 color palette.
2914 """
2916 from numpy import linspace, power, abs, repeat, where
2917 from numpy import ndarray, ndenumerate
2918 from .colorlib import HSV
2920 alpha = self._get_alpha_array(alpha, n)
2921 fixup = fixup if isinstance(fixup, bool) else self.settings["fixup"]
2923 # Calculate palette
2924 rval = linspace(-self.get("s"), self.get("s"), n)
2926 # Calculate H, S, V coordinates
2927 H = repeat(self.get("h1"), n)
2928 H[where(rval > 0)] = self.get("h2")
2929 S = power(abs(rval), self.get("power"))
2930 V = repeat(self.get("v"), n)
2932 # Generate color object
2933 HSV = HSV(H, S, V, alpha)
2934 HSV.to("RGB") # Force to go trough RGB (not sRGB directly)
2936 # Reversing colors
2937 rev = self._rev
2938 if "rev" in kwargs.keys(): rev = kwargs["rev"]
2940 # Return hex colors
2941 return HSV.colors(fixup = fixup, rev = rev)
2945# -------------------------------------------------------------------
2946# -------------------------------------------------------------------
2947class rainbow(hclpalette):
2948 """Infamous sRGB Rainbow Color Palette
2950 Implements the (in-)famous rainbow (or jet) color palette that was used
2951 very frequently in many software packages but has been widely criticized
2952 for its many perceptual problems. It is specified by a `start` and `end`
2953 hue $\in [0.-1.]$ with `red = 0`, `yellow = 1/6`, `green = 2/6`, `cyan =
2954 3/6`, blue = `4/6`, and `magenta = 5/6`. However, these are very flashy and
2955 unbalanced with respect to both chroma and luminance which can lead to
2956 various optical illusions. Also, the hues that are equispaced in RGB space
2957 tend to cluster at the red, green, and blue primaries. Therefore, it is
2958 recommended to use a suitable palette from `hcl.colors` instead of
2959 `rainbow`.
2961 `start` and/or `end` both allow for lambda functions with one single
2962 argument `n` (number of colors), see examples.
2964 See also: :py:class:`qualitative_hcl`, :py:class:`sequential_hcl`,
2965 :py:class:`diverging_hcl`, :py:class:`divergingx_hcl`,
2966 :py:class:`rainbow_hcl`, :py:class:`heat_hcl`, :py:class:`terrain_hcl`, and
2967 :py:class:`diverging_hsv`.
2969 Args:
2970 s (float, int): saturation value, a value in `[0., 1.]`. Defaults to `1.0`.
2971 v (float, int): value, a value in `[0., 1.]`. Defaults to `1.0`.
2972 start (float, int, function): the (corrected) hue in `[0., 1.]` at which
2973 the rainbow begins. Defaults to `0.`. Can be a function with one input
2974 `n` (number of colors). If outside `[0., 1.]` it will be wrapped.
2975 end (float, int, function): the (corrected) hue in `[0., 1.]` at which
2976 the rainbow ends. Defaults to `0.`. Can be a function with one input
2977 `n` (number of colors). If outside `[0., 1.]` it will be wrapped.
2978 rev (bool): Should the color map be reversed.
2979 *args: Unused.
2980 **kwargs: Unused.
2982 Returns:
2983 Initialize new object, no return. Raises a set of errors if the parameters
2984 are misspecified. Note that the object is callable, the default object call
2985 can be used to return hex colors (identical to the `.colors()` method),
2986 see examples.
2988 Example:
2990 >>> from colorspace import rainbow
2991 >>> pal = rainbow()
2992 >>> pal.colors(10)
2993 >>> #:
2994 >>> pal.swatchplot(show_names = False, figsize = (5.5, 0.5));
2995 >>> #: The standard call of the object also returns hex colors
2996 >>> rainbow()(10)
2997 >>>
2998 >>> #: Using lambda functions for start/end
2999 >>> p = rainbow(start = lambda n: 1 / n, end = lambda n: 1 - 1 / n)
3000 >>> p.swatchplot(n = 5, show_names = False, figsize = (5.5, 0.5));
3001 >>> #:
3002 >>> p.swatchplot(n = 10, show_names = False, figsize = (5.5, 0.5));
3003 >>> #:
3004 >>> p.specplot(rgb = True, figsize = (8, 6))
3006 Raises:
3007 TypeError: If `s` or `v` are not float or int.
3008 ValueError: If `s` or `v` are outside range, must be in `[0., 1.]`.
3009 TypeError: If `start` and `end` are not float/int in `[0., 1.]` or lambda functions.
3010 TypeError: If `rev` is not bool.
3011 """
3013 _allowed_parameters = ["s", "v", "start", "end"]
3014 _name = "Diverging HCL"
3016 def __init__(self, s = 1, v = 1, start = 0, end = lambda n: max(1., n - 1.) / n,
3017 rev = False, *args, **kwargs):
3019 self._set_rev(rev)
3021 # Doing all the sanity checks.
3022 if not isinstance(s, (float, int)):
3023 raise TypeError("argument 's' must be float or int")
3024 s = float(s)
3025 if s < 0. or s > 1.: raise ValueError("argument 's' must be in [0., 1.]")
3027 if not isinstance(v, (float, int)):
3028 raise TypeError("argument 'v' must be float or int")
3029 v = float(v)
3030 if v < 0. or v > 1.: raise ValueError("argument 'v' must be in [0., 1.]")
3032 if not isinstance(start, (float, int)) and not callable(start):
3033 raise TypeError("argument `start` must be float, int, or lambda function")
3034 if not isinstance(end, (float, int)) and not callable(end):
3035 raise TypeError("argument `end` must be float, int, or lambda function")
3037 # Check start and end. Either functions (lambda functions)
3038 # or a single floating point number in [0-1].
3039 if callable(start): pass
3040 elif isinstance(start, (float, int)): start = float(start)
3041 if callable(end): pass
3042 elif isinstance(end, (float, int)): end = float(end)
3044 # Store these settings
3045 self.settings = {"s": float(s), "v": float(v), "start": start, "end": end, "rev": self._rev}
3048 # Return hex colors
3049 def colors(self, n = 11, alpha = None, **kwargs):
3050 """Get Colors
3052 Returns the colors of the current color palette.
3054 Args:
3055 n (int): Number of colors which should be returned. Defaults to `11`.
3056 alpha (None, float, list, or numpy.ndarray): Allows to add an transparency
3057 (alpha channel) to the colors. Can be a single float, a list, or a
3058 numpy array. If a list or array is provided it must be of length 1 or
3059 of length `n` and be convertible to float, providing values
3060 between `0.0` (full opacity) and `1.0` (full transparency)
3061 **kwargs: Currently allows for `rev = True` to reverse the colors.
3063 Returns:
3064 list: Returns a list of str with `n` colors from the
3065 color palette.
3067 Examples:
3068 >>> from colorspace import rainbow
3069 >>> rainbow().colors(4)
3071 Raises:
3072 ValueError: If input `n` is not float/int or smaller than 1.
3073 """
3075 from numpy import linspace, mod, repeat
3076 from .colorlib import HSV
3078 if not isinstance(n, int):
3079 raise TypeError("argument `n` must be int")
3080 n = int(n)
3081 if n <= 0: raise ValueError("argument `n` must be positive (>= 1)")
3083 alpha = self._get_alpha_array(alpha, n)
3085 # Evaluate start and end given 'n'
3086 start = self.settings["start"]
3087 start = start(n) if callable(start) else start
3088 end = self.settings["end"]
3089 end = end(n) if callable(end) else end
3091 h = mod(linspace(start, end, n), 1.)
3092 s = repeat(self.settings["s"], n)
3093 v = repeat(self.settings["v"], n)
3095 # Generate HSV colorobject
3096 HSV = HSV(h * 360., s, v, alpha)
3097 # If kwargs have a key "colorobject" return HCL colorobject
3098 if "colorobject" in kwargs.keys(): return HSV
3100 # Reversing colors if needed
3101 rev = self._rev
3102 if "rev" in kwargs.keys(): rev = kwargs["rev"]
3104 # Return hex colors
3105 return HSV.colors(fixup = False, rev = rev)