Source code for plot_utils.colors_and_lines

# -*- coding: utf-8 -*-

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

from . import helper as hlp

#%%============================================================================
[docs]class Color(): ''' A class that defines a color. Parameters ---------- color : str or <tuple> or <list> The color information to initialize the Color object. Can be a list or tuple of 3 elements (i.e., the RGB information), or a HEX string such as "#00FF00", or XKCD color names (https://xkcd.com/color/rgb/) or X11 color names (http://cng.seas.rochester.edu/CNG/docs/x11color.html). is_rgb_normalized : bool Whether or not the input information (if RGB) contains the normalized values (such as [0, 0.5, 0.5]). This parameter has no effect if the input is not RGB. ''' def __init__(self, color, is_rgb_normalized=True): import matplotlib._color_data as mcd if not isinstance(color, (list, tuple, str)): raise TypeError('`color` must be a list/tuple of length 3 or a str.') if isinstance(color, (list, tuple)): if len(color) != 3: raise TypeError('If `color` is a list/tuple, its length must be 3.') else: self.__color = self.__rgb_to_hex(color, is_rgb_normalized) if isinstance(color, str): color = color.lower() # convert all to lower case if len(color) == 1: # base color specification, such as 'w' or 'b' _rgb_color = mcd.BASE_COLORS[color] self.__color = self.__rgb_to_hex(_rgb_color, is_normalized=True) elif color[0] == '#': # HEX color specification, such as '#00FFFF' self.__color = color elif color.startswith('xkcd:'): self.__color = mcd.XKCD_COLORS[color] elif color.startswith('tab:'): self.__color = mcd.TABLEAU_COLORS[color] else: try: self.__color = mcd.CSS4_COLORS[color] except KeyError: raise ValueError("Unrecognized color: '%s'" % color) def __repr__(self): return 'RGB color: %s' % str(self.as_rgb()); def __rgb_to_hex(self, rgb, is_normalized=True): ''' Private method. Converts RGB values into HEX. Parameters ---------- rgb : list<float> or tuple<float> RGB values is_normalized : bool Whether the RGB values are normalized (i.e., from 0 to 1) Returns ------- hex_val : str HEX value ''' if np.any(np.array(rgb) > 255): raise ValueError('`rgb` values should not exceed 255.') if np.any(np.array(rgb) < 0): raise ValueError('`rgb` values should not be negative.') if max(rgb) > 1.0 and is_normalized == True: rgb = [_ / 255.0 for _ in rgb] if is_normalized: rgb_255 = [int(_ * 255) for _ in rgb] else: rgb_255 = [int(_) for _ in rgb] return u'#{:02x}{:02x}{:02x}'.format(*rgb_255) def __hex_to_rgb(self, hex_, normalize=True): ''' Private method. Converts HEX values into RGB. Reference: https://stackoverflow.com/a/29643643/8892243 Parameters ---------- hex_ : str HEX representation of a color normalize : bool Whether or not to return the normalized (between 0 and 1) RGB Returns ------- rgb : tuple<float> RGB values in three numbers. ''' h = hex_[1:] # strip the '#' in the front if normalize: rgb = tuple(int(h[i:i+2], 16) / 255.0 for i in (0, 2 ,4)) else: rgb = tuple(int(h[i:i+2], 16) for i in (0, 2 ,4)) return rgb
[docs] def as_rgb(self, normalize=True): ''' Export thes color as RGB values. Parameter --------- normalize : bool Whether or not to return the normalized (between 0 and 1) RGB. Returns ------- rgb_val : tuple<float> RGB values in three numbers. ''' return self.__hex_to_rgb(self.__color, normalize=normalize)
[docs] def as_rgba(self, alpha=1.0): ''' Exports the color object as RGBA values. The R, G, and B values are always normalized (between 0 and 1). Parameter --------- alpha : float The transparency (0 being completely transparent and 1 opaque). Returns ------- rgba_val : tuple<float> RGBA values in four numbers. ''' if alpha < 0 or alpha > 1: raise ValueError('`alpha` must be between 0 and 1 (inclusive).') rgb = self.__hex_to_rgb(self.__color, normalize=True) rgba = (rgb[0], rgb[1], rgb[2], alpha) return rgba
[docs] def as_hex(self): ''' Exports the color object as HEX values. Returns ------- hex_val : str HEX value. ''' return self.__color
[docs] def show(self): ''' Shows color as a square patch. ''' import matplotlib.patches as mpatch fig = plt.figure(figsize=(0.5, 0.5)) ax = fig.add_axes([0, 0, 1, 1]) p = mpatch.Rectangle((0, 0), 1, 1, color=self.__color) ax.add_patch(p) ax.axis('off')
#%%============================================================================
[docs]class Multiple_Colors(): ''' A class that defines multiple colors. Parameters ---------- colors : list A list of color information to initialize the Multiple_Colors object. The list elements can be: - a list or tuple of 3 elements (i.e., the RGB information) - a HEX string such as "#00FF00" - an XKCD color name (https://xkcd.com/color/rgb/) - an X11 color name (http://cng.seas.rochester.edu/CNG/docs/x11color.html) Different elements of colors do not need to be of the same type. is_rgb_normalized : bool Whether or not the input information (if RGB) contains the normalized values (such as [0, 0.5, 0.5]). This parameter has no effect if the input is not RGB. ''' def __init__(self, colors, is_rgb_normalized=True): if not isinstance(colors, list): raise TypeError('`colors` must be a list.') if len(colors) == 0: raise hlp.LengthError('Length of `colors` must non-zero.') self.__length = len(colors) self.__Colors = [None] * self.__length for j, color in enumerate(colors): self.__Colors[j] = Color(color, is_rgb_normalized) def __repr__(self): return self.as_rgb()
[docs] def as_rgb(self, normalize=True): ''' Exports the colors as a list of RGB values Parameter --------- normalize : bool Whether or not to return the normalized (between 0 and 1) RGB. Returns ------- rgb_list : list<list<float>> A list of list: each sub-list represents a RGB color in three numbers. ''' result = [None] * self.__length for j in range(self.__length): result[j] = self.__Colors[j].as_rgb(normalize=normalize) return result
[docs] def as_rgba(self, alpha=1.0): ''' Exports the colors as a list of RGBA values Parameter --------- alpha : float The transparency (0 being completely transparent and 1 opaque). Returns ------- rgba_list : list<list<float>> A list of list: each sub-list represents a RGBA color in four numbers. ''' result = [None] * self.__length for j in range(self.__length): result[j] = self.__Colors[j].as_rgba(alpha=alpha) return result
[docs] def as_hex(self): ''' Exports the colors as a list of HEX values Returns ------- hex_list : list<str> A list of HEX colors ''' result = [None] * self.__length for j in range(self.__length): result[j] = self.__Colors[j].as_hex() return result
[docs] def show(self, vertical=False, text=None): ''' Shows the colors as square patches Parameters ---------- vertical : bool Whether or not to show the patches vertically text : str The text to show next to the colors ''' import matplotlib.patches as mpatch figsize = (.5, self.__length/2) if vertical else (self.__length/2, .5) fig = plt.figure(figsize=figsize) ax = fig.add_axes([0, 0, 1, 1]) for j in range(self.__length): loc = (j, 0) if not vertical else (0, self.__length - j - 1) p = mpatch.Rectangle(loc, 1, 1, color=self.__Colors[j].as_hex()) ax.add_patch(p) ax.axis('off') if not vertical: ax.set_xlim(0, self.__length) ax.set_ylim(0, 1) else: ax.set_ylim(0, self.__length) ax.set_xlim(0, 1) if text: if not vertical: ax.text(j + 1.5, 0.5, text, va='center') else: ax.text(0.5, j + 1.5, text, ha='center')
#%%============================================================================ def _check_color_types(color, n=None): ''' Helper function that checks whether a Python object ``color`` is indeed a valid list (or tuple) of length n that defines ``n`` colors. Returns ``True`` (valid) or ``False`` (otherwise), and an error message (empty message if ``True``). ''' if not isinstance(color,(list,tuple)): is_valid = False err_msg = '"color" must be a list of lists (or tuple of tuples).' elif not all([isinstance(c_, (list, tuple)) for c_ in color]) and \ not all([isinstance(c_, str) for c_ in color]): is_valid = False err_msg = '"color" must be a list of lists (or tuple of tuples).' else: if n and len(color) < n: is_valid = False err_msg = 'Length of "color" must be at least the same length as "n".' else: is_valid = True err_msg = '' return is_valid, err_msg #%%============================================================================
[docs]def get_colors(N=None, color_scheme='tab10'): ''' Return a list of N distinguisable colors. When N is larger than the color scheme capacity, the color cycle is wrapped around. What does each color_scheme look like? https://matplotlib.org/mpl_examples/color/colormaps_reference_04.png https://matplotlib.org/users/dflt_style_changes.html#colors-color-cycles-and-color-maps https://github.com/vega/vega/wiki/Scales#scale-range-literals https://www.mathworks.com/help/matlab/graphics_transition/why-are-plot-lines-different-colors.html Parameters ---------- N : int or ``None`` Number of qualitative colors desired. If None, returns all the colors in the specified color scheme. color_scheme : str or {8.3, 8.4} Color scheme specifier. Valid specifiers are: (1) Matplotlib qualitative color map names: 'Pastel1' 'Pastel2' 'Paired' 'Accent' 'Dark2' 'Set1' 'Set2' 'Set3' 'tab10' 'tab20' 'tab20b' 'tab20c' (https://matplotlib.org/mpl_examples/color/colormaps_reference_04.png) (2) 'tab10_muted': A set of 10 colors that are the muted version of "tab10" (3) '8.3' and '8.4': old and new MATLAB color scheme Old: https://www.mathworks.com/help/matlab/graphics_transition/transition_colororder_old.png New: https://www.mathworks.com/help/matlab/graphics_transition/transition_colororder.png (4) 'rgbcmyk': old default Matplotlib color palette (v1.5 and earlier) (5) 'bw' (or 'bw3'), 'bw4', and 'bw5' Black-and-white (grayscale colors in 3, 4, and 5 levels) Returns ------- colors : list<list<float>>, list<str> A list of colors (as RGB, or color name, or hex) ''' nr_c = { 'Pastel1': 9, # number of qualitative colors in each color map 'Pastel2': 8, 'Paired': 12, 'Accent': 8, 'Dark2': 8, 'Set1': 9, 'Set2': 8, 'Set3': 12, 'tab10': 10, 'tab20': 20, 'tab20b': 20, 'tab20c': 20, } qcm_names = list(nr_c.keys()) # valid names of qualititative color maps qcm_names_lower = [ 'pastel1','pastel2','paired','accent','dark2','set1', 'set2','set3', # lower case version (without 'tab' ones) ] if not isinstance(color_scheme,(str, int, float, np.number)): raise TypeError('`color_scheme` must be str, int, or float.') d = { 'rgbcmyk': ['b','g','r','c','m','y','k'], # matplotlib v1.5 palette 'bw': [[0]*3,[0.4]*3,[0.75]*3], # black and white: 3 levels 'bw3': [[0]*3,[0.4]*3,[0.75]*3], # black and white: 3 levels 'bw4': [[0]*3,[0.25]*3,[0.5]*3,[0.75]*3], # b and w, 4 levels 'bw5': [[0]*3,[0.15]*3,[0.3]*3,[0.5]*3,[0.7]*3], # b and w, 5 levels 'tab10': [ '#1f77b4','#ff7f0e','#2ca02c','#d62728', # old Tableau palette '#9467bd', '#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf', ], '8.3': [ [0, 0, 1.0000], # blue (MATLAB ver 8.3 (R2014a) or earlier) [0, 0.5000, 0], # green [1.0000, 0, 0], # red [0, 0.7500, 0.7500], # cyan [0.7500, 0, 0.7500], # magenta [0.7500, 0.7500, 0], # dark yellow [0.2500, 0.2500, 0.2500], # dark gray ], '8.4': [ [0.0000, 0.4470, 0.7410], # MATLAB ver 8.4 (R2014b) or later [0.8500, 0.3250, 0.0980], [0.9290, 0.6940, 0.1250], [0.4940, 0.1840, 0.5560], [0.4660, 0.6740, 0.1880], [0.3010, 0.7450, 0.9330], [0.6350, 0.0780, 0.1840], ], } if color_scheme in d: palette = d[color_scheme] else: if color_scheme in qcm_names: c_s = color_scheme # short hand [Note: no wrap-around behavior in mpl.cm functions] rgba = eval('mpl.cm.%s(range(%d))' % (c_s, nr_c[c_s])) # e.g., mpl.cm.Set1(range(10)) palette = [list(_)[:3] for _ in rgba] # remove alpha value from each sub-list elif color_scheme in qcm_names_lower: c_s = color_scheme.title() # first letter upper case rgba = eval('mpl.cm.%s(range(%d))' % (c_s, nr_c[c_s])) palette = [list(_)[:3] for _ in rgba] elif color_scheme == 'tab10_muted': rgba_tmp = mpl.cm.tab20(range(nr_c['tab20'])) palette_tmp = [list(_)[:3] for _ in rgba_tmp] palette = palette_tmp[1::2] else: raise ValueError( f"You provided an invalid `color_scheme` ('{color_scheme}'). " "A valid `color_scheme` must be one of " "{'pastel1', 'pastel2', 'paired', 'accent', " "'dark2', 'set1', 'set2', 'set3', 'tab10', " "'tab10_muted', 'tab20', 'tab20b', 'tab20c', " "'rgbcmyk', 'bw', 'bw3', 'bw4', 'bw5', " "'8.3', '8.4'}." ) L = len(palette) if N is None: N = L elif not isinstance(N, (int, np.integer)): raise TypeError('`N` should be either None or integers.') return [palette[i % L] for i in range(N)] # wrap around 'palette' if N > L
#%%============================================================================
[docs]def get_linespecs( color_scheme='tab10', n_linestyle=4, range_linewidth=[1,2,3], priority='color', ): ''' Return a list of distinguishable line specifications (color, line style, and line width combinations). Parameters ---------- color_scheme : str or {8.3, 8.4} Color scheme specifier. See documentation of ``get_colors()`` for valid specifiers. n_linestyle : {1, 2, 3, 4} Number of different line styles to use. There are only four available line stylies in Matplotlib: (1) - (2) -- (3) -. and (4) .. For example, if you use 2, you choose only - and -- range_linewidth : list, numpy.ndarray, or pandas.Series The range of different line width values to use. priority : {'color', 'linestyle', 'linewidth'} Which one of the three line specification aspects (i.e., color, line style, or line width) should change first in the resulting list of line specifications. Returns ------- style_cycle_list : list<dict> A list whose every element is a dictionary that looks like this: {'color': '#1f77b4', 'ls': '-', 'lw': 1}. Each element can then be passed as keyword arguments to ``matplotlib.pyplot.plot()`` or other similar functions. Example ------- >>> import plot_utils as pu >>> import matplotlib.pyplot as plt >>> plt.plot([0,1], [0,1], **pu.get_linespecs()[53]) ''' import cycler colors = get_colors(N=None, color_scheme=color_scheme) if n_linestyle in [1,2,3,4]: linestyles = ['-', '--', '-.', ':'][:n_linestyle] else: raise ValueError('`n_linestyle` should be 1, 2, 3, or 4.') color_cycle = cycler.cycler(color=colors) ls_cycle = cycler.cycler(ls=linestyles) lw_cycle = cycler.cycler(lw=range_linewidth) if priority == 'color': style_cycle = lw_cycle * ls_cycle * color_cycle elif priority == 'linestyle': style_cycle = lw_cycle * color_cycle * ls_cycle elif priority == 'linewidth': style_cycle = color_cycle * lw_cycle * ls_cycle return list(style_cycle)
#%%============================================================================
[docs]def linespecs_demo(line_specs, horizontal_plot=False): ''' Demonstrate line specifications generated by :func:~`get_linespecs()`. Parameter --------- line_spec : list<dict> A list of line specifications. It can be the returned value of :func:`~get_linespecs()`. horizontal_plot : bool Whether or not to demonstrate the line specifications in a horizontal plot. Returns ------- fig : matplotlib.figure.Figure The figure object being created or being passed into this function. ax : matplotlib.axes._subplots.AxesSubplot The axes object being created or being passed into this function. ''' x = np.arange(0,10,0.05) # define x and y points to plot y = np.sin(x) fig_width = 8 fig_height = len(line_specs) * 0.2 if horizontal_plot: x, y = y, x fig_width, fig_height = fig_height, fig_width figsize = (fig_width, fig_height) fig = plt.figure(figsize=figsize) ax = plt.axes() for j, linespec in enumerate(line_specs): if horizontal_plot: plt.plot(x+j, y, **linespec) else: plt.plot(x, y-j, **linespec) ax.axis('off') # no coordinate axes box return fig, ax