diff --git a/CHANGELOG b/CHANGELOG index d8ec922..088ab91 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ V 1.5.0 UNRELEASED Add half pie (thanks philt2001) Make lxml an optionnal dependency (huge speed boost in pypy) Add render_table (WIP) + Support colors in rgb / rgba for parametric styles V 1.4.6 Add support for \n separated multiline titles (thanks sirlark) diff --git a/pygal/colors.py b/pygal/colors.py index 0853040..52e438a 100644 --- a/pygal/colors.py +++ b/pygal/colors.py @@ -79,27 +79,78 @@ def hsl_to_rgb(h, s, l): return r, g, b +def parse_color(color): + r = g = b = a = type = None + if color.startswith('#'): + color = color[1:] + if len(color) == 3: + type = '#rgb' + color = color + 'f' + if len(color) == 4: + type = type or '#rgba' + color = ''.join([c * 2 for c in color]) + if len(color) == 6: + type = type or '#rrggbb' + color = color + 'ff' + assert len(color) == 8 + type = type or '#rrggbbaa' + r, g, b, a = [ + int(''.join(c), 16) for c in zip(color[::2], color[1::2])] + a /= 255 + elif color.startswith('rgb('): + type = 'rgb' + color = color[4:-1] + r, g, b, a = [int(c) for c in color.split(',')] + [1] + elif color.startswith('rgba('): + type = 'rgba' + color = color[5:-1] + r, g, b, a = [int(c) for c in color.split(',')[:-1]] + [ + float(color.split(',')[-1])] + return r, g, b, a, type + + +def unparse_color(r, g, b, a, type): + if type == '#rgb': + # Don't lose precision on rgb shortcut + if r % 17 == 0 and g % 17 == 0 and b % 17 == 0: + return '#%x%x%x' % (r / 17, g / 17, b / 17) + type = '#rrggbb' + + if type == '#rgba': + if r % 17 == 0 and g % 17 == 0 and b % 17 == 0: + return '#%x%x%x%x' % (r / 17, g / 17, b / 17, a * 15) + type = '#rrggbbaa' + + if type == '#rrggbb': + return '#%02x%02x%02x' % (r, g, b) + + if type == '#rrggbbaa': + return '#%02x%02x%02x%02x' % (r, g, b, a * 255) + + if type == 'rgb': + return 'rgb(%d, %d, %d)' % (r, g, b) + + if type == 'rgba': + return 'rgba(%d, %d, %d, %g)' % (r, g, b, a) + + +_clamp = lambda x: max(0, min(100, x)) + + +def _adjust(hsl, attribute, percent): + hsl = list(hsl) + if attribute > 0: + hsl[attribute] = _clamp(hsl[attribute] + percent) + else: + hsl[attribute] += percent + + return hsl + + def adjust(color, attribute, percent): - assert color[0] == '#', '#rrggbb and #rgb format are supported' - color = color[1:] - assert len(color) in (3, 6), '#rrggbb and #rgb format are supported' - if len(color) == 3: - color = [a for b in zip(color, color) for a in b] - - bound = lambda x: max(0, min(100, x)) - - def _adjust(hsl): - hsl = list(hsl) - if attribute > 0: - hsl[attribute] = bound(hsl[attribute] + percent) - else: - hsl[attribute] += percent - - return hsl - return '#%02x%02x%02x' % hsl_to_rgb( - *_adjust( - rgb_to_hsl(*map(lambda x: int(''.join(x), 16), - zip(color[::2], color[1::2]))))) + r, g, b, a, type = parse_color(color) + r, g, b = hsl_to_rgb(*_adjust(rgb_to_hsl(r, g, b), attribute, percent)) + return unparse_color(r, g, b, a, type) def rotate(color, percent): diff --git a/pygal/ghost.py b/pygal/ghost.py index 4347010..7e1dab6 100644 --- a/pygal/ghost.py +++ b/pygal/ghost.py @@ -111,8 +111,8 @@ class Ghost(object): .make_instance(overrides=kwargs) .render(is_unicode=is_unicode)) - def render_tree(self): - return self.make_instance().render_tree() + def render_tree(self, **kwargs): + return self.make_instance(overrides=kwargs).render_tree() def render_table(self, **kwargs): # Import here to avoid lxml import @@ -130,29 +130,29 @@ class Ghost(object): from pyquery import PyQuery as pq return pq(self.render(), parser='html') - def render_in_browser(self): + def render_in_browser(self, **kwargs): """Render the graph, open it in your browser with black magic""" try: from lxml.html import open_in_browser except ImportError: raise ImportError('You must install lxml to use render in browser') - open_in_browser(self.render_tree(), encoding='utf-8') + open_in_browser(self.render_tree(**kwargs), encoding='utf-8') - def render_response(self): + def render_response(self, **kwargs): """Render the graph, and return a Flask response""" from flask import Response - return Response(self.render(), mimetype='image/svg+xml') + return Response(self.render(**kwargs), mimetype='image/svg+xml') - def render_to_file(self, filename): + def render_to_file(self, filename, **kwargs): """Render the graph, and write it to filename""" with io.open(filename, 'w', encoding='utf-8') as f: - f.write(self.render(is_unicode=True)) + f.write(self.render(is_unicode=True, **kwargs)) - def render_to_png(self, filename=None, dpi=72): + def render_to_png(self, filename=None, dpi=72, **kwargs): """Render the graph, convert it to png and write it to filename""" import cairosvg return cairosvg.svg2png( - bytestring=self.render(), write_to=filename, dpi=dpi) + bytestring=self.render(**kwargs), write_to=filename, dpi=dpi) def render_sparktext(self, relative_to=None): """Make a mini text sparkline from chart""" diff --git a/pygal/test/test_colors.py b/pygal/test/test_colors.py index c60f72b..2ef9bf3 100644 --- a/pygal/test/test_colors.py +++ b/pygal/test/test_colors.py @@ -1,21 +1,55 @@ +from __future__ import division + from pygal.colors import ( + parse_color, unparse_color, rgb_to_hsl, hsl_to_rgb, darken, lighten, saturate, desaturate, rotate) +def test_parse_color(): + assert parse_color('#123') == (17, 34, 51, 1., '#rgb') + assert parse_color('#cdf') == (204, 221, 255, 1., '#rgb') + assert parse_color('#a3d7') == (170, 51, 221, 119 / 255, '#rgba') + assert parse_color('#584b4f') == (88, 75, 79, 1., '#rrggbb') + assert parse_color('#8cbe22') == (140, 190, 34, 1., '#rrggbb') + assert parse_color('#16cbf055') == (22, 203, 240, 1 / 3, '#rrggbbaa') + assert parse_color('rgb(134, 67, 216)') == (134, 67, 216, 1., 'rgb') + assert parse_color('rgb(0, 111, 222)') == (0, 111, 222, 1., 'rgb') + assert parse_color('rgba(237, 83, 48, .8)') == (237, 83, 48, .8, 'rgba') + assert parse_color('rgba(0, 1, 0, 0.1223)') == (0, 1, 0, .1223, 'rgba') + + +def test_unparse_color(): + assert unparse_color(17, 34, 51, 1., '#rgb') == '#123' + assert unparse_color(204, 221, 255, 1., '#rgb') == '#cdf' + assert unparse_color(170, 51, 221, 119 / 255, '#rgba') == '#a3d7' + assert unparse_color(88, 75, 79, 1., '#rrggbb') == '#584b4f' + assert unparse_color(140, 190, 34, 1., '#rrggbb') == '#8cbe22' + assert unparse_color(22, 203, 240, 1 / 3, '#rrggbbaa') == '#16cbf055' + assert unparse_color(134, 67, 216, 1., 'rgb') == 'rgb(134, 67, 216)' + assert unparse_color(0, 111, 222, 1., 'rgb') == 'rgb(0, 111, 222)' + assert unparse_color(237, 83, 48, .8, 'rgba') == 'rgba(237, 83, 48, 0.8)' + assert unparse_color(0, 1, 0, .1223, 'rgba') == 'rgba(0, 1, 0, 0.1223)' + + def test_darken(): - assert darken('#800', 20) == '#220000' - assert darken('#800', 0) == '#880000' + assert darken('#800', 20) == '#200' + assert darken('#800e', 20) == '#200e' + assert darken('#800', 0) == '#800' assert darken('#ffffff', 10) == '#e6e6e6' assert darken('#000000', 10) == '#000000' assert darken('#f3148a', 25) == '#810747' + assert darken('#f3148aab', 25) == '#810747ab' assert darken('#121212', 1) == '#0f0f0f' assert darken('#999999', 100) == '#000000' + assert darken('#99999999', 100) == '#00000099' assert darken('#1479ac', 8) == '#105f87' + assert darken('rgb(136, 0, 0)', 20) == 'rgb(34, 0, 0)' + assert darken('rgba(20, 121, 172, .13)', 8) == 'rgba(16, 95, 135, 0.13)' def test_lighten(): - assert lighten('#800', 20) == '#ee0000' - assert lighten('#800', 0) == '#880000' + assert lighten('#800', 20) == '#e00' + assert lighten('#800', 0) == '#800' assert lighten('#ffffff', 10) == '#ffffff' assert lighten('#000000', 10) == '#1a1a1a' assert lighten('#f3148a', 25) == '#f98dc6' @@ -25,26 +59,26 @@ def test_lighten(): def test_saturate(): - assert saturate('#000', 20) == '#000000' - assert saturate('#fff', 20) == '#ffffff' - assert saturate('#8a8', 100) == '#33ff33' + assert saturate('#000', 20) == '#000' + assert saturate('#fff', 20) == '#fff' + assert saturate('#8a8', 100) == '#3f3' assert saturate('#855', 20) == '#9e3f3f' def test_desaturate(): - assert desaturate('#000', 20) == '#000000' - assert desaturate('#fff', 20) == '#ffffff' - assert desaturate('#8a8', 100) == '#999999' + assert desaturate('#000', 20) == '#000' + assert desaturate('#fff', 20) == '#fff' + assert desaturate('#8a8', 100) == '#999' assert desaturate('#855', 20) == '#726b6b' def test_rotate(): - assert rotate('#000', 45) == '#000000' - assert rotate('#fff', 45) == '#ffffff' + assert rotate('#000', 45) == '#000' + assert rotate('#fff', 45) == '#fff' assert rotate('#811', 45) == '#886a11' - assert rotate('#8a8', 360) == '#88aa88' - assert rotate('#8a8', 0) == '#88aa88' - assert rotate('#8a8', -360) == '#88aa88' + assert rotate('#8a8', 360) == '#8a8' + assert rotate('#8a8', 0) == '#8a8' + assert rotate('#8a8', -360) == '#8a8' def test_hsl_to_rgb_part_0():