I'm currently using the wonderful chroma.js JavaScript library to create colour values from Color Brewer palettes. However, I'd like to move this code into Python instead.
I'm struggling to find any Python libraries to do what I want. As an example, here's my current chroma.js code:
var scale = chroma.scale('GnBu').domain([minval, maxval]);
var col = scale(val).hex();
This creates a colour scale using the Green-Blue color brewer palette between my minimum and maximum values. Then, the colour corresponding to val
is picked ready for use. Pretty simple!
Does anyone know of a way to do this in Python?
For anyone finding this on google - I ended up using the excellent spectra Python library to handle interpolation. I used this to create a helper function that is now part of my tool MultiQC. The code is on GitHub but I've pasted it below in case the link changes in the future:
#!/usr/bin/env python
"""
Helper functions to manipulate colours and colour scales
"""
from __future__ import print_function
import spectra
import numpy as np
import re
# Default logger will be replaced by caller
import logging
logger = logging.getLogger(__name__)
class mqc_colour_scale(object):
""" Class to hold a colour scheme. """
def __init__(self, name='GnBu', minval=0, maxval=100):
""" Initialise class with a colour scale """
self.colours = self.get_colours(name)
# Sanity checks
minval = re.sub("[^0-9\.]", "", str(minval))
maxval = re.sub("[^0-9\.]", "", str(maxval))
if minval == '':
minval = 0
if maxval == '':
maxval = 100
if float(minval) == float(maxval):
self.minval = float(minval)
self.maxval = float(minval) + 1.0
elif minval > maxval:
self.minval = float(maxval)
self.maxval = float(minval)
else:
self.minval = float(minval)
self.maxval = float(maxval)
def get_colour(self, val, colformat='hex'):
""" Given a value, return a colour within the colour scale """
try:
# Sanity checks
val = re.sub("[^0-9\.]", "", str(val))
if val == '':
val = self.minval
val = float(val)
val = max(val, self.minval)
val = min(val, self.maxval)
domain_nums = list( np.linspace(self.minval, self.maxval, len(self.colours)) )
my_scale = spectra.scale(self.colours).domain(domain_nums)
# Weird, I know. I ported this from the original JavaScript for continuity
# Seems to work better than adjusting brightness / saturation / luminosity
rgb_converter = lambda x: max(0, min(1, 1+((x-1)*0.3)))
thecolour = spectra.rgb( *[rgb_converter(v) for v in my_scale(val).rgb] )
return thecolour.hexcode
except:
# Shouldn't crash all of MultiQC just for colours
return ''
def get_colours(self, name='GnBu'):
""" Function to get a colour scale by name
Input: Name of colour scale (suffix with -rev for reversed)
Defaults to 'GnBu' if scale not found.
Returns: List of hex colours
"""
# ColorBrewer colours, taken from Chroma.js source code
# https://github.com/gka/chroma.js
### Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The
### Pennsylvania State University.
###
### Licensed under the Apache License, Version 2.0 (the "License");
### you may not use this file except in compliance with the License.
### You may obtain a copy of the License at
### http://www.apache.org/licenses/LICENSE-2.0
###
### Unless required by applicable law or agreed to in writing, software distributed
### under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
### CONDITIONS OF ANY KIND, either express or implied. See the License for the
### specific language governing permissions and limitations under the License.
colorbrewer_scales = {
# sequential
'OrRd': ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000'],
'PuBu': ['#fff7fb', '#ece7f2', '#d0d1e6', '#a6bddb', '#74a9cf', '#3690c0', '#0570b0', '#045a8d', '#023858'],
'BuPu': ['#f7fcfd', '#e0ecf4', '#bfd3e6', '#9ebcda', '#8c96c6', '#8c6bb1', '#88419d', '#810f7c', '#4d004b'],
'Oranges': ['#fff5eb', '#fee6ce', '#fdd0a2', '#fdae6b', '#fd8d3c', '#f16913', '#d94801', '#a63603', '#7f2704'],
'BuGn': ['#f7fcfd', '#e5f5f9', '#ccece6', '#99d8c9', '#66c2a4', '#41ae76', '#238b45', '#006d2c', '#00441b'],
'YlOrBr': ['#ffffe5', '#fff7bc', '#fee391', '#fec44f', '#fe9929', '#ec7014', '#cc4c02', '#993404', '#662506'],
'YlGn': ['#ffffe5', '#f7fcb9', '#d9f0a3', '#addd8e', '#78c679', '#41ab5d', '#238443', '#006837', '#004529'],
'Reds': ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15', '#67000d'],
'RdPu': ['#fff7f3', '#fde0dd', '#fcc5c0', '#fa9fb5', '#f768a1', '#dd3497', '#ae017e', '#7a0177', '#49006a'],
'Greens': ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#006d2c', '#00441b'],
'YlGnBu': ['#ffffd9', '#edf8b1', '#c7e9b4', '#7fcdbb', '#41b6c4', '#1d91c0', '#225ea8', '#253494', '#081d58'],
'Purples': ['#fcfbfd', '#efedf5', '#dadaeb', '#bcbddc', '#9e9ac8', '#807dba', '#6a51a3', '#54278f', '#3f007d'],
'GnBu': ['#f7fcf0', '#e0f3db', '#ccebc5', '#a8ddb5', '#7bccc4', '#4eb3d3', '#2b8cbe', '#0868ac', '#084081'],
'Greys': ['#ffffff', '#f0f0f0', '#d9d9d9', '#bdbdbd', '#969696', '#737373', '#525252', '#252525', '#000000'],
'YlOrRd': ['#ffffcc', '#ffeda0', '#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c', '#bd0026', '#800026'],
'PuRd': ['#f7f4f9', '#e7e1ef', '#d4b9da', '#c994c7', '#df65b0', '#e7298a', '#ce1256', '#980043', '#67001f'],
'Blues': ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#08519c', '#08306b'],
'PuBuGn': ['#fff7fb', '#ece2f0', '#d0d1e6', '#a6bddb', '#67a9cf', '#3690c0', '#02818a', '#016c59', '#014636'],
# diverging
'Spectral': ['#9e0142', '#d53e4f', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#e6f598', '#abdda4', '#66c2a5', '#3288bd', '#5e4fa2'],
'RdYlGn': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850', '#006837'],
'RdBu': ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#fddbc7', '#f7f7f7', '#d1e5f0', '#92c5de', '#4393c3', '#2166ac', '#053061'],
'PiYG': ['#8e0152', '#c51b7d', '#de77ae', '#f1b6da', '#fde0ef', '#f7f7f7', '#e6f5d0', '#b8e186', '#7fbc41', '#4d9221', '#276419'],
'PRGn': ['#40004b', '#762a83', '#9970ab', '#c2a5cf', '#e7d4e8', '#f7f7f7', '#d9f0d3', '#a6dba0', '#5aae61', '#1b7837', '#00441b'],
'RdYlBu': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee090', '#ffffbf', '#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'],
'BrBG': ['#543005', '#8c510a', '#bf812d', '#dfc27d', '#f6e8c3', '#f5f5f5', '#c7eae5', '#80cdc1', '#35978f', '#01665e', '#003c30'],
'RdGy': ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#fddbc7', '#ffffff', '#e0e0e0', '#bababa', '#878787', '#4d4d4d', '#1a1a1a'],
'PuOr': ['#7f3b08', '#b35806', '#e08214', '#fdb863', '#fee0b6', '#f7f7f7', '#d8daeb', '#b2abd2', '#8073ac', '#542788', '#2d004b'],
# qualitative
'Set2': ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494', '#b3b3b3'],
'Accent': ['#7fc97f', '#beaed4', '#fdc086', '#ffff99', '#386cb0', '#f0027f', '#bf5b17', '#666666'],
'Set1': ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf', '#999999'],
'Set3': ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5', '#ffed6f'],
'Dark2': ['#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d', '#666666'],
'Paired': ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928'],
'Pastel2': ['#b3e2cd', '#fdcdac', '#cbd5e8', '#f4cae4', '#e6f5c9', '#fff2ae', '#f1e2cc', '#cccccc'],
'Pastel1': ['#fbb4ae', '#b3cde3', '#ccebc5', '#decbe4', '#fed9a6', '#ffffcc', '#e5d8bd', '#fddaec', '#f2f2f2'],
}
# Detect reverse colour scales
reverse = False
if str(name).endswith('-rev'):
reverse = True
name = name[:-4]
# Default colour scale
if name not in colorbrewer_scales:
name = 'GnBu'
# Return colours
if reverse:
return list(reversed(colorbrewer_scales[name]))
else:
return colorbrewer_scales[name]
This function can then be used as follows:
c_scale = mqc_colour.mqc_colour_scale(scale_name, min_val, max_val)
hex_code = c_scale.get_colour(val)
Note that the code in its current state returns a very washed out version of the colour, for use as a background colour behind black text. You should be able to easily remove this from the get_colour
function if desired though.
I hope this helps someone!
Phil