Source code for pytimings.plotting.colors

#!/usr/bin/env python3
from __future__ import annotations

import colorsys

__all__ = [
    "contrast_ratio",
    "discrete_cmap",
    "get_colour_palette",
    "get_colour_palette_cheat",
    "get_hue_vector",
    "get_hue_vector_rec",
]

_SWITCH_COLOR_VALUE = 2
_WCAG_LINEARIZE_THRESHOLD = 0.03928
_MAX_CONTRAST_RATIO = 0.6


[docs] def get_hue_vector(amount): level = 0 while (1 << level) < amount: level += 1 out = [] return get_hue_vector_rec(out, amount, level)
[docs] def get_hue_vector_rec(out, amount, level): if level <= 1: if len(out) < amount: out.append(0.0) if len(out) < amount: out.append(0.5) return out else: out = get_hue_vector_rec(out, amount, level - 1) lower = len(out) out = get_hue_vector_rec(out, amount, level - 1) upper = len(out) for i in range(lower, upper): out[i] += 1.0 / (1 << level) return out
[docs] def get_colour_palette(size): result = [] # colors satvalbifurcatepos = 0 satvalsplittings = [] # doubles if len(satvalsplittings) == 0: # // insert ranges to bifurcate satvalsplittings.append(1) satvalsplittings.append(0) satvalbifurcatepos = 0 huevector = get_hue_vector(size) bisectionlimit = 20 for i in range(len(result), size): hue = huevector[i] saturation = 1 value = 1 switccolors = i % 3 # ; // why only 3 and not all combinations? because it's easy, plus the bisection limit # cannot be divided integer by it if i % bisectionlimit == 0: satvalbifurcatepos = satvalbifurcatepos % (len(satvalsplittings) - 1) toinsert = satvalbifurcatepos + 1 satvalsplittings.insert( toinsert, (satvalsplittings[satvalbifurcatepos] - satvalsplittings[satvalbifurcatepos + 1]) / 2 + satvalsplittings[satvalbifurcatepos + 1], ) satvalbifurcatepos += 2 if switccolors == 1: saturation = satvalsplittings[satvalbifurcatepos - 1] elif switccolors == _SWITCH_COLOR_VALUE: value = satvalsplittings[satvalbifurcatepos - 1] hue += 0.17 # ; // use as starting point a zone where color band is narrow so that small variations means # high change in visual effect if hue > 1: hue -= 1 col = colorsys.hsv_to_rgb(hue, saturation, value) result.append(col) return result
[docs] def contrast_ratio(color_a, color_b): """Directional WCAG-style contrast ratio of two RGB colors with channels in [0, 1].""" def _lum(c): c = float(c) if c <= _WCAG_LINEARIZE_THRESHOLD: return c / 12.92 return ((c + 0.055) / 1.055) ** 2.4 def _rel(rgb): return 0.2126 * _lum(rgb[0]) + 0.7152 * _lum(rgb[1]) + 0.0722 * _lum(rgb[2]) return (_rel(color_a) + 0.05) / (_rel(color_b) + 0.05)
[docs] def get_colour_palette_cheat(size, filter_colors=None, bg_color=(1, 1, 1)): filter_colors = filter_colors or [] target_len = size candidate_size = size max_size = size + 1000 # guard against impossible filters (e.g. a dark bg_color) k = [] while len(k) < target_len: k = [ p for p in set(get_colour_palette(candidate_size)) if p not in filter_colors and contrast_ratio(p, bg_color) < _MAX_CONTRAST_RATIO ] candidate_size += 1 if candidate_size > max_size: raise RuntimeError( f"could not find {target_len} colors with sufficient contrast against bg_color={bg_color}" ) return k[:target_len]
[docs] def discrete_cmap(count, bg_color=(255, 255, 255), name="indexed"): import matplotlib as mpl cmap = mpl.colors.ListedColormap(get_colour_palette_cheat(count, bg_color=bg_color), name) mpl.colormaps.register(cmap=cmap) return cmap
if __name__ == "__main__": print(get_colour_palette(4)) print(get_colour_palette_cheat(4)) print(get_colour_palette_cheat(4, [(0.0, 0, 0.0)]))