Source code for branca.colormap

"""
Colormap
--------

Utility module for dealing with colormaps.

"""

import json
import math
import os

from jinja2 import Template

from branca.element import ENV, Figure, JavascriptLink, MacroElement
from branca.utilities import legend_scaler

rootpath = os.path.abspath(os.path.dirname(__file__))

with open(os.path.join(rootpath, '_cnames.json')) as f:
    _cnames = json.loads(f.read())

with open(os.path.join(rootpath, '_schemes.json')) as f:
    _schemes = json.loads(f.read())


def _is_hex(x):
    return x.startswith('#') and len(x) == 7


def _parse_hex(color_code):
    return (int(color_code[1:3], 16),
            int(color_code[3:5], 16),
            int(color_code[5:7], 16))


def _parse_color(x):
    if isinstance(x, (tuple, list)):
        color_tuple = tuple(x)[:4]
    elif isinstance(x, (str, bytes)) and _is_hex(x):
        color_tuple = _parse_hex(x)
    elif isinstance(x, (str, bytes)):
        cname = _cnames.get(x.lower(), None)
        if cname is None:
            raise ValueError('Unknown color {!r}.'.format(cname))
        color_tuple = _parse_hex(cname)
    else:
        raise ValueError('Unrecognized color code {!r}'.format(x))
    if max(color_tuple) > 1.:
        color_tuple = tuple(u/255. for u in color_tuple)
    return tuple(map(float, (color_tuple+(1.,))[:4]))


def _base(x):
    if x > 0:
        base = pow(10, math.floor(math.log10(x)))
        return round(x/base)*base
    else:
        return 0


[docs]class ColorMap(MacroElement): """A generic class for creating colormaps. Parameters ---------- vmin: float The left bound of the color scale. vmax: float The right bound of the color scale. caption: str A caption to draw with the colormap. max_labels : int, default 10 Maximum number of legend tick labels """ _template = ENV.get_template('color_scale.js') def __init__(self, vmin=0., vmax=1., caption='', max_labels=10): super(ColorMap, self).__init__() self._name = 'ColorMap' self.vmin = vmin self.vmax = vmax self.caption = caption self.index = [vmin, vmax] self.max_labels = max_labels self.tick_labels = None self.width = 450 self.height = 40
[docs] def render(self, **kwargs): """Renders the HTML representation of the element.""" self.color_domain = [self.vmin + (self.vmax-self.vmin) * k/499. for k in range(500)] self.color_range = [self.__call__(x) for x in self.color_domain] if self.tick_labels is None: self.tick_labels = legend_scaler(self.index, self.max_labels) super(ColorMap, self).render(**kwargs) figure = self.get_root() assert isinstance(figure, Figure), ('You cannot render this Element ' 'if it is not in a Figure.') figure.header.add_child(JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), name='d3') # noqa
[docs] def rgba_floats_tuple(self, x): """ This class has to be implemented for each class inheriting from Colormap. This has to be a function of the form float -> (float, float, float, float) describing for each input float x, the output color in RGBA format; Each output value being between 0 and 1. """ raise NotImplementedError
[docs] def rgba_bytes_tuple(self, x): """Provides the color corresponding to value `x` in the form of a tuple (R,G,B,A) with int values between 0 and 255. """ return tuple(int(u*255.9999) for u in self.rgba_floats_tuple(x))
[docs] def rgb_bytes_tuple(self, x): """Provides the color corresponding to value `x` in the form of a tuple (R,G,B) with int values between 0 and 255. """ return self.rgba_bytes_tuple(x)[:3]
[docs] def rgb_hex_str(self, x): """Provides the color corresponding to value `x` in the form of a string of hexadecimal values "#RRGGBB". """ return '#%02x%02x%02x' % self.rgb_bytes_tuple(x)
[docs] def rgba_hex_str(self, x): """Provides the color corresponding to value `x` in the form of a string of hexadecimal values "#RRGGBBAA". """ return '#%02x%02x%02x%02x' % self.rgba_bytes_tuple(x)
def __call__(self, x): """Provides the color corresponding to value `x` in the form of a string of hexadecimal values "#RRGGBBAA". """ return self.rgba_hex_str(x) def _repr_html_(self): """Display the colormap in a Jupyter Notebook. Does not support all the class arguments. """ nb_ticks = 7 delta_x = math.floor(self.width / (nb_ticks - 1)) x_ticks = [(i) * delta_x for i in range(0, nb_ticks)] delta_val = delta_x * (self.vmax - self.vmin) / self.width val_ticks = [round(self.vmin + (i) * delta_val, 1) for i in range(0, nb_ticks)] return ( '<svg height="40" width="{}">'.format(self.width) + "".join( [ ( '<line x1="{i}" y1="15" x2="{i}" ' 'y2="27" style="stroke:{color};stroke-width:2;" />' ).format( i=i * 1, color=self.rgba_hex_str( self.vmin + (self.vmax - self.vmin) * i / (self.width - 1) ), ) for i in range(self.width) ] ) + '<text x="0" y="38" style="text-anchor:start; font-size:11px; font:Arial">{}</text>'.format( self.vmin ) + "".join( [ ( '<text x="{}" y="38"; style="text-anchor:middle; font-size:11px; font:Arial">{}</text>' ).format(x_ticks[i], val_ticks[i]) for i in range(1, nb_ticks - 1) ] ) + '<text x="{}" y="38" style="text-anchor:end; font-size:11px; font:Arial">{}</text>'.format( self.width, self.vmax ) + '<text x="0" y="12" style="font-size:11px; font:Arial">{}</text>'.format( self.caption ) + "</svg>" )
[docs]class LinearColormap(ColorMap): """Creates a ColorMap based on linear interpolation of a set of colors over a given index. Parameters ---------- colors : list-like object with at least two colors. The set of colors to be used for interpolation. Colors can be provided in the form: * tuples of RGBA ints between 0 and 255 (e.g: `(255, 255, 0)` or `(255, 255, 0, 255)`) * tuples of RGBA floats between 0. and 1. (e.g: `(1.,1.,0.)` or `(1., 1., 0., 1.)`) * HTML-like string (e.g: `"#ffff00`) * a color name or shortcut (e.g: `"y"` or `"yellow"`) index : list of floats, default None The values corresponding to each color. It has to be sorted, and have the same length as `colors`. If None, a regular grid between `vmin` and `vmax` is created. vmin : float, default 0. The minimal value for the colormap. Values lower than `vmin` will be bound directly to `colors[0]`. vmax : float, default 1. The maximal value for the colormap. Values higher than `vmax` will be bound directly to `colors[-1]`. max_labels : int, default 10 Maximum number of legend tick labels tick_labels: list of floats, default None If given, used as the positions of ticks.""" def __init__(self, colors, index=None, vmin=0., vmax=1., caption='', max_labels=10, tick_labels=None): super(LinearColormap, self).__init__(vmin=vmin, vmax=vmax, caption=caption, max_labels=max_labels) self.tick_labels = tick_labels n = len(colors) if n < 2: raise ValueError('You must provide at least 2 colors.') if index is None: self.index = [vmin + (vmax-vmin)*i*1./(n-1) for i in range(n)] else: self.index = list(index) self.colors = [_parse_color(x) for x in colors]
[docs] def rgba_floats_tuple(self, x): """Provides the color corresponding to value `x` in the form of a tuple (R,G,B,A) with float values between 0. and 1. """ if x <= self.index[0]: return self.colors[0] if x >= self.index[-1]: return self.colors[-1] i = len([u for u in self.index if u < x]) # 0 < i < n. if self.index[i-1] < self.index[i]: p = (x - self.index[i-1])*1./(self.index[i]-self.index[i-1]) elif self.index[i-1] == self.index[i]: p = 1. else: raise ValueError('Thresholds are not sorted.') return tuple((1.-p) * self.colors[i-1][j] + p*self.colors[i][j] for j in range(4))
[docs] def to_step(self, n=None, index=None, data=None, method=None, quantiles=None, round_method=None, max_labels=10): """Splits the LinearColormap into a StepColormap. Parameters ---------- n : int, default None The number of expected colors in the ouput StepColormap. This will be ignored if `index` is provided. index : list of floats, default None The values corresponding to each color bounds. It has to be sorted. If None, a regular grid between `vmin` and `vmax` is created. data : list of floats, default None A sample of data to adapt the color map to. method : str, default 'linear' The method used to create data-based colormap. It can be 'linear' for linear scale, 'log' for logarithmic, or 'quant' for data's quantile-based scale. quantiles : list of floats, default None Alternatively, you can provide explicitely the quantiles you want to use in the scale. round_method : str, default None The method used to round thresholds. * If 'int', all values will be rounded to the nearest integer. * If 'log10', all values will be rounded to the nearest order-of-magnitude integer. For example, 2100 is rounded to 2000, 2790 to 3000. max_labels : int, default 10 Maximum number of legend tick labels Returns ------- A StepColormap with `n=len(index)-1` colors. Examples: >> lc.to_step(n=12) >> lc.to_step(index=[0, 2, 4, 6, 8, 10]) >> lc.to_step(data=some_list, n=12) >> lc.to_step(data=some_list, n=12, method='linear') >> lc.to_step(data=some_list, n=12, method='log') >> lc.to_step(data=some_list, n=12, method='quantiles') >> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1]) >> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1], ... round_method='log10') """ msg = 'You must specify either `index` or `n`' if index is None: if data is None: if n is None: raise ValueError(msg) else: index = [self.vmin + (self.vmax-self.vmin)*i*1./n for i in range(1+n)] scaled_cm = self else: max_ = max(data) min_ = min(data) scaled_cm = self.scale(vmin=min_, vmax=max_) method = ('quantiles' if quantiles is not None else method if method is not None else 'linear' ) if method.lower().startswith('lin'): if n is None: raise ValueError(msg) index = [min_ + i*(max_-min_)*1./n for i in range(1+n)] elif method.lower().startswith('log'): if n is None: raise ValueError(msg) if min_ <= 0: msg = ('Log-scale works only with strictly ' 'positive values.') raise ValueError(msg) index = [math.exp( math.log(min_) + i * (math.log(max_) - math.log(min_)) * 1./n ) for i in range(1+n)] elif method.lower().startswith('quant'): if quantiles is None: if n is None: msg = ('You must specify either `index`, `n` or' '`quantiles`.') raise ValueError(msg) else: quantiles = [i*1./n for i in range(1+n)] p = len(data)-1 s = sorted(data) index = [s[int(q*p)] * (1.-(q*p) % 1) + s[min(int(q*p) + 1, p)] * ((q*p) % 1) for q in quantiles] else: raise ValueError('Unknown method {}'.format(method)) else: scaled_cm = self.scale(vmin=min(index), vmax=max(index)) n = len(index)-1 if round_method == 'int': index = [round(x) for x in index] if round_method == 'log10': index = [_base(x) for x in index] colors = [scaled_cm.rgba_floats_tuple(index[i] * (1.-i/(n-1.)) + index[i+1] * i/(n-1.)) for i in range(n)] caption = self.caption return StepColormap(colors, index=index, vmin=index[0], vmax=index[-1], caption=caption, max_labels=max_labels, tick_labels=self.tick_labels)
[docs] def scale(self, vmin=0., vmax=1., max_labels=10): """Transforms the colorscale so that the minimal and maximal values fit the given parameters. """ return LinearColormap( self.colors, index=[vmin + (vmax-vmin)*(x-self.vmin)*1./(self.vmax-self.vmin) for x in self.index], # noqa vmin=vmin, vmax=vmax, caption=self.caption, max_labels=max_labels )
[docs]class StepColormap(ColorMap): """Creates a ColorMap based on linear interpolation of a set of colors over a given index. Parameters ---------- colors : list-like object The set of colors to be used for interpolation. Colors can be provided in the form: * tuples of int between 0 and 255 (e.g: `(255,255,0)` or `(255, 255, 0, 255)`) * tuples of floats between 0. and 1. (e.g: `(1.,1.,0.)` or `(1., 1., 0., 1.)`) * HTML-like string (e.g: `"#ffff00`) * a color name or shortcut (e.g: `"y"` or `"yellow"`) index : list of floats, default None The values corresponding to each color. It has to be sorted, and have the same length as `colors`. If None, a regular grid between `vmin` and `vmax` is created. vmin : float, default 0. The minimal value for the colormap. Values lower than `vmin` will be bound directly to `colors[0]`. vmax : float, default 1. The maximal value for the colormap. Values higher than `vmax` will be bound directly to `colors[-1]`. max_labels : int, default 10 Maximum number of legend tick labels tick_labels: list of floats, default None If given, used as the positions of ticks. """ def __init__(self, colors, index=None, vmin=0., vmax=1., caption='', max_labels=10, tick_labels=None): super(StepColormap, self).__init__(vmin=vmin, vmax=vmax, caption=caption, max_labels=max_labels) self.tick_labels = tick_labels n = len(colors) if n < 1: raise ValueError('You must provide at least 1 colors.') if index is None: self.index = [vmin + (vmax-vmin)*i*1./n for i in range(n+1)] else: self.index = list(index) self.colors = [_parse_color(x) for x in colors]
[docs] def rgba_floats_tuple(self, x): """ Provides the color corresponding to value `x` in the form of a tuple (R,G,B,A) with float values between 0. and 1. """ if x <= self.index[0]: return self.colors[0] if x >= self.index[-1]: return self.colors[-1] i = len([u for u in self.index if u < x]) # 0 < i < n. return tuple(self.colors[i-1])
[docs] def to_linear(self, index=None, max_labels=10): """ Transforms the StepColormap into a LinearColormap. Parameters ---------- index : list of floats, default None The values corresponding to each color in the output colormap. It has to be sorted. If None, a regular grid between `vmin` and `vmax` is created. max_labels : int, default 10 Maximum number of legend tick labels """ if index is None: n = len(self.index)-1 index = [self.index[i]*(1.-i/(n-1.))+self.index[i+1]*i/(n-1.) for i in range(n)] colors = [self.rgba_floats_tuple(x) for x in index] return LinearColormap(colors, index=index, vmin=self.vmin, vmax=self.vmax, max_labels=max_labels)
[docs] def scale(self, vmin=0., vmax=1., max_labels=10): """Transforms the colorscale so that the minimal and maximal values fit the given parameters. """ return StepColormap( self.colors, index=[vmin + (vmax-vmin)*(x-self.vmin)*1./(self.vmax-self.vmin) for x in self.index], # noqa vmin=vmin, vmax=vmax, caption=self.caption, max_labels=max_labels )
class _LinearColormaps(object): """A class for hosting the list of built-in linear colormaps.""" def __init__(self): self._schemes = _schemes.copy() self._colormaps = {key: LinearColormap(val) for key, val in _schemes.items()} for key, val in _schemes.items(): setattr(self, key, LinearColormap(val)) def _repr_html_(self): return Template(""" <table> {% for key,val in this._colormaps.items() %} <tr><td>{{key}}</td><td>{{val._repr_html_()}}</td></tr> {% endfor %}</table> """).render(this=self) linear = _LinearColormaps() class _StepColormaps(object): """A class for hosting the list of built-in step colormaps.""" def __init__(self): self._schemes = _schemes.copy() self._colormaps = {key: StepColormap(val) for key, val in _schemes.items()} for key, val in _schemes.items(): setattr(self, key, StepColormap(val)) def _repr_html_(self): return Template(""" <table> {% for key,val in this._colormaps.items() %} <tr><td>{{key}}</td><td>{{val._repr_html_()}}</td></tr> {% endfor %}</table> """).render(this=self) step = _StepColormaps()