2

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?

ewels
  • 463
  • 7
  • 19
  • Check out http://stackoverflow.com/questions/168838/color-scaling-function – Sebastian Nov 04 '15 at 17:35
  • Thanks! No doubt I can write my own code to do interpolation between colours (it would be a bit more complicated than the above, as the brewer scales have up to 12 different colours). I'm just amazed that no-one has beaten me to it with such a package, and don't want to reinvent the wheel if one exists. – ewels Nov 06 '15 at 12:11

1 Answers1

1

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

ewels
  • 463
  • 7
  • 19