Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugarpycha/color.py
blob: 82b436f04fce8757ad2d5caf6fafbc100bde3dcf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
#              2009 by Yaco S.L. <lgs@yaco.es>
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha.  If not, see <http://www.gnu.org/licenses/>.

import math

from pycha.utils import clamp


DEFAULT_COLOR = '#3c581a'


def hex2rgb(hexstring, digits=2):
    """Converts a hexstring color to a rgb tuple.

    Example: #ff0000 -> (1.0, 0.0, 0.0)

    digits is an integer number telling how many characters should be
    interpreted for each component in the hexstring.
    """
    if isinstance(hexstring, (tuple, list)):
        return hexstring

    top = float(int(digits * 'f', 16))
    r = int(hexstring[1:digits + 1], 16)
    g = int(hexstring[digits + 1:digits * 2 + 1], 16)
    b = int(hexstring[digits * 2 + 1:digits * 3 + 1], 16)
    return r / top, g / top, b / top


def rgb2hsv(r, g, b):
    """Converts a RGB color into a HSV one

    See http://en.wikipedia.org/wiki/HSV_color_space
    """
    maximum = max(r, g, b)
    minimum = min(r, g, b)
    if maximum == minimum:
        h = 0.0
    elif maximum == r:
        h = 60.0 * ((g - b) / (maximum - minimum)) + 360.0
        if h >= 360.0:
            h -= 360.0
    elif maximum == g:
        h = 60.0 * ((b - r) / (maximum - minimum)) + 120.0
    elif maximum == b:
        h = 60.0 * ((r - g) / (maximum - minimum)) + 240.0

    if maximum == 0.0:
        s = 0.0
    else:
        s = 1.0 - (minimum / maximum)

    v = maximum

    return h, s, v


def hsv2rgb(h, s, v):
    """Converts a HSV color into a RGB one

    See http://en.wikipedia.org/wiki/HSV_color_space
    """
    hi = int(math.floor(h / 60.0)) % 6
    f = (h / 60.0) - hi
    p = v * (1 - s)
    q = v * (1 - f * s)
    t = v * (1 - (1 - f) * s)

    if hi == 0:
        r, g, b = v, t, p
    elif hi == 1:
        r, g, b = q, v, p
    elif hi == 2:
        r, g, b = p, v, t
    elif hi == 3:
        r, g, b = p, q, v
    elif hi == 4:
        r, g, b = t, p, v
    elif hi == 5:
        r, g, b = v, p, q

    return r, g, b


def lighten(r, g, b, amount):
    """Return a lighter version of the color (r, g, b)"""
    return (clamp(0.0, 1.0, r + amount),
            clamp(0.0, 1.0, g + amount),
            clamp(0.0, 1.0, b + amount))


basicColors = dict(
    red='#6d1d1d',
    green=DEFAULT_COLOR,
    blue='#224565',
    grey='#444444',
    black='#000000',
    darkcyan='#305755',
    )


class ColorSchemeMetaclass(type):
    """This metaclass is used to autoregister all ColorScheme classes"""

    def __new__(mcs, name, bases, dict):
        klass = type.__new__(mcs, name, bases, dict)
        klass.registerColorScheme()
        return klass


class ColorScheme(dict):
    """A color scheme is a dictionary where the keys match the keys
    constructor argument and the values are colors"""

    __metaclass__ = ColorSchemeMetaclass
    __registry__ = {}

    def __init__(self, keys):
        super(ColorScheme, self).__init__()

    @classmethod
    def registerColorScheme(cls):
        key = cls.__name__.replace('ColorScheme', '').lower()
        if key:
            cls.__registry__[key] = cls

    @classmethod
    def getColorScheme(cls, name, default=None):
        return cls.__registry__.get(name, default)


class GradientColorScheme(ColorScheme):
    """In this color scheme each color is a lighter version of initialColor.

    This difference is computed based on the number of keys.

    The initialColor is given in a hex string format.
    """

    def __init__(self, keys, initialColor=DEFAULT_COLOR):
        super(GradientColorScheme, self).__init__(keys)
        if initialColor in basicColors:
            initialColor = basicColors[initialColor]

        r, g, b = hex2rgb(initialColor)
        light = 1.0 / (len(keys) * 2)

        for i, key in enumerate(keys):
            self[key] = lighten(r, g, b, light * i)


class FixedColorScheme(ColorScheme):
    """In this color scheme fixed colors are used.

    These colors are provided as a list argument in the constructor.
    """

    def __init__(self, keys, colors=[]):
        super(FixedColorScheme, self).__init__(keys)

        if len(keys) != len(colors):
            raise ValueError("You must provide as many colors as datasets "
                             "for the fixed color scheme")

        for i, key in enumerate(keys):
            self[key] = hex2rgb(colors[i])


class RainbowColorScheme(ColorScheme):
    """In this color scheme the rainbow is divided in N pieces
    where N is the number of datasets.

    So each dataset gets a color of the rainbow.
    """

    def __init__(self, keys, initialColor=DEFAULT_COLOR):
        super(RainbowColorScheme, self).__init__(keys)
        if initialColor in basicColors:
            initialColor = basicColors[initialColor]

        r, g, b = hex2rgb(initialColor)
        h, s, v = rgb2hsv(r, g, b)

        angleDelta = 360.0 / (len(keys) + 1)
        for key in keys:
            self[key] = hsv2rgb(h, s, v)
            h += angleDelta
            if h >= 360.0:
                h -= 360.0