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