1# colormap.py - color scheme for annotation
2#
3# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8import sys, math
9
10def _days(ctx, now):
11    return (now - ctx.date()[0]) / (24 * 60 * 60)
12
13def _rescale(val, step):
14    return float(step) * int(val / step)
15
16def _rescaleceil(val, step):
17    return float(step) * math.ceil(float(val) / step)
18
19class AnnotateColorSaturation(object):
20    def __init__(self, maxhues=None, maxsaturations=None, isdarktheme=False):
21        self._maxhues = maxhues
22        self._maxsaturations = maxsaturations
23        self._isdarktheme = isdarktheme
24
25    def hue(self, angle):
26        return tuple([self.v(angle, r) for r in (0, 120, 240)])
27
28    @staticmethod
29    def ang(angle, rotation):
30        angle += rotation
31        angle = angle % 360
32        if angle > 180:
33            angle = 180 - (angle - 180)
34        return abs(angle)
35
36    def v(self, angle, rotation):
37        ang = self.ang(angle, rotation)
38        if ang < 60:
39            return 1
40        elif ang > 120:
41            return 0
42        else:
43            return 1 - ((ang - 60) / 60)
44
45    def saturate_v(self, saturation, hv):
46        if self._isdarktheme:
47            return int(saturation / 4 * (1 - hv))
48        else:
49            return int(255 - (saturation / 3 * (1 - hv)))
50
51    def committer_angle(self, committer):
52        angle = float(abs(hash(committer))) / sys.maxsize * 360.0
53        if self._maxhues is None:
54            return angle
55        return _rescale(angle, 360.0 / self._maxhues)
56
57    def get_color(self, ctx, now):
58        days = max(_days(ctx, now), 0.0)
59        saturation = 255/((days/50) + 1)
60        if self._maxsaturations:
61            saturation = _rescaleceil(saturation, 255. / self._maxsaturations)
62        hue = self.hue(self.committer_angle(ctx.user()))
63        color = tuple([self.saturate_v(saturation, h) for h in hue])
64        return "#%x%x%x" % color
65
66def makeannotatepalette(fctxs, now, maxcolors, maxhues=None,
67                        maxsaturations=None, mindate=None,
68                        isdarktheme=False):
69    """Assign limited number of colors for annotation
70
71    :fctxs: list of filecontexts by lines
72    :now: latest time which will have most significat color
73    :maxcolors: max number of colors
74    :maxhues: max number of committer angles (hues)
75    :maxsaturations: max number of saturations by age
76    :mindate: reassign palette until it includes fctx of mindate
77              (requires maxsaturations)
78
79    This returns dict of {color: fctxs, ...}.
80    """
81    if mindate is not None and maxsaturations is None:
82        raise ValueError('mindate must be specified with maxsaturations')
83
84    sortedfctxs = list(sorted(set(fctxs), key=lambda fctx: -fctx.date()[0]))
85    return _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues,
86                                maxsaturations, mindate,
87                                isdarktheme)[0]
88
89def _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues,
90                         maxsaturations, mindate,
91                         isdarktheme):
92    cm = AnnotateColorSaturation(maxhues=maxhues,
93                                 maxsaturations=maxsaturations,
94                                 isdarktheme=isdarktheme)
95    palette = {}
96
97    def reassignifneeded(fctx):
98        # fctx is the latest fctx which is NOT included in the palette
99        if mindate is None or fctx.date()[0] < mindate or maxsaturations <= 1:
100            return palette, cm
101        return _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues,
102                                    maxsaturations - 1, mindate,
103                                    isdarktheme)
104
105    # assign from the latest for maximum discrimination
106    for fctx in sortedfctxs:
107        color = cm.get_color(fctx, now)
108        if color not in palette:
109            if len(palette) >= maxcolors:
110                return reassignifneeded(fctx)
111            palette[color] = []
112        palette[color].append(fctx)
113
114    return palette, cm  # return cm for debbugging
115