"""
This module provides access to color schemes for
- qualitative (or categorical) data: :func:`qualitative_colors`
- sequential data: :func:`sequential_colors`
- diverging data: :func:`diverging_colors`
as explained at https://personal.sron.nl/~pault/
Color schemes are provided through three functions, all accepting the number of different values
as first argument. While py:func`sequential_colors` is limited to at most 9 values and
py:func`diverging_colors` to at most 11, py:func`qualitative_colors` works with any number of
values - but will use different ways to create the scheme depending on the number of values.
"""
import math
import typing
import colorsys
import fractions
import itertools
__all__ = [
'diverging_colors',
'qualitative_colors',
'sequential_colors',
'brightness',
'is_bright',
'rgb_as_hex',
]
def _to_rgb(s: typing.Union[str, list, tuple]) -> tuple:
def f2i(d):
assert 0 <= d <= 1
res = int(math.floor(d * 256))
if res == 256:
res = 255
return res
if isinstance(s, (tuple, list)):
assert len(s) == 3
if isinstance(s[0], (float, fractions.Fraction)):
s = [f2i(d) for d in s]
return s
assert isinstance(s, str)
if s.startswith('#'):
s = s[1:]
if len(s) == 3:
s = ''.join(c + c for c in s)
assert len(s) == 6
return tuple(int(c, 16) for c in [s[i:i + 2] for i in range(0, 6, 2)])
[docs]def rgb_as_hex(s: typing.Union[str, list, tuple]) -> str:
"""
Convert a RGB triple to a `HEX triplet <https://en.wikipedia.org/wiki/Web_colors#Hex_triplet>`_
"""
return '#{0:02X}{1:02X}{2:02X}'.format(*_to_rgb(s))
[docs]def brightness(color: typing.Union[str, list, tuple]) -> float:
"""
Compute the brightness of a color specified as RGB triple (or Hex triplet).
.. seealso:: `<https://www.w3.org/TR/AERT/#color-contrast>`_
"""
R, G, B = _to_rgb(color)
return 0.299 * R + 0.587 * G + 0.114 * B
[docs]def is_bright(color: typing.Union[str, list, tuple]) -> bool:
"""
Compute whether a color is considered bright or not.
.. note::
A brightness value of 125 seems to be a common cut-off above which to regard a color as
"bright".
"""
return brightness(color) > 125
[docs]def qualitative_colors(n: int, set: str = typing.Optional[str]) -> typing.List[str]:
"""
Choses `n` distinct colors suitable for visualizing categorical variables.
.. seealso:: https://en.wikipedia.org/wiki/Categorical_variable
Depending on `n` (and `set`), different algorithms to compute the colors are chosen:
- `n <= 11 and set == 'boynton'`: Colors are chosen according to "Eleven colors that are
almost never confused." by R. M. Boynton, 1989.
- `n <= 12 and set == 'tol'`: Colors are chosen according to
https://personal.sron.nl/~pault/colourschemes.pdf
- `n <= 22`: Kenneth Kelly's "22 colours of maximum contrast" are chosen (as reported by
Paul Green-Armytage in https://eleanormaclure.files.wordpress.com/2011/03/colour-coding.pdf)
- else: Recipe taken from https://stackoverflow.com/a/13781114
:param n: number of distinct colors to return
:param set: name of a particular color set to choose from. "boynton" works for `n<=11` and will\
choose from "Eleven colors that are almost never confused"; "tol" works for `n<=12` and will \
choose from Paul Tol's qualitative color scheme.
:return: list of `n` hex color codes
"""
if n <= 11 and set == 'boynton':
# R. M. Boynton. Eleven colors that are almost never confused.
# In B. E. Rogowitz, editor,
# Proceedings of the SPIE Symposium: Human Vision, Visual Processing, and Digital
# Display, volume 1077, pages 322{332, Bellingham, WA, 1989.
# SPIE Int. Soc. Optical Engineering.
return [
rgb_as_hex(c) for c in
[
(91, 0, 13),
(0, 255, 223),
(23, 169, 255),
(255, 232, 0),
(8, 0, 91),
(255, 208, 198),
(4, 255, 4),
(0, 0, 255),
(0, 79, 0),
(255, 21, 205),
(255, 0, 0),
][:n]]
if n <= 12 and set == 'tol':
# https://personal.sron.nl/~pault/colourschemes.pdf
# as implemented by drmccloy here https://github.com/drammock/colorblind
cols = ['#4477AA', '#332288', '#6699CC', '#88CCEE', '#44AA99', '#117733',
'#999933', '#DDCC77', '#661100', '#CC6677', '#AA4466', '#882255',
'#AA4499']
indices = [[0],
[0, 9],
[0, 7, 9],
[0, 5, 7, 9],
[1, 3, 5, 7, 9],
[1, 3, 5, 7, 9, 12],
[1, 3, 4, 5, 7, 9, 12],
[1, 3, 4, 5, 6, 7, 9, 12],
[1, 3, 4, 5, 6, 7, 9, 11, 12],
[1, 3, 4, 5, 6, 7, 8, 9, 11, 12],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]]
return [cols[ix] for ix in indices[n - 1]]
if n <= 22:
# theory:
# https://eleanormaclure.files.wordpress.com/2011/03/colour-coding.pdf (page 5)
# kelly's colors:
# https://i.kinja-img.com/gawker-media/image/upload/1015680494325093012.JPG
return [
rgb_as_hex(c) for c in
[
'F2F3F4',
'222222',
'F3C300',
'875692',
'F38400',
'A1CAF1',
'BE0032',
'C2B280',
'848482',
'008856',
'E68FAC',
'0067A5',
'F99379',
'604E97',
'F6A600',
'B3446C',
'DCD300',
'882D17',
'8DB600',
'654522',
'E25822',
'2B3D26'][:n]
]
#
# taken from https://stackoverflow.com/a/13781114
#
def zenos_dichotomy():
"""
http://en.wikipedia.org/wiki/1/2_%2B_1/4_%2B_1/8_%2B_1/16_%2B_%C2%B7_%C2%B7_%C2%B7
"""
for k in itertools.count():
yield fractions.Fraction(1, 2**k)
def getfracs():
yield 0
for k in zenos_dichotomy():
i = k.denominator # [1,2,4,8,16,...]
for j in range(1, i, 2):
yield fractions.Fraction(j, i)
def genhsv(h):
for s in [fractions.Fraction(6, 10)]: # optionally use range
for v in [fractions.Fraction(8, 10), fractions.Fraction(5, 10)]: # could use range too
yield (h, s, v) # use bias for v here if you use range
def gethsvs():
return itertools.chain.from_iterable(map(genhsv, getfracs()))
return [
rgb_as_hex(c) for c in
itertools.islice((colorsys.hsv_to_rgb(*x) for x in gethsvs()), n)]
[docs]def sequential_colors(n):
"""
Between 3 and 9 sequential colors.
.. seealso:: `<https://personal.sron.nl/~pault/#sec:sequential>`_
"""
# https://personal.sron.nl/~pault/
# as implemented by drmccloy here https://github.com/drammock/colorblind
assert 3 <= n <= 9
cols = ['#FFFFE5', '#FFFBD5', '#FFF7BC', '#FEE391', '#FED98E', '#FEC44F',
'#FB9A29', '#EC7014', '#D95F0E', '#CC4C02', '#993404', '#8C2D04',
'#662506']
indices = [[2, 5, 8],
[1, 3, 6, 9],
[1, 3, 6, 8, 10],
[1, 3, 5, 6, 8, 10],
[1, 3, 5, 6, 7, 9, 10],
[0, 2, 3, 5, 6, 7, 9, 10],
[0, 2, 3, 5, 6, 7, 9, 10, 12]]
return [cols[ix] for ix in indices[n - 3]]
[docs]def diverging_colors(n):
"""
Between 3 and 11 diverging colors
.. seealso:: `<https://personal.sron.nl/~pault/#sec:diverging>`_
"""
# https://personal.sron.nl/~pault/
# as implemented by drmccloy here https://github.com/drammock/colorblind
assert 3 <= n <= 11
cols = ['#3D52A1', '#3A89C9', '#008BCE', '#77B7E5', '#99C7EC', '#B4DDF7',
'#E6F5FE', '#FFFAD2', '#FFE3AA', '#F9BD7E', '#F5A275', '#ED875E',
'#D03232', '#D24D3E', '#AE1C3E']
indices = [[4, 7, 10],
[2, 5, 9, 12],
[2, 5, 7, 9, 12],
[1, 4, 6, 8, 10, 13],
[1, 4, 6, 7, 8, 10, 13],
[1, 3, 5, 6, 8, 9, 11, 13],
[1, 3, 5, 6, 7, 8, 9, 11, 13],
[0, 1, 3, 5, 6, 8, 9, 11, 13, 14],
[0, 1, 3, 5, 6, 7, 8, 9, 11, 13, 14]]
return [cols[ix] for ix in indices[n - 3]]