# Copyright (c) SecretBiology 2020.
#
# Library Name: SecretColors
# Author: Rohit Suratekar
# Website: https://github.com/secretBiology/SecretColors
#
#
import random
from SecretColors.data.constants import DIV_COLOR_PAIRS
from SecretColors.helpers.logging import Log
from SecretColors.models.palette import Palette
from typing import List
[docs]class ColorMapParent:
"""
This is parent class which will be inherited by all ColorMap objects. It
includes all basic methods which will be common to all the ColorMaps.
.. danger::
Do not use this class in your workflow. This class is intended as a
parent class which you can inherit to make new colormaps. For
general purpose use, you should use
:class:`~SecretColors.cmaps.ColorMap` instead.
"""
[docs] def __init__(self, matplotlib,
palette: Palette = None,
log: Log = None,
seed=None):
"""
Initializing of any ColorMap.
:param matplotlib: matplotlib object from matplotlib library
:param palette: Palette from which you want colors
:type palette: Palette
:param log: Log class
:type log: Log
:param seed: Seed for random number generation
"""
self._mat = matplotlib
if log is None:
log = Log()
self.log = log
if palette is None:
palette = Palette()
self.log.info(f"ColorMap will use '{palette.name}' palette")
self._palette = palette
self._seed = seed
if seed is not None:
random.seed(seed)
self.log.info(f"Random seed set for : {seed}")
self.no_of_colors = 10
@property
def data(self) -> dict:
"""Returns all available ColorMap data. This is valid ONLY for
special subclass (e.g. BrewerMap). It will return None for 'ColorMap'
class.
:rtype: dict
"""
raise NotImplementedError
@property
def seed(self):
return self._seed
@seed.setter
def seed(self, value):
"""
Seed for random number generator
:param value: Seed value
"""
self._seed = value
random.seed(value)
self.log.info(f"Random seed set for : {value}")
@property
def palette(self) -> Palette:
"""
:return: Returns current palette from which colors are drawn
:rtype: Palette
"""
return self._palette
@palette.setter
def palette(self, palette: Palette):
"""
Set Palette from which colors will be drawn
Note: Do not set this for special subclasses (like BrewerMap)
:param palette: Color Palette
:type palette: Palette
"""
self._palette = palette
self.log.info(f"ColorMap is now using '{palette.name}' palette")
@property
def get_all(self) -> list:
"""Returns list of available special colormaps. This works only with
special subclasses like BrewerMap.
:return: List of colormap names
:rtype: List[str]
"""
if self.data is None:
return []
else:
return list(self.data.keys())
def _get_linear_segment(self, color_list: list):
"""
:param color_list: List of colors
:return: LinearSegmentedColormap
"""
try:
return self._mat.colors.LinearSegmentedColormap.from_list(
"secret_color", color_list)
except AttributeError:
raise Exception("Matplotlib is required to use this function")
def _get_listed_segment(self, color_list: list):
"""
:param color_list: List of colors
:return: ListedColormap
"""
try:
return self._mat.colors.ListedColormap(color_list)
except AttributeError:
raise Exception("Matplotlib is required to use this function")
def _derive_map(self, color_list: list,
is_qualitative=False,
is_reversed=False):
"""
:param color_list: List of colors
:param is_qualitative: If True, makes listed colormap
:param is_reversed: Reverses the order of color in Colormap
:return: Colormap which can be directly used with matplotlib
"""
if is_reversed:
color_list = [x for x in reversed(color_list)]
if is_qualitative:
return self._get_listed_segment(color_list)
else:
return self._get_linear_segment(color_list)
def _get_colors(self, key: str, no_of_colors: int, backup: str,
staring_shade, ending_shade):
if no_of_colors < 2:
self.log.error("Minimum of 2 colors are required for generating "
"Colormap", exception=ValueError)
colors = None
# First check if for given combinations of parameters, colors are
# available
if self.data is not None:
if key in self.data:
if str(no_of_colors) in self.data[key]:
colors = self.data[key][str(no_of_colors)]
self.log.info("Colormap for given combination found")
if colors is None:
self.log.info("Colormap for given combination not found.")
self.log.info("Searching standard colors")
# Just make request for the color so that additional colors will
# be added to palette
self.palette.get(backup)
if (staring_shade is not None or
ending_shade is not None or
colors is None):
self.log.warn("Overriding the available standard Colormaps "
"because starting_shade or ending_shade is provided")
if staring_shade is None:
staring_shade = min(self.palette.colors[
backup].get_all_shades())
if ending_shade is None:
ending_shade = max(
self.palette.colors[backup].get_all_shades())
return self.palette.get(backup, no_of_colors=no_of_colors,
starting_shade=staring_shade,
ending_shade=ending_shade)
return colors
[docs] def get_colors(self, name: str, no_of_colors: int) -> list:
"""
This is easy way to get the available colors in current colormap
.. code-block:: python
cm = BrewerMap(matplotlib)
cm.get_colors('Spectral', 9) # Returns 9 'Spectral' colors from BrewerMap colormap
.. warning::
Be careful in using `no_of_colors` argument. It actually
points
to number of colors available in given colormap. For example,
'Tableau' map from :class:`~SecretColors.cmaps.TableauMap`
contains two list of colors, 10 and 20. So you need to enter
either 10 or 20. Any other number will raise ValueError. You can
check which all options are available by :attr:`get_all`
property. More about this can be read in documentation of
:func:`~SecretColors.cmaps.parent.ColorMapParent.get` function.
:param name: Name of the special colormap
:type name: str
:param no_of_colors: Number of colors (see warning above)
:type no_of_colors: int
:return: List of colors
:rtype: List[str]
:raises: ValueError (if used on
:class:`~SecretColors.cmaps.ColorMap` or wrong
`no_of_colors` provided)
"""
if self.data is not None:
if name not in self.data.keys():
self.log.error(f"'{name}' is not available in current "
f"colormap. Following are allowed arguments "
f"here: {self.get_all}")
if str(no_of_colors) not in self.data[name].keys():
n = list(self.data[name].keys())
if "type" in n:
n.remove("type")
n = [int(x) for x in n]
self.log.error(f"Currently following number of colors are "
f"allowed for {name}. : {n}")
if str(no_of_colors) in self.data[name]:
return self.data[name][str(no_of_colors)]
else:
raise KeyError(
f"This palette did not have this key '{no_of_colors}'")
return []
def _default(self, name, backup, kwargs):
if "self" in kwargs:
del kwargs['self']
if "starting_shade" not in kwargs:
kwargs["starting_shade"] = None
if "ending_shade" not in kwargs:
kwargs["ending_shade"] = None
no_of_colors = kwargs['no_of_colors'] or self.no_of_colors
bak_name = backup or name
colors = self._get_colors(key=name,
no_of_colors=no_of_colors,
backup=bak_name,
staring_shade=kwargs['starting_shade'],
ending_shade=kwargs['ending_shade'])
return self._derive_map(colors,
is_qualitative=kwargs['is_qualitative'],
is_reversed=kwargs['is_reversed'])
def _special_maps(self, name, backup, kwargs):
if name not in self.data.keys():
self.log.error(f"There is no '{name}' colormap in our "
f"database. Following special colormaps are"
f" available in current class :"
f" {list(self.data.keys())}")
no_of_colors = kwargs['no_of_colors'] or self.no_of_colors
cols = list(self.data[name].keys())
if 'type' in cols:
cols.remove('type')
cols = [int(x) for x in cols]
if no_of_colors not in cols:
self.log.error(f"Sorry, for {name} colormap, 'no_of_colors' "
f"argument can "
f"only take these values: {cols}.")
return self._default(name, backup, kwargs)
[docs] def from_list(self, color_list: list, is_qualitative: bool = False,
is_reversed=False):
"""
You can create your own colormap with list of own colors
:param color_list: List of colors
:param is_qualitative: If True, makes listed colormap
:param is_reversed: Reverses the order of color in Colormap
:return: Colormap which can be directly used with matplotlib
"""
return self._derive_map(color_list, is_qualitative, is_reversed)
[docs] def get(self, name: str, *, no_of_colors: int = None,
is_qualitative: bool = False, is_reversed=False):
"""
Get arbitrary color map from current ColorMap object
`no_of_colors` is probably the most important parameter in the
colormap classes. In this library each colormap data is structured
in the form of dictionary as shown below::
data = { 'map_name' : {
'10': [c1, c2, ... c10],
'5' : [b1, b2, ... b5],
...
'type': Type of colormap
}
}
In above example, if you want to access list [c1, c2...c10], you can
do following,
>>> YourMap().get('map_name',no_of_colors=10) # Returns [c1, c2 ...c10]
You can check which all colormaps are
available by :attr:`~SecretColors.cmaps.parent.ColorMapParent.get_all` property
:param name: Exact Name of the Colormap
:type name: str
:param no_of_colors: Number of colors. (See discussion above)
:type no_of_colors: int
:param is_qualitative: If True, listed colormap will be returned. (
default: False)
:type is_qualitative: bool
:param is_reversed: If True, colormap will be reversed. (default:
False)
:type is_reversed: bool
:return: Colormap object
:rtype: :class:`matplotlib.colors.ListedColormap` or :class:`matplotlib.colors.LinearSegmentedColormap`
"""
if self.data is None:
self.log.error(f"This method can only be used with special "
f"colormap. If you are using 'ColorMap' class "
f"directly. You can only use standard maps. or "
f"create your own.")
return self._special_maps(name, None, locals())
def greens(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "green", locals())
def reds(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "red", locals())
def oranges(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "orange", locals())
def purples(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "purple", locals())
def grays(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "gray", locals())
def blues(self, *, starting_shade: float = None,
ending_shade: float = None,
no_of_colors: int = None,
is_qualitative: bool = False,
is_reversed=False):
return self._default(None, "blue", locals())
def random_divergent(self, is_qualitative=False, is_reversed=False):
names = []
if self.data is not None:
for k in self.data:
if self.data[k]["type"] == "div":
names.append(k)
if len(names) > 0:
random.shuffle(names)
keys = list(self.data[names[0]].keys())
keys.remove("type")
random.shuffle(keys)
kwargs = locals()
kwargs["no_of_colors"] = int(keys[0])
return self._special_maps(names[0], None, kwargs)
else:
names = [x for x in DIV_COLOR_PAIRS]
random.shuffle(names)
cols = []
for c in names[0]:
for s in c[1]:
cols.append(self.palette.get(c[0], shade=s))
return self.from_list(cols)
[docs]class ColorMap(ColorMapParent):
"""
This is simple wrapper around
:class:`~SecretColors.cmaps.parent.ColorMapParent`. This wrapper let you
utilize all methods from its parent class. For all general purpose use,
you should use this class. If you want more specialized ColorMaps,
use their respective classes. Following is the simplest use where you
want to visualize your data in typical 'greens' palette
.. code-block:: python
import matplotlib
import matplotlib.pyplot as plt
from SecretColors.cmaps import ColorMap
import numpy as np
cm = ColorMap(matplotlib)
data = np.random.rand(5, 5)
plt.imshow(data, cmap=cm.greens())
plt.colorbar()
plt.show()
You can easily change standard colormaps like following
.. code-block:: python
cm.reds() # Reds colormap
cm.oranges() # Oranges colormap
cm.blues() # Blues colormap
cm.grays() # Grays colormap
All standard colormaps accepts following basic options (which should be
provided as a named arguments)
- :no_of_colors: Number of colors you want in your
colormap. It usually defines how smaooth your color gradient will be
- :starting_shade: What will be the first shade of your colormap
- :ending_shade: What will be the last shade of your colormap
- :is_qualitative: If True,
:class:`matplotlib.colors.ListedColormap` will be used instead
:class:`matplotlib.colors.LinearSegmentedColormap`. Essentially it
will provide discrete colormap instead linear
- :is_reversed: If True, colormap will be reversed
.. code-block:: python
cm.purples(no_of_colors=8)
cm.greens(starting_shade=30, ending_shade=80)
cm.blues(is_qualitative=True)
cm.reds(ending_shade=50, is_reversed=True, no_of_colors=5)
You can mix-and-match every argument. Essentially there are infinite
possibilities.
If you want even more fine-tune control over your colormap, you can use
your own colormaps by :func:`~SecretColors.cmaps.parent.ColorMapParent
.from_list` method.
.. code-block:: python
cm = ColorMap(matplotlib)
p = Palette()
my_colors = [p.red(shade=30), p.white(), p.blue(shade=60)]
my_cmap = cm.from_list(my_colors)
plt.imshow(data, cmap=my_cmap)
We have some in-build color lists for divergent colormaps. You can use
:func:`~SecretColors.cmaps.parent.ColorMapParent.random_divergent` for
its easy access. Read :class:`~SecretColors.cmaps.parent.ColorMapParent`
documentation for more details on helper functions.
If you like colors from specific :class:`~SecretColors.Palette`, you can
easily switch all colors with single line
.. code-block:: python
cm = ColorMap(matplotlib)
cm.palette = Palette("material") # Material Palette colors will be used.
cm.palette = Palette("brewer") # ColorBrewer colors will be used.
.. tip::
For "brewer" and "tableau", you should prefer using
:class:`~SecretColors.cmaps.BrewerMap` and
:class:`~SecretColors.cmaps.TableauMap` intsead just changing
palette here. As these classes will provide you much more additional
methods which are only available in those classes.
"""
@property
def data(self) -> dict:
return None
def run():
from SecretColors.data.cmaps.brewer import BREWER_DATA
for b in BREWER_DATA:
print(f"* {b}")