1# Copyright(c) 2007-2019 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> 2# 2009 by Yaco S.L. <lgs@yaco.es> 3# 4# This file is part of PyCha. 5# 6# PyCha is free software: you can redistribute it and/or modify 7# it under the terms of the GNU Lesser General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# PyCha is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public License 17# along with PyCha. If not, see <http://www.gnu.org/licenses/>. 18 19import math 20 21import six 22 23from pycha.utils import clamp 24 25 26DEFAULT_COLOR = '#3c581a' 27 28 29def hex2rgb(hexstring, digits=2): 30 """Converts a hexstring color to a rgb tuple. 31 32 Example: #ff0000 -> (1.0, 0.0, 0.0) 33 34 digits is an integer number telling how many characters should be 35 interpreted for each component in the hexstring. 36 """ 37 if isinstance(hexstring, (tuple, list)): 38 return hexstring 39 40 top = float(int(digits * 'f', 16)) 41 r = int(hexstring[1:digits + 1], 16) 42 g = int(hexstring[digits + 1:digits * 2 + 1], 16) 43 b = int(hexstring[digits * 2 + 1:digits * 3 + 1], 16) 44 return r / top, g / top, b / top 45 46 47def rgb2hsv(r, g, b): 48 """Converts a RGB color into a HSV one 49 50 See http://en.wikipedia.org/wiki/HSV_color_space 51 """ 52 maximum = max(r, g, b) 53 minimum = min(r, g, b) 54 if maximum == minimum: 55 h = 0.0 56 elif maximum == r: 57 h = 60.0 * ((g - b) / (maximum - minimum)) + 360.0 58 if h >= 360.0: 59 h -= 360.0 60 elif maximum == g: 61 h = 60.0 * ((b - r) / (maximum - minimum)) + 120.0 62 elif maximum == b: 63 h = 60.0 * ((r - g) / (maximum - minimum)) + 240.0 64 65 if maximum == 0.0: 66 s = 0.0 67 else: 68 s = 1.0 - (minimum / maximum) 69 70 v = maximum 71 72 return h, s, v 73 74 75def hsv2rgb(h, s, v): 76 """Converts a HSV color into a RGB one 77 78 See http://en.wikipedia.org/wiki/HSV_color_space 79 """ 80 hi = int(math.floor(h / 60.0)) % 6 81 f = (h / 60.0) - hi 82 p = v * (1 - s) 83 q = v * (1 - f * s) 84 t = v * (1 - (1 - f) * s) 85 86 if hi == 0: 87 r, g, b = v, t, p 88 elif hi == 1: 89 r, g, b = q, v, p 90 elif hi == 2: 91 r, g, b = p, v, t 92 elif hi == 3: 93 r, g, b = p, q, v 94 elif hi == 4: 95 r, g, b = t, p, v 96 elif hi == 5: 97 r, g, b = v, p, q 98 99 return r, g, b 100 101 102def lighten(r, g, b, amount): 103 """Return a lighter version of the color (r, g, b)""" 104 return (clamp(0.0, 1.0, r + amount), 105 clamp(0.0, 1.0, g + amount), 106 clamp(0.0, 1.0, b + amount)) 107 108 109basicColors = dict( 110 red='#6d1d1d', 111 green=DEFAULT_COLOR, 112 blue='#224565', 113 grey='#444444', 114 black='#000000', 115 darkcyan='#305755', 116) 117 118 119class ColorSchemeMetaclass(type): 120 """This metaclass is used to autoregister all ColorScheme classes""" 121 122 def __new__(mcs, name, bases, dict): 123 klass = type.__new__(mcs, name, bases, dict) 124 klass.registerColorScheme() 125 return klass 126 127 128class ColorScheme(six.with_metaclass(ColorSchemeMetaclass, dict)): 129 """A color scheme is a dictionary where the keys match the keys 130 constructor argument and the values are colors""" 131 132 __registry__ = {} 133 134 def __init__(self, keys): 135 super(ColorScheme, self).__init__() 136 137 @classmethod 138 def registerColorScheme(cls): 139 key = cls.__name__.replace('ColorScheme', '').lower() 140 if key: 141 cls.__registry__[key] = cls 142 143 @classmethod 144 def getColorScheme(cls, name, default=None): 145 return cls.__registry__.get(name, default) 146 147 148class GradientColorScheme(ColorScheme): 149 """In this color scheme each color is a lighter version of initialColor. 150 151 This difference is computed based on the number of keys. 152 153 The initialColor is given in a hex string format. 154 """ 155 156 def __init__(self, keys, initialColor=DEFAULT_COLOR): 157 super(GradientColorScheme, self).__init__(keys) 158 if initialColor in basicColors: 159 initialColor = basicColors[initialColor] 160 161 r, g, b = hex2rgb(initialColor) 162 light = 1.0 / (len(keys) * 2) 163 164 for i, key in enumerate(keys): 165 self[key] = lighten(r, g, b, light * i) 166 167 168class FixedColorScheme(ColorScheme): 169 """In this color scheme fixed colors are used. 170 171 These colors are provided as a list argument in the constructor. 172 """ 173 174 def __init__(self, keys, colors=[]): 175 super(FixedColorScheme, self).__init__(keys) 176 177 if len(keys) != len(colors): 178 raise ValueError("You must provide as many colors as datasets " 179 "for the fixed color scheme") 180 181 for i, key in enumerate(keys): 182 self[key] = hex2rgb(colors[i]) 183 184 185class RainbowColorScheme(ColorScheme): 186 """In this color scheme the rainbow is divided in N pieces 187 where N is the number of datasets. 188 189 So each dataset gets a color of the rainbow. 190 """ 191 192 def __init__(self, keys, initialColor=DEFAULT_COLOR): 193 super(RainbowColorScheme, self).__init__(keys) 194 if initialColor in basicColors: 195 initialColor = basicColors[initialColor] 196 197 r, g, b = hex2rgb(initialColor) 198 h, s, v = rgb2hsv(r, g, b) 199 200 angleDelta = 360.0 / (len(keys) + 1) 201 for key in keys: 202 self[key] = hsv2rgb(h, s, v) 203 h += angleDelta 204 if h >= 360.0: 205 h -= 360.0 206