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