Source code for moniplot.tol_colors

#
# https://github.com/rmvanhees/moniplot.git
#
# Copyright (c) 2019-2022, Paul Tol
#
# License:  GPLv3
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
This module contains the definitions of color schemes and color set provided
by `Paul Tol <https://personal.sron.nl/~pault/>`_.

Routines in this module::

   tol_cmap(colormap=None, lut=0)
   tol_cset(colorset=None)
   tol_rgba(cname, cnum=None)
"""
__all__ = ['tol_cmap', 'tol_cset', 'tol_rgba']

from dataclasses import astuple, dataclass, field

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.cm import ScalarMappable
from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba_array


# pylint: disable=too-many-instance-attributes
# - Define color maps -------------------------
@dataclass
class SunSet:
    """Defines diverging color scheme 'SunSet'
    """
    bad_color: str = '#FFFFFF'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#364B9A', '#4A7BB7', '#6EA6CD', '#98CAE1', '#C2E4EF',
            '#EAECCC', '#FEDA8B', '#FDB366', '#F67E4B', '#DD3D2D', '#A50026')


@dataclass
class NightFall:
    """Defines diverging color scheme 'NightFall'.
    """
    bad_color: str = '#FFFFFF'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#125A56', '#00767B', '#238F9D', '#42A7C6', '#60BCE9',
            '#9DCCEF', '#C6DBED', '#DEE6E7', '#ECEADA', '#F0E6B2',
            '#F9D576', '#FFB954', '#FD9A44', '#F57634', '#E94C1F',
            '#D11807', '#A01813')


@dataclass
class BuRd:
    """Defines diverging color scheme 'BuRd'.
    """
    bad_color: str = '#FFEE99'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#2166AC', '#4393C3', '#92C5DE', '#D1E5F0', '#F7F7F7',
            '#FDDBC7', '#F4A582', '#D6604D', '#B2182B')


@dataclass
class PRGn:
    """Defines diverging color scheme 'PRGn'.
    """
    bad_color: str = '#FFEE99'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#762A83', '#9970AB', '#C2A5CF', '#E7D4E8', '#F7F7F7',
            '#D9F0D3', '#ACD39E', '#5AAE61', '#1B7837')


@dataclass
class YlOrBr:
    """Defines sequential color scheme 'YlOrBr'.
    """
    bad_color: str = '#888888'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FB9A29',
            '#EC7014', '#CC4C02', '#993404', '#662506')


@dataclass
class WhOrBr:
    """Defines sequential color scheme 'WhOrBr'.
    """
    bad_color: str = '#888888'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#FFFFFF', '#FFF7BC', '#FEE391', '#FEC44F', '#FB9A29',
            '#EC7014', '#CC4C02', '#993404', '#662506')


@dataclass
class IriDescent:
    """Defines sequential color scheme 'IriDescent'.
    """
    bad_color: str = '#999999'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#FEFBE9', '#FCF7D5', '#F5F3C1', '#EAF0B5', '#DDECBF',
            '#D0E7CA', '#C2E3D2', '#B5DDD8', '#A8D8DC', '#9BD2E1',
            '#8DCBE4', '#81C4E7', '#7BBCE7', '#7EB2E4', '#88A5DD',
            '#9398D2', '#9B8AC4', '#9D7DB2', '#9A709E', '#906388',
            '#805770', '#684957', '#46353A')


@dataclass
class RainbowPuRd:
    """Defines sequential color scheme 'RainbowPuRd'.
    """
    bad_color: str = '#FFFFFF'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
            '#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
            '#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
            '#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
            '#DF4828', '#DA2222')


@dataclass
class RainbowPuBr:
    """Defines sequential color scheme 'RainbowPuBr'.
    """
    bad_color: str = '#FFFFFF'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
            '#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
            '#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
            '#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
            '#DF4828', '#DA2222', '#B8221E', '#95211B', '#721E17',
            '#521A13')


@dataclass
class RainbowWhRd:
    """Defines sequential color scheme 'RainbowWhRd'.
    """
    bad_color: str = '#666666'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#E8ECFB', '#DDD8EF', '#D1C1E1', '#C3A8D1', '#B58FC2',
            '#A778B4', '#9B62A7', '#8C4E99', '#6F4C9B', '#6059A9',
            '#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC', '#549EB3',
            '#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
            '#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39',
            '#E78C35', '#E67932', '#E4632D', '#DF4828', '#DA2222')


@dataclass
class RainbowWhBr:
    """Defines sequential color scheme 'RainbowWhBr'.
    """
    bad_color: str = '#666666'
    colors: tuple[str] = field(default_factory=tuple)

    def __post_init__(self):
        self.colors += (
            '#E8ECFB', '#DDD8EF', '#D1C1E1', '#C3A8D1', '#B58FC2',
            '#A778B4', '#9B62A7', '#8C4E99', '#6F4C9B', '#6059A9',
            '#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC', '#549EB3',
            '#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
            '#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39',
            '#E78C35', '#E67932', '#E4632D', '#DF4828', '#DA2222',
            '#B8221E', '#95211B', '#721E17', '#521A13')


@dataclass
class RainbowDiscrete:
    """Defines sequential color scheme 'RainbowDiscrete'.
    """
    bad_color: str = '#777777'
    colors: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.set_lut()

    def set_lut(self, lut: int = 22):
        """Define list of colors of 'rainbow_discrete'.
        """
        hexclrs = (
            '#E8ECFB', '#D9CCE3', '#D1BBD7', '#CAACCB', '#BA8DB4',
            '#AE76A3', '#AA6F9E', '#994F88', '#882E72', '#1965B0',
            '#437DBF', '#5289C7', '#6195CF', '#7BAFDE', '#4EB265',
            '#90C987', '#CAE0AB', '#F7F056', '#F7CB45', '#F6C141',
            '#F4A736', '#F1932D', '#EE8026', '#E8601C', '#E65518',
            '#DC050C', '#A5170E', '#72190E', '#42150A')

        indexes = [
            [9],
            [9, 25],
            [9, 17, 25],
            [9, 14, 17, 25],
            [9, 13, 14, 17, 25],
            [9, 13, 14, 16, 17, 25],
            [8, 9, 13, 14, 16, 17, 25],
            [8, 9, 13, 14, 16, 17, 22, 25],
            [8, 9, 13, 14, 16, 17, 22, 25, 27],
            [8, 9, 13, 14, 16, 17, 20, 23, 25, 27],
            [8, 9, 11, 13, 14, 16, 17, 20, 23, 25, 27],
            [2, 5, 8, 9, 11, 13, 14, 16, 17, 20, 23, 25],
            [2, 5, 8, 9, 11, 13, 14, 15, 16, 17, 20, 23, 25],
            [2, 5, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25],
            [2, 5, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25, 27],
            [2, 4, 6, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25, 27],
            [2, 4, 6, 7, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25, 27],
            [2, 4, 6, 7, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25, 26,
             27],
            [1, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 16, 17, 19, 21, 23, 25,
             26, 27],
            [1, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 19, 21, 23,
             25, 26, 27],
            [1, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 20, 22,
             24, 25, 26, 27],
            [1, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 20, 22,
             24, 25, 26, 27, 28],
            [0, 1, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 20,
             22, 24, 25, 26, 27, 28]
        ]

        if not 0 < lut <= len(indexes):
            lut = 23

        self.bad_color = '#FFFFFF' if lut < 23 else '#777777'
        self.colors = [hexclrs[i] for i in indexes[lut-1]]


# - Define color sets -------------------------
@dataclass
class Bright:
    """Defines a qualitative colour scheme that is colour-blind safe.
    The main scheme for lines and their labels.
    """
    blue: str = '#4477AA'
    cyan: str = '#66CCEE'
    green: str = '#228833'
    yellow: str = '#CCBB44'
    red: str = '#EE6677'
    purple: str = '#AA3377'
    grey: str = '#BBBBBB'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


@dataclass
class HighContrast:
    """Defines a qualitative colour scheme, an alternative to the bright scheme
    that is colour-blind safe and optimized for contrast. The samples
    underneath are shades of grey with the same luminance; this scheme also
    works well for people with monochrome vision and in a monochrome printout.
    """
    blue: str = '#004488'
    yellow: str = '#DDAA33'
    red: str = '#BB5566'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


@dataclass
class MediumContrast:
    """Defines a qualitative colour scheme, an alternative to the high-contrast
    scheme that is colour-blind safe with more colours. It is also optimized
    for contrast to work in a monochrome printout, but the differences are
    inevitably smaller. It is designed for situations needing colour pairs,
    shown by the three rectangles, with the lower half in the greyscale
    equivalent.
    """
    light_yellow: str = '#EECC66'
    light_red: str = '#EE99AA'
    light_blue: str = '#6699CC'
    dark_yellow: str = '#997700'
    dark_red: str = '#994455'
    dark_blue: str = '#004488'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


@dataclass
class Vibrant:
    """Defines a qualitative colour scheme, an alternative to the bright scheme
    that is equally colour-blind safe. It has been designed for data
    visualization framework TensorBoard, built around their signature orange
    FF7043. That colour has been replaced here to make it print-friendly.
    """
    blue: str = '#0077BB'
    cyan: str = '#33BBEE'
    teal: str = '#009988'
    orange: str = '#EE7733'
    red: str = '#CC3311'
    magenta: str = '#EE3377'
    grey: str = '#BBBBBB'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


@dataclass
class Muted:
    """Defines a qualitative colour scheme, an alternative to the bright scheme
    that is equally colour-blind safe with more colours, but lacking a clear
    red or medium blue. Pale grey is meant for bad data in maps.
    """
    indigo: str = '#332288'
    cyan: str = '#88CCEE'
    teal: str = '#44AA99'
    green: str = '#117733'
    olive: str = '#999933'
    sand: str = '#DDCC77'
    rose: str = '#CC6677'
    wine: str = '#882255'
    purple: str = '#AA4499'
    pale_grey: str = '#DDDDDD'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


@dataclass
class Light:
    """Defines a qualitative colour scheme that is reasonably distinct in both
    normal and colour-blind vision. It was designed to fill labelled cells with
    more and lighter colours than contained in the bright scheme, using more
    distinct colours than that in the pale scheme, but keeping black labels
    clearly readable. However, it can also be used for general qualitative
    maps.
    """
    light_blue: str = '#77AADD'
    light_cyan: str = '#99DDFF'
    mint: str = '#44BB99'
    pear: str = '#BBCC33'
    olive: str = '#AAAA00'
    light_yellow: str = '#EEDD88'
    orange: str = '#EE8866'
    pink: str = '#FFAABB'
    pale_grey: str = '#DDDDDD'
    black: str = '#000000'

    @property
    def colors(self):
        """Return tuple with colors."""
        return astuple(self)


_cset_dict = {
    'bright': Bright(),
    'high-contrast': HighContrast(),
    'medium-contrast': MediumContrast(),
    'vibrant': Vibrant(),
    'muted': Muted(),
    'light': Light()
}

_cmap_dict = {
    'sunset': SunSet(),
    'nightfall': NightFall(),
    'BuRd': BuRd(),
    'PRGn': PRGn(),
    'YlOrBr': YlOrBr(),
    'WhOrBr': WhOrBr(),
    'iridescent': IriDescent(),
    'rainbow_PuRd': RainbowPuRd(),
    'rainbow_PuBr': RainbowPuBr(),
    'rainbow_WhRd': RainbowWhRd(),
    'rainbow_WhBr': RainbowWhBr()
}


# - functions --------------------------------------
[docs]def tol_cmap(colormap: str = None, lut: int = 0): """Continuous and discrete color sets for ordered data. Definition of colour schemes for lines which also work for colour-blind people. See `https://personal.sron.nl/~pault/` for background information and best usage of the schemes. Parameters ---------- colormap : str, optional Return predefined colormap with given name. If not given, all possible values for colormap. lut : int, default=0 Number of discrete colors in colormap. Parameter lut is ignored for all colormaps except 'rainbow_discrete'. Returns ------- matplotlib.colormaps Examples -------- Typical usage:: > cmap = tol_cmap('nightfall') """ if colormap is None: return _cmap_dict.keys() if colormap == 'rainbow_discrete': cclass = RainbowDiscrete() cclass.set_lut(lut) clrs = to_rgba_array(cclass.colors) clrs = np.vstack([clrs[0], clrs, clrs[-1]]) cdict = {} for ii, key in enumerate(('red', 'green', 'blue')): cdict[key] = [(jj / (len(clrs)-2.), clrs[jj, ii], clrs[jj+1, ii]) for jj in range(len(clrs)-1)] cmap = LinearSegmentedColormap('rainbow_discrete', cdict) cmap.set_bad(cclass.bad_color) return cmap cclass = _cmap_dict.get(colormap, 'nightfall') cmap = LinearSegmentedColormap.from_list(colormap, cclass.colors) cmap.set_bad(cclass.bad_color) return cmap
[docs]def tol_cset(colorset: str = None) -> dataclass: """Discrete color sets for qualitative data. Definition of colour schemes for lines which also work for colour-blind people. See `https://personal.sron.nl/~pault/` for background information and best usage of the schemes. Parameters ---------- colorset : str if None then the names of all color-sets are returned. Returns ------- dataclass: a dataclass instance with colors as hexadecimal values. Examples -------- Typical usage:: > cset = tol_cset('bright') """ if colorset is None: return _cset_dict.keys() return _cset_dict.get(colorset, Bright())
[docs]def tol_rgba(cname: str, lut: int | None = None) -> list[str, ...]: """ Parameters ---------- cname : str Name of a colormap or colorset. lut : int, default=0 Number of discrete colors in colormap (*not colorset*). """ if cname == 'rainbow_discrete': cclass = RainbowDiscrete() cclass.set_lut(lut) elif cname in _cset_dict: cclass = _cset_dict.get(cname) else: cclass = _cmap_dict.get(cname, 'nightfall') if lut is None: return to_rgba_array(cclass.colors) cmap = tol_cmap(cname) cnorm = Normalize(vmin=0, vmax=lut-1) scalar_map = ScalarMappable(norm=cnorm, cmap=cmap) return [scalar_map.to_rgba(i) for i in range(lut)]
# - test functions ------------------------- def __show_cset(): """Show colormaps tol_cset(). """ schemes = tol_cset() fig, axes = plt.subplots(ncols=len(schemes), figsize=(9, 3)) fig.subplots_adjust(top=0.9, bottom=0.02, left=0.02, right=0.92) for axx, scheme in zip(axes, schemes): cset = tol_cset(scheme) for name, color in cset.__dict__.items(): axx.scatter([], [], c=color, s=80, label=name) axx.set_axis_off() axx.legend(loc=2) axx.set_title(scheme) plt.show() def __show_cmap(): """Show colormaps tol_cmap(<non-discrete>). """ schemes = tol_cmap() gradient = np.linspace(0, 1, 256) gradient = np.vstack((gradient, gradient)) fig, axes = plt.subplots(nrows=len(schemes)) fig.subplots_adjust(top=0.98, bottom=0.02, left=0.29, right=0.99) for axx, scheme in zip(axes, schemes): pos = list(axx.get_position().bounds) axx.set_axis_off() axx.imshow(gradient, aspect=4, cmap=tol_cmap(scheme)) fig.text(pos[0] - 0.01, pos[1] + pos[3]/2., scheme, va='center', ha='right', fontsize=10) plt.show() def __show_discrete(): """Show colormaps tol_cmap('rainbow_discrete', <lut>). """ gradient = np.linspace(0, 1, 256) gradient = np.vstack((gradient, gradient)) fig, axes = plt.subplots(nrows=23) fig.subplots_adjust(top=0.98, bottom=0.02, left=0.25, right=0.99) for lut, axx in enumerate(axes, start=1): pos = list(axx.get_position().bounds) axx.set_axis_off() axx.imshow(gradient, aspect=4, cmap=tol_cmap('rainbow_discrete', lut)) fig.text(pos[0] - 0.01, pos[1] + pos[3]/2., f'rainbow_discrete, {lut}', va='center', ha='right', fontsize=10) plt.show() def __show_rgba(): """Show usage of tol_rgba(). """ cname = 'sunset' print(cname, clrs := tol_rgba(cname, 16)) fig = plt.figure() axx = fig.add_subplot(111) axx.set_prop_cycle(color=clrs) for ii in range(len(clrs)): axx.plot(np.arange(10)*(ii+1)) plt.show() cname = 'muted' print(cname, clrs := tol_rgba(cname)) fig = plt.figure() axx = fig.add_subplot(111) axx.set_prop_cycle(color=clrs) for ii in range(len(clrs)): axx.plot(np.arange(10)*(ii+1)) plt.show() cname = 'rainbow_discrete' print(cname, clrs := tol_rgba(cname, 17)) if clrs is not None: fig = plt.figure() axx = fig.add_subplot(111) axx.set_prop_cycle(color=clrs) for ii in range(len(clrs)): axx.plot(np.arange(10)*(ii+1)) plt.show() if __name__ == '__main__': __show_cset() __show_cmap() __show_discrete() __show_rgba()