pygal-2.4.0/0000755000175000017500000000000013127127737012623 5ustar zerozero00000000000000pygal-2.4.0/pygal_gen.py0000755000175000017500000000443113114033313015125 0ustar zerozero00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . import argparse import pygal parser = argparse.ArgumentParser( description='Generate pygal chart in command line', prog='pygal_gen') parser.add_argument('-t', '--type', dest='type', default='Line', choices=map(lambda x: x.__name__, pygal.CHARTS), help='Kind of chart to generate') parser.add_argument('-o', '--output', dest='filename', default='pygal_out.svg', help='Filename to write the svg to') parser.add_argument('-s', '--serie', dest='series', nargs='+', action='append', help='Add a serie in the form (title val1 val2...)') parser.add_argument('--version', action='version', version='pygal %s' % pygal.__version__) for key in pygal.config.CONFIG_ITEMS: opt_name = key.name val = key.value opts = {} if key.type == list: opts['type'] = key.subtype opts['nargs'] = '+' else: opts['type'] = key.type if opts['type'] == bool: del opts['type'] opts['action'] = 'store_true' if not val else 'store_false' if val: opt_name = 'no-' + opt_name if key.name == 'interpolate': opts['choices'] = list(pygal.interpolate.INTERPOLATIONS.keys()) parser.add_argument( '--%s' % opt_name, dest=key.name, default=val, **opts) config = parser.parse_args() chart = getattr(pygal, config.type)(**vars(config)) for serie in config.series: chart.add(serie[0], map(float, serie[1:])) chart.render_to_file(config.filename) pygal-2.4.0/setup.cfg0000644000175000017500000000025613127127737014447 0ustar zerozero00000000000000[wheel] universal = 1 [pytest] flake8-ignore = *.py E731 E402 pygal/__init__.py F401 pygal/_compat.py F821 F401 docs/conf.py ALL [egg_info] tag_build = tag_date = 0 pygal-2.4.0/README0000644000175000017500000000355412707661227013512 0ustar zerozero00000000000000# Pygal [![Build Status](https://travis-ci.org/Kozea/pygal.svg?branch=master)](https://travis-ci.org/Kozea/pygal) [![Coverage Status](https://coveralls.io/repos/Kozea/pygal/badge.svg?branch=master&service=github)](https://coveralls.io/github/Kozea/pygal?branch=master) [![Documentation Status](https://readthedocs.org/projects/pygal/badge/?version=latest)](https://readthedocs.org/projects/pygal/?badge=latest) @@TOC@@ ## Description **pygal** is a dynamic SVG charting library written in python. All the documentation is on http://pygal.org ## Installation As simple as: ``` $ pip install pygal ``` ## Test Pygal is tested with py.test: ``` $ pip install pytest $ py.test ``` ## Contribute You are welcomed to fork the project and make pull requests. Be sure to create a branch for each feature, write tests if needed and run the current tests ! You can also support the project: [![Flattr](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=paradoxxx_zero&url=https://github.com/Kozea/pygal&title=Pygal&tags=github&category=software) [![gittip](http://i.imgur.com/IKcQB2P.png)](https://www.gittip.com/paradoxxxzero/) ## License Copyright © 2012-2016 Kozea LGPLv3: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . pygal-2.4.0/pygal/0000755000175000017500000000000013127127737013737 5ustar zerozero00000000000000pygal-2.4.0/pygal/_compat.py0000644000175000017500000000523713114033322015720 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Various hacks for transparent python 2 / python 3 support""" from __future__ import division import sys from collections import Iterable from datetime import datetime, timedelta, tzinfo if sys.version_info[0] == 3: base = (str, bytes) coerce = str _ellipsis = eval('...') else: base = basestring coerce = unicode class EllipsisGetter(object): def __getitem__(self, key): return key _ellipsis = EllipsisGetter()[...] def is_list_like(value): """Return whether value is an iterable but not a mapping / string""" return isinstance(value, Iterable) and not isinstance(value, (base, dict)) def is_str(string): """Return whether value is a string or a byte list""" return isinstance(string, base) def to_str(obj): """Cast obj to unicode string""" if not is_str(obj): return coerce(obj) return obj def to_unicode(string): """Force string to be a string in python 3 or a unicode in python 2""" if not isinstance(string, coerce): return string.decode('utf-8') return string def u(s): """Emulate u'str' in python 2, do nothing in python 3""" if sys.version_info[0] == 2: return s.decode('utf-8') return s try: from datetime import timezone utc = timezone.utc except ImportError: class UTC(tzinfo): def tzname(self, dt): return 'UTC' def utcoffset(self, dt): return timedelta(0) def dst(self, dt): return None utc = UTC() def timestamp(x): """Get a timestamp from a date in python 3 and python 2""" if x.tzinfo is None: # Naive dates to utc x = x.replace(tzinfo=utc) if hasattr(x, 'timestamp'): return x.timestamp() else: return (x - datetime(1970, 1, 1, tzinfo=utc)).total_seconds() try: from urllib import quote_plus except ImportError: from urllib.parse import quote_plus pygal-2.4.0/pygal/__about__.py0000644000175000017500000000063113127127735016215 0ustar zerozero00000000000000__title__ = "pygal" __version__ = "2.4.0" __summary__ = "A python svg graph plotting library" __uri__ = "http://pygal.org/" __author__ = "Florian Mounier" __email__ = "florian.mounier@kozea.fr" __license__ = "GNU LGPL v3+" __copyright__ = "Copyright 2017 %s" % __author__ __all__ = [ '__title__', '__version__', '__summary__', '__uri__', '__author__', '__email__', '__license__', '__copyright__' ] pygal-2.4.0/pygal/colors.py0000644000175000017500000001315113114033322015571 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ This package is an utility package oriented on color alteration. This is used by the :py:mod:`pygal.style` package to generate parametric styles. """ from __future__ import division def normalize_float(f): """Round float errors""" if abs(f - round(f)) < .0000000000001: return round(f) return f def rgb_to_hsl(r, g, b): """Convert a color in r, g, b to a color in h, s, l""" r = r or 0 g = g or 0 b = b or 0 r /= 255 g /= 255 b /= 255 max_ = max((r, g, b)) min_ = min((r, g, b)) d = max_ - min_ if not d: h = 0 elif r is max_: h = 60 * (g - b) / d elif g is max_: h = 60 * (b - r) / d + 120 else: h = 60 * (r - g) / d + 240 l = .5 * (max_ + min_) if not d: s = 0 elif l < 0.5: s = .5 * d / l else: s = .5 * d / (1 - l) return tuple(map(normalize_float, (h % 360, s * 100, l * 100))) def hsl_to_rgb(h, s, l): """Convert a color in h, s, l to a color in r, g, b""" h /= 360 s /= 100 l /= 100 m2 = l * (s + 1) if l <= .5 else l + s - l * s m1 = 2 * l - m2 def h_to_rgb(h): h = h % 1 if 6 * h < 1: return m1 + 6 * h * (m2 - m1) if 2 * h < 1: return m2 if 3 * h < 2: return m1 + 6 * (2 / 3 - h) * (m2 - m1) return m1 r, g, b = map(lambda x: round(x * 255), map(h_to_rgb, (h + 1 / 3, h, h - 1 / 3))) return r, g, b def parse_color(color): """Take any css color definition and give back a tuple containing the r, g, b, a values along with a type which can be: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb, rgba """ 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): """ Take the r, g, b, a color values and give back a type css color string. This is the inverse function of parse_color """ 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' % (int(r / 17), int(g / 17), int(b / 17)) type = '#rrggbb' if type == '#rgba': if r % 17 == 0 and g % 17 == 0 and b % 17 == 0: return '#%x%x%x%x' % (int(r / 17), int(g / 17), int(b / 17), int(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, int(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) def is_foreground_light(color): """ Determine if the background color need a light or dark foreground color """ return rgb_to_hsl(*parse_color(color)[:3])[2] < 17.9 _clamp = lambda x: max(0, min(100, x)) def _adjust(hsl, attribute, percent): """Internal adjust function""" hsl = list(hsl) if attribute > 0: hsl[attribute] = _clamp(hsl[attribute] + percent) else: hsl[attribute] += percent return hsl def adjust(color, attribute, percent): """Adjust an attribute of color by a percent""" 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): """Rotate a color by changing its hue value by percent""" return adjust(color, 0, percent) def saturate(color, percent): """Saturate a color by increasing its saturation by percent""" return adjust(color, 1, percent) def desaturate(color, percent): """Desaturate a color by decreasing its saturation by percent""" return adjust(color, 1, -percent) def lighten(color, percent): """Lighten a color by increasing its lightness by percent""" return adjust(color, 2, percent) def darken(color, percent): """Darken a color by decreasing its lightness by percent""" return adjust(color, 2, -percent) pygal-2.4.0/pygal/style.py0000644000175000017500000003607513114033322015442 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Charts styling classes""" from __future__ import division from itertools import chain from pygal import colors from pygal.colors import darken, is_foreground_light, lighten class Style(object): """Styling class containing colors for the css generation""" plot_background = 'rgba(255, 255, 255, 1)' background = 'rgba(249, 249, 249, 1)' value_background = 'rgba(229, 229, 229, 1)' foreground = 'rgba(0, 0, 0, .87)' foreground_strong = 'rgba(0, 0, 0, 1)' foreground_subtle = 'rgba(0, 0, 0, .54)' # Monospaced font is highly encouraged font_family = ( 'Consolas, "Liberation Mono", Menlo, Courier, monospace') label_font_family = None major_label_font_family = None value_font_family = None value_label_font_family = None tooltip_font_family = None title_font_family = None legend_font_family = None no_data_font_family = None label_font_size = 10 major_label_font_size = 10 value_font_size = 16 value_label_font_size = 10 tooltip_font_size = 14 title_font_size = 16 legend_font_size = 14 no_data_font_size = 64 # Guide line dash array style guide_stroke_dasharray = '4,4' major_guide_stroke_dasharray = '6,6' opacity = '.7' opacity_hover = '.8' stroke_opacity = '.8' stroke_opacity_hover = '.9' transition = '150ms' colors = ( '#F44336', # 0 '#3F51B5', # 4 '#009688', # 8 '#FFC107', # 13 '#FF5722', # 15 '#9C27B0', # 2 '#03A9F4', # 6 '#8BC34A', # 10 '#FF9800', # 14 '#E91E63', # 1 '#2196F3', # 5 '#4CAF50', # 9 '#FFEB3B', # 12 '#673AB7', # 3 '#00BCD4', # 7 '#CDDC39', # 11b '#9E9E9E', # 17 '#607D8B', # 18 ) value_colors = () ci_colors = () def __init__(self, **kwargs): """Create the style""" self.__dict__.update(kwargs) self._google_fonts = set() if self.font_family.startswith('googlefont:'): self.font_family = self.font_family.replace('googlefont:', '') self._google_fonts.add(self.font_family.split(',')[0].strip()) for name in dir(self): if name.endswith('_font_family'): fn = getattr(self, name) if fn is None: setattr(self, name, self.font_family) elif fn.startswith('googlefont:'): setattr(self, name, fn.replace('googlefont:', '')) self._google_fonts.add( getattr(self, name).split(',')[0].strip()) def get_colors(self, prefix, len_): """Get the css color list""" def color(tupl): """Make a color css""" return (( '%s.color-{0}, %s.color-{0} a:visited {{\n' ' stroke: {1};\n' ' fill: {1};\n' '}}\n') % (prefix, prefix)).format(*tupl) def value_color(tupl): """Make a value color css""" return (( '%s .text-overlay .color-{0} text {{\n' ' fill: {1};\n' '}}\n') % (prefix,)).format(*tupl) def ci_color(tupl): """Make a value color css""" if not tupl[1]: return '' return (( '%s .color-{0} .ci {{\n' ' stroke: {1};\n' '}}\n') % (prefix,)).format(*tupl) if len(self.colors) < len_: missing = len_ - len(self.colors) cycles = 1 + missing // len(self.colors) colors = [] for i in range(0, cycles + 1): for color_ in self.colors: colors.append(darken(color_, 33 * i / cycles)) if len(colors) >= len_: break else: continue break else: colors = self.colors[:len_] # Auto compute foreground value color when color is missing value_colors = [] for i in range(len_): if i < len(self.value_colors) and self.value_colors[i] is not None: value_colors.append(self.value_colors[i]) else: value_colors.append('white' if is_foreground_light( colors[i]) else 'black') return '\n'.join(chain( map(color, enumerate(colors)), map(value_color, enumerate(value_colors)), map(ci_color, enumerate(self.ci_colors)))) def to_dict(self): """Convert instance to a serializable mapping.""" config = {} for attr in dir(self): if not attr.startswith('_'): value = getattr(self, attr) if not hasattr(value, '__call__'): config[attr] = value return config DefaultStyle = Style class DarkStyle(Style): """A dark style (old default)""" background = 'black' plot_background = '#111' foreground = '#999' foreground_strong = '#eee' foreground_subtle = '#555' opacity = '.8' opacity_hover = '.4' transition = '250ms' colors = ( '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#899ca1', '#f8f8f2', '#bf4646', '#516083', '#f92672', '#82b414', '#fd971f', '#56c2d6', '#808384', '#8c54fe', '#465457') class LightStyle(Style): """A light style""" background = 'white' plot_background = 'rgba(0, 0, 255, 0.1)' foreground = 'rgba(0, 0, 0, 0.7)' foreground_strong = 'rgba(0, 0, 0, 0.9)' foreground_subtle = 'rgba(0, 0, 0, 0.5)' colors = ('#242424', '#9f6767', '#92ac68', '#d0d293', '#9aacc3', '#bb77a4', '#77bbb5', '#777777') class NeonStyle(DarkStyle): """Similar to DarkStyle but with more opacity and effects""" opacity = '.1' opacity_hover = '.75' transition = '1s ease-out' class CleanStyle(Style): """A rather clean style""" background = 'transparent' plot_background = 'rgba(240, 240, 240, 0.7)' foreground = 'rgba(0, 0, 0, 0.9)' foreground_strong = 'rgba(0, 0, 0, 0.9)' foreground_subtle = 'rgba(0, 0, 0, 0.5)' colors = ( 'rgb(12,55,149)', 'rgb(117,38,65)', 'rgb(228,127,0)', 'rgb(159,170,0)', 'rgb(149,12,12)') class DarkSolarizedStyle(Style): """Dark solarized popular theme""" background = '#073642' plot_background = '#002b36' foreground = '#839496' foreground_strong = '#fdf6e3' foreground_subtle = '#657b83' opacity = '.66' opacity_hover = '.9' transition = '500ms ease-in' colors = ( '#b58900', '#cb4b16', '#dc322f', '#d33682', '#6c71c4', '#268bd2', '#2aa198', '#859900') class LightSolarizedStyle(DarkSolarizedStyle): """Light solarized popular theme""" background = '#fdf6e3' plot_background = '#eee8d5' foreground = '#657b83' foreground_strong = '#073642' foreground_subtle = '#073642' class RedBlueStyle(Style): """A red and blue theme""" background = lighten('#e6e7e9', 7) plot_background = lighten('#e6e7e9', 10) foreground = 'rgba(0, 0, 0, 0.9)' foreground_strong = 'rgba(0, 0, 0, 0.9)' foreground_subtle = 'rgba(0, 0, 0, 0.5)' opacity = '.6' opacity_hover = '.9' colors = ( '#d94e4c', '#e5884f', '#39929a', lighten('#d94e4c', 10), darken('#39929a', 15), lighten('#e5884f', 17), darken('#d94e4c', 10), '#234547') class LightColorizedStyle(Style): """A light colorized style""" background = '#f8f8f8' plot_background = lighten('#f8f8f8', 3) foreground = '#333' foreground_strong = '#666' foreground_subtle = 'rgba(0, 0 , 0, 0.5)' opacity = '.5' opacity_hover = '.9' transition = '250ms ease-in' colors = ( '#fe9592', '#534f4c', '#3ac2c0', '#a2a7a1', darken('#fe9592', 15), lighten('#534f4c', 15), lighten('#3ac2c0', 15), lighten('#a2a7a1', 15), lighten('#fe9592', 15), darken('#3ac2c0', 10)) class DarkColorizedStyle(Style): """A dark colorized style""" background = darken('#3a2d3f', 5) plot_background = lighten('#3a2d3f', 2) foreground = 'rgba(255, 255, 255, 0.9)' foreground_strong = 'rgba(255, 255, 255, 0.9)' foreground_subtle = 'rgba(255, 255 , 255, 0.5)' opacity = '.2' opacity_hover = '.7' transition = '250ms ease-in' colors = ( '#c900fe', '#01b8fe', '#59f500', '#ff00e4', '#f9fa00', darken('#c900fe', 20), darken('#01b8fe', 15), darken('#59f500', 20), darken('#ff00e4', 15), lighten('#f9fa00', 20)) class TurquoiseStyle(Style): """A turquoise style""" background = darken('#1b8088', 15) plot_background = darken('#1b8088', 17) foreground = 'rgba(255, 255, 255, 0.9)' foreground_strong = 'rgba(255, 255, 255, 0.9)' foreground_subtle = 'rgba(255, 255 , 255, 0.5)' opacity = '.5' opacity_hover = '.9' transition = '250ms ease-in' colors = ( '#93d2d9', '#ef940f', '#8C6243', '#fff', darken('#93d2d9', 20), lighten('#ef940f', 15), lighten('#8c6243', 15), '#1b8088') class LightGreenStyle(Style): """A light green style""" background = lighten('#f3f3f3', 3) plot_background = '#fff' foreground = '#333333' foreground_strong = '#666' foreground_subtle = '#222222' opacity = '.5' opacity_hover = '.9' transition = '250ms ease-in' colors = ( '#7dcf30', '#247fab', lighten('#7dcf30', 10), '#ccc', darken('#7dcf30', 15), '#ddd', lighten('#247fab', 10), darken('#247fab', 15)) class DarkGreenStyle(Style): """A dark green style""" background = darken('#251e01', 3) plot_background = darken('#251e01', 1) foreground = 'rgba(255, 255, 255, 0.9)' foreground_strong = 'rgba(255, 255, 255, 0.9)' foreground_subtle = 'rgba(255, 255, 255, 0.6)' opacity = '.6' opacity_hover = '.9' transition = '250ms ease-in' colors = ( '#adde09', '#6e8c06', '#4a5e04', '#fcd202', '#C1E34D', lighten('#fcd202', 25)) class DarkGreenBlueStyle(Style): """A dark green and blue style""" background = '#000' plot_background = lighten('#000', 8) foreground = 'rgba(255, 255, 255, 0.9)' foreground_strong = 'rgba(255, 255, 255, 0.9)' foreground_subtle = 'rgba(255, 255, 255, 0.6)' opacity = '.55' opacity_hover = '.9' transition = '250ms ease-in' colors = (lighten('#34B8F7', 15), '#7dcf30', '#247fab', darken('#7dcf30', 10), lighten('#247fab', 10), lighten('#7dcf30', 10), darken('#247fab', 10), '#fff') class BlueStyle(Style): """A blue style""" background = darken('#f8f8f8', 3) plot_background = '#f8f8f8' foreground = 'rgba(0, 0, 0, 0.9)' foreground_strong = 'rgba(0, 0, 0, 0.9)' foreground_subtle = 'rgba(0, 0, 0, 0.6)' opacity = '.5' opacity_hover = '.9' transition = '250ms ease-in' colors = ( '#00b2f0', '#43d9be', '#0662ab', darken('#00b2f0', 20), lighten('#43d9be', 20), lighten('#7dcf30', 10), darken('#0662ab', 15), '#ffd541', '#7dcf30', lighten('#00b2f0', 15), darken('#ffd541', 20)) class SolidColorStyle(Style): """A light style with strong colors""" background = '#FFFFFF' plot_background = '#FFFFFF' foreground = '#000000' foreground_strong = '#000000' foreground_subtle = '#828282' opacity = '.8' opacity_hover = '.9' transition = '400ms ease-in' colors = ( '#FF9900', '#DC3912', '#4674D1', '#109618', '#990099', '#0099C6', '#DD4477', '#74B217', '#B82E2E', '#316395', '#994499') styles = {'default': DefaultStyle, 'dark': DarkStyle, 'light': LightStyle, 'neon': NeonStyle, 'clean': CleanStyle, 'light_red_blue': RedBlueStyle, 'dark_solarized': DarkSolarizedStyle, 'light_solarized': LightSolarizedStyle, 'dark_colorized': DarkColorizedStyle, 'light_colorized': LightColorizedStyle, 'turquoise': TurquoiseStyle, 'green': LightGreenStyle, 'dark_green': DarkGreenStyle, 'dark_green_blue': DarkGreenBlueStyle, 'blue': BlueStyle, 'solid_color': SolidColorStyle} class ParametricStyleBase(Style): """Parametric Style base class for all the parametric operations""" _op = None def __init__(self, color, step=10, max_=None, base_style=None, **kwargs): """ Initialization of the parametric style. This takes several parameters: * a `step` which correspond on how many colors will be needed * a `max_` which defines the maximum amplitude of the color effect * a `base_style` which will be taken as default for everything except colors * any keyword arguments setting other style parameters """ if self._op is None: raise RuntimeError('ParametricStyle is not instanciable') defaults = {} if base_style is not None: if isinstance(base_style, type): base_style = base_style() defaults.update(base_style.to_dict()) defaults.update(kwargs) super(ParametricStyleBase, self).__init__(**defaults) if max_ is None: violency = { 'darken': 50, 'lighten': 50, 'saturate': 100, 'desaturate': 100, 'rotate': 360 } max_ = violency[self._op] def modifier(index): percent = max_ * index / (step - 1) return getattr(colors, self._op)(color, percent) self.colors = list(map(modifier, range(0, max(2, step)))) class LightenStyle(ParametricStyleBase): """Create a style by lightening the given color""" _op = 'lighten' class DarkenStyle(ParametricStyleBase): """Create a style by darkening the given color""" _op = 'darken' class SaturateStyle(ParametricStyleBase): """Create a style by saturating the given color""" _op = 'saturate' class DesaturateStyle(ParametricStyleBase): """Create a style by desaturating the given color""" _op = 'desaturate' class RotateStyle(ParametricStyleBase): """Create a style by rotating the given color""" _op = 'rotate' parametric_styles = { 'lighten': LightenStyle, 'darken': DarkenStyle, 'saturate': SaturateStyle, 'desaturate': DesaturateStyle, 'rotate': RotateStyle } pygal-2.4.0/pygal/maps/0000755000175000017500000000000013127127737014677 5ustar zerozero00000000000000pygal-2.4.0/pygal/maps/__init__.py0000644000175000017500000000145512707661227017015 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Maps extensions namespace module""" pygal-2.4.0/pygal/etree.py0000644000175000017500000000410313114033322015371 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Wrapper for seamless lxml.etree / xml.etree usage depending on whether lxml is installed or not. """ import os class Etree(object): """Etree wrapper using lxml.etree or standard xml.etree""" def __init__(self): """Create the wrapper""" from xml.etree import ElementTree as _py_etree self._py_etree = _py_etree try: from lxml import etree as _lxml_etree self._lxml_etree = _lxml_etree except ImportError: self._lxml_etree = None if os.getenv('NO_LXML', None): self._etree = self._py_etree else: self._etree = self._lxml_etree or self._py_etree self.lxml = self._etree is self._lxml_etree def __getattribute__(self, attr): """Retrieve attr from current active etree implementation""" if (attr not in object.__getattribute__(self, '__dict__') and attr not in Etree.__dict__): return object.__getattribute__(self._etree, attr) return object.__getattribute__(self, attr) def to_lxml(self): """Force lxml.etree to be used""" self._etree = self._lxml_etree self.lxml = True def to_etree(self): """Force xml.etree to be used""" self._etree = self._py_etree self.lxml = False etree = Etree() pygal-2.4.0/pygal/stats.py0000644000175000017500000000417213114033322015431 0ustar zerozero00000000000000from math import log, pi, sqrt def erfinv(x, a=.147): """Approximation of the inverse error function https://en.wikipedia.org/wiki/Error_function #Approximation_with_elementary_functions """ lnx = log(1 - x * x) part1 = (2 / (a * pi) + lnx / 2) part2 = lnx / a sgn = 1 if x > 0 else -1 return sgn * sqrt(sqrt(part1 * part1 - part2) - part1) def norm_ppf(x): if not 0 < x < 1: raise ValueError("Can't compute the percentage point for value %d" % x) return sqrt(2) * erfinv(2 * x - 1) def ppf(x, n): try: from scipy import stats except ImportError: stats = None if stats: if n < 30: return stats.t.ppf(x, n) return stats.norm.ppf(x) else: if n < 30: # TODO: implement power series: # http://eprints.maths.ox.ac.uk/184/1/tdist.pdf raise ImportError( 'You must have scipy installed to use t-student ' 'when sample_size is below 30') return norm_ppf(x) # According to http://sphweb.bumc.bu.edu/otlt/MPH-Modules/BS/ # BS704_Confidence_Intervals/BS704_Confidence_Intervals_print.html def confidence_interval_continuous( point_estimate, stddev, sample_size, confidence=.95, **kwargs): """Continuous confidence interval from sample size and standard error""" alpha = ppf((confidence + 1) / 2, sample_size - 1) margin = stddev / sqrt(sample_size) return (point_estimate - alpha * margin, point_estimate + alpha * margin) def confidence_interval_dichotomous( point_estimate, sample_size, confidence=.95, bias=False, percentage=True, **kwargs): """Dichotomous confidence interval from sample size and maybe a bias""" alpha = ppf((confidence + 1) / 2, sample_size - 1) p = point_estimate if percentage: p /= 100 margin = sqrt(p * (1 - p) / sample_size) if bias: margin += .5 / sample_size if percentage: margin *= 100 return (point_estimate - alpha * margin, point_estimate + alpha * margin) def confidence_interval_manual(point_estimate, low, high): return (low, high) pygal-2.4.0/pygal/svg.py0000644000175000017500000004426213127124121015100 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Svg helper""" from __future__ import division import io import json import os from datetime import date, datetime from math import pi from numbers import Number from pygal import __version__ from pygal._compat import quote_plus, to_str, u from pygal.etree import etree from pygal.util import ( coord_abs_project, coord_diff, coord_dual, coord_format, coord_project, minify_css, template) nearly_2pi = 2 * pi - .00001 class Svg(object): """Svg related methods""" ns = 'http://www.w3.org/2000/svg' xlink_ns = 'http://www.w3.org/1999/xlink' def __init__(self, graph): """Create the svg helper with the chart instance""" self.graph = graph if not graph.no_prefix: self.id = '#chart-%s ' % graph.uuid else: self.id = '' self.processing_instructions = [] if etree.lxml: attrs = { 'nsmap': { None: self.ns, 'xlink': self.xlink_ns } } else: attrs = { 'xmlns': self.ns } if hasattr(etree, 'register_namespace'): etree.register_namespace('xlink', self.xlink_ns) else: etree._namespace_map[self.xlink_ns] = 'xlink' self.root = etree.Element('svg', **attrs) self.root.attrib['id'] = self.id.lstrip('#').rstrip() if graph.classes: self.root.attrib['class'] = ' '.join(graph.classes) self.root.append( etree.Comment(u( 'Generated with pygal %s (%s) ©Kozea 2012-2016 on %s' % ( __version__, 'lxml' if etree.lxml else 'etree', date.today().isoformat())))) self.root.append(etree.Comment(u('http://pygal.org'))) self.root.append(etree.Comment(u('http://github.com/Kozea/pygal'))) self.defs = self.node(tag='defs') self.title = self.node(tag='title') self.title.text = graph.title or 'Pygal' for def_ in self.graph.defs: self.defs.append(etree.fromstring(def_)) def add_styles(self): """Add the css to the svg""" colors = self.graph.style.get_colors(self.id, self.graph._order) strokes = self.get_strokes() all_css = [] auto_css = ['file://base.css'] if self.graph.style._google_fonts: auto_css.append( '//fonts.googleapis.com/css?family=%s' % quote_plus( '|'.join(self.graph.style._google_fonts)) ) for css in auto_css + list(self.graph.css): css_text = None if css.startswith('inline:'): css_text = css[len('inline:'):] elif css.startswith('file://'): css = css[len('file://'):] if not os.path.exists(css): css = os.path.join( os.path.dirname(__file__), 'css', css) with io.open(css, encoding='utf-8') as f: css_text = template( f.read(), style=self.graph.style, colors=colors, strokes=strokes, id=self.id) if css_text is not None: if not self.graph.pretty_print: css_text = minify_css(css_text) all_css.append(css_text) else: if css.startswith('//') and self.graph.force_uri_protocol: css = '%s:%s' % (self.graph.force_uri_protocol, css) self.processing_instructions.append( etree.PI( u('xml-stylesheet'), u('href="%s"' % css))) self.node( self.defs, 'style', type='text/css').text = '\n'.join(all_css) def add_scripts(self): """Add the js to the svg""" common_script = self.node(self.defs, 'script', type='text/javascript') def get_js_dict(): return dict( (k, getattr(self.graph.state, k)) for k in dir(self.graph.config) if not k.startswith('_') and hasattr(self.graph.state, k) and not hasattr(getattr(self.graph.state, k), '__call__')) def json_default(o): if isinstance(o, (datetime, date)): return o.isoformat() if hasattr(o, 'to_dict'): return o.to_dict() return json.JSONEncoder().default(o) dct = get_js_dict() # Config adds dct['legends'] = [ l.get('title') if isinstance(l, dict) else l for l in self.graph._legends + self.graph._secondary_legends] common_js = 'window.pygal = window.pygal || {};' common_js += 'window.pygal.config = window.pygal.config || {};' if self.graph.no_prefix: common_js += 'window.pygal.config = ' else: common_js += 'window.pygal.config[%r] = ' % self.graph.uuid common_script.text = common_js + json.dumps(dct, default=json_default) for js in self.graph.js: if js.startswith('file://'): script = self.node(self.defs, 'script', type='text/javascript') with io.open(js[len('file://'):], encoding='utf-8') as f: script.text = f.read() else: if js.startswith('//') and self.graph.force_uri_protocol: js = '%s:%s' % (self.graph.force_uri_protocol, js) self.node(self.defs, 'script', type='text/javascript', href=js) def node(self, parent=None, tag='g', attrib=None, **extras): """Make a new svg node""" if parent is None: parent = self.root attrib = attrib or {} attrib.update(extras) def in_attrib_and_number(key): return key in attrib and isinstance(attrib[key], Number) for pos, dim in (('x', 'width'), ('y', 'height')): if in_attrib_and_number(dim) and attrib[dim] < 0: attrib[dim] = - attrib[dim] if in_attrib_and_number(pos): attrib[pos] = attrib[pos] - attrib[dim] for key, value in dict(attrib).items(): if value is None: del attrib[key] attrib[key] = to_str(value) if key.endswith('_'): attrib[key.rstrip('_')] = attrib[key] del attrib[key] elif key == 'href': attrib[etree.QName( 'http://www.w3.org/1999/xlink', key)] = attrib[key] del attrib[key] return etree.SubElement(parent, tag, attrib) def transposable_node(self, parent=None, tag='g', attrib=None, **extras): """Make a new svg node which can be transposed if horizontal""" if self.graph.horizontal: for key1, key2 in (('x', 'y'), ('width', 'height'), ('cx', 'cy')): attr1 = extras.get(key1, None) attr2 = extras.get(key2, None) if attr2: extras[key1] = attr2 elif attr1: del extras[key1] if attr1: extras[key2] = attr1 elif attr2: del extras[key2] return self.node(parent, tag, attrib, **extras) def serie(self, serie): """Make serie node""" return dict( plot=self.node( self.graph.nodes['plot'], class_='series serie-%d color-%d' % ( serie.index, serie.index)), overlay=self.node( self.graph.nodes['overlay'], class_='series serie-%d color-%d' % ( serie.index, serie.index)), text_overlay=self.node( self.graph.nodes['text_overlay'], class_='series serie-%d color-%d' % ( serie.index, serie.index))) def line(self, node, coords, close=False, **kwargs): """Draw a svg line""" line_len = len(coords) if len([c for c in coords if c[1] is not None]) < 2: return root = 'M%s L%s Z' if close else 'M%s L%s' origin_index = 0 while origin_index < line_len and None in coords[origin_index]: origin_index += 1 if origin_index == line_len: return if self.graph.horizontal: coord_format = lambda xy: '%f %f' % (xy[1], xy[0]) else: coord_format = lambda xy: '%f %f' % xy origin = coord_format(coords[origin_index]) line = ' '.join([coord_format(c) for c in coords[origin_index + 1:] if None not in c]) return self.node( node, 'path', d=root % (origin, line), **kwargs) def slice( self, serie_node, node, radius, small_radius, angle, start_angle, center, val, i, metadata): """Draw a pie slice""" if angle == 2 * pi: angle = nearly_2pi if angle > 0: to = [coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle)] rv = self.node( node, 'path', d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( to[0], coord_dual(radius), int(angle > pi), to[1], to[2], coord_dual(small_radius), int(angle > pi), to[3]), class_='slice reactive tooltip-trigger') else: rv = None x, y = coord_diff(center, coord_project( (radius + small_radius) / 2, start_angle + angle / 2)) self.graph._tooltip_data( node, val, x, y, "centered", self.graph._x_labels and self.graph._x_labels[i][0]) if angle >= 0.3: # 0.3 radians is about 17 degrees self.graph._static_value(serie_node, val, x, y, metadata) return rv def gauge_background( self, serie_node, start_angle, center, radius, small_radius, end_angle, half_pie, max_value): if end_angle == 2 * pi: end_angle = nearly_2pi to_shade = [ coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, end_angle), coord_abs_project(center, small_radius, end_angle), coord_abs_project(center, small_radius, start_angle)] self.node( serie_node['plot'], 'path', d='M%s A%s 0 1 1 %s L%s A%s 0 1 0 %s z' % ( to_shade[0], coord_dual(radius), to_shade[1], to_shade[2], coord_dual(small_radius), to_shade[3]), class_='gauge-background reactive') if half_pie: begin_end = [ coord_diff( center, coord_project( radius - (radius - small_radius) / 2, start_angle)), coord_diff( center, coord_project( radius - (radius - small_radius) / 2, end_angle))] pos = 0 for i in begin_end: self.node( serie_node['plot'], 'text', class_='y-{} bound reactive'.format(pos), x=i[0], y=i[1] + 10, attrib={'text-anchor': 'middle'} ).text = '{}'.format(0 if pos == 0 else max_value) pos += 1 else: middle_radius = .5 * (radius + small_radius) # Correct text vertical alignment middle_radius -= .1 * (radius - small_radius) to_labels = [ coord_abs_project( center, middle_radius, 0), coord_abs_project( center, middle_radius, nearly_2pi) ] self.node( self.defs, 'path', id='valuePath-%s%s' % center, d='M%s A%s 0 1 1 %s' % ( to_labels[0], coord_dual(middle_radius), to_labels[1] )) text_ = self.node( serie_node['text_overlay'], 'text') self.node( text_, 'textPath', class_='max-value reactive', attrib={ 'href': '#valuePath-%s%s' % center, 'startOffset': '99%', 'text-anchor': 'end' } ).text = max_value def solid_gauge( self, serie_node, node, radius, small_radius, angle, start_angle, center, val, i, metadata, half_pie, end_angle, max_value): """Draw a solid gauge slice and background slice""" if angle == 2 * pi: angle = nearly_2pi if angle > 0: to = [coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle)] self.node( node, 'path', d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( to[0], coord_dual(radius), int(angle > pi), to[1], to[2], coord_dual(small_radius), int(angle > pi), to[3]), class_='slice reactive tooltip-trigger') else: return x, y = coord_diff(center, coord_project( (radius + small_radius) / 2, start_angle + angle / 2)) self.graph._static_value(serie_node, val, x, y, metadata, 'middle') self.graph._tooltip_data( node, val, x, y, "centered", self.graph._x_labels and self.graph._x_labels[i][0]) def confidence_interval(self, node, x, low, high, width=7): if self.graph.horizontal: fmt = lambda xy: '%f %f' % (xy[1], xy[0]) else: fmt = coord_format shr = lambda xy: (xy[0] + width, xy[1]) shl = lambda xy: (xy[0] - width, xy[1]) top = (x, high) bottom = (x, low) ci = self.node(node, class_="ci") self.node( ci, 'path', d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple( map(fmt, ( top, shr(top), top, shl(top), top, bottom, shr(bottom), bottom, shl(bottom) )) ), class_='nofill reactive' ) def pre_render(self): """Last things to do before rendering""" self.add_styles() self.add_scripts() self.root.set( 'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height)) if self.graph.explicit_size: self.root.set('width', str(self.graph.width)) self.root.set('height', str(self.graph.height)) def draw_no_data(self): """Write the no data text to the svg""" no_data = self.node(self.graph.nodes['text_overlay'], 'text', x=self.graph.view.width / 2, y=self.graph.view.height / 2, class_='no_data') no_data.text = self.graph.no_data_text def render(self, is_unicode=False, pretty_print=False): """Last thing to do before rendering""" for f in self.graph.xml_filters: self.root = f(self.root) args = { 'encoding': 'utf-8' } svg = b'' if etree.lxml: args['pretty_print'] = pretty_print if not self.graph.disable_xml_declaration: svg = b"\n" if not self.graph.disable_xml_declaration: svg += b'\n'.join( [etree.tostring( pi, **args) for pi in self.processing_instructions] ) svg += etree.tostring( self.root, **args) if self.graph.disable_xml_declaration or is_unicode: svg = svg.decode('utf-8') return svg def get_strokes(self): """Return a css snippet containing all stroke style options""" def stroke_dict_to_css(stroke, i=None): """Return a css style for the given option""" css = ['%s.series%s {\n' % ( self.id, '.serie-%d' % i if i is not None else '')] for key in ( 'width', 'linejoin', 'linecap', 'dasharray', 'dashoffset'): if stroke.get(key): css.append(' stroke-%s: %s;\n' % ( key, stroke[key])) css.append('}') return '\n'.join(css) css = [] if self.graph.stroke_style is not None: css.append(stroke_dict_to_css(self.graph.stroke_style)) for serie in self.graph.series: if serie.stroke_style is not None: css.append(stroke_dict_to_css(serie.stroke_style, serie.index)) for secondary_serie in self.graph.secondary_series: if secondary_serie.stroke_style is not None: css.append(stroke_dict_to_css(secondary_serie.stroke_style, secondary_serie.index)) return '\n'.join(css) pygal-2.4.0/pygal/__init__.py0000644000175000017500000000700613114030245016032 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Main pygal package. This package holds all available charts in pygal, the Config class and the maps extensions namespace module. """ from .__about__ import * # noqa: F401,F403 import pkg_resources import sys import traceback import warnings from pygal.graph.bar import Bar from pygal.graph.box import Box from pygal.graph.dot import Dot from pygal.graph.funnel import Funnel from pygal.graph.gauge import Gauge from pygal.graph.solidgauge import SolidGauge from pygal.graph.histogram import Histogram from pygal.graph.horizontalbar import HorizontalBar from pygal.graph.horizontalstackedbar import HorizontalStackedBar from pygal.graph.line import Line from pygal.graph.horizontalline import HorizontalLine from pygal.graph.horizontalstackedline import HorizontalStackedLine from pygal.graph.pie import Pie from pygal.graph.pyramid import Pyramid, VerticalPyramid from pygal.graph.radar import Radar from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedline import StackedLine from pygal.graph.time import DateLine, DateTimeLine, TimeLine, TimeDeltaLine from pygal.graph.treemap import Treemap from pygal.graph.xy import XY from pygal.graph.graph import Graph from pygal.config import Config from pygal import maps CHARTS_BY_NAME = dict( [(k, v) for k, v in locals().items() if isinstance(v, type) and issubclass(v, Graph) and v != Graph]) from pygal.graph.map import BaseMap for entry in pkg_resources.iter_entry_points('pygal.maps'): try: module = entry.load() except Exception: warnings.warn('Unable to load %s pygal plugin \n\n%s' % ( entry, traceback.format_exc()), Warning) continue setattr(maps, entry.name, module) for k, v in module.__dict__.items(): if isinstance(v, type) and issubclass(v, BaseMap) and v != BaseMap: CHARTS_BY_NAME[entry.name.capitalize() + k + 'Map'] = v CHARTS_NAMES = list(CHARTS_BY_NAME.keys()) CHARTS = list(CHARTS_BY_NAME.values()) class PluginImportFixer(object): """ Allow external map plugins to be imported from pygal.maps package. It is a ``sys.meta_path`` loader. """ def find_module(self, fullname, path=None): """ Tell if the module to load can be loaded by the load_module function, ie: if it is a ``pygal.maps.*`` module. """ if fullname.startswith('pygal.maps.') and hasattr( maps, fullname.split('.')[2]): return self return None def load_module(self, name): """ Load the ``pygal.maps.name`` module from the previously loaded plugin """ if name not in sys.modules: sys.modules[name] = getattr(maps, name.split('.')[2]) return sys.modules[name] sys.meta_path += [PluginImportFixer()] pygal-2.4.0/pygal/graph/0000755000175000017500000000000013127127737015040 5ustar zerozero00000000000000pygal-2.4.0/pygal/graph/gauge.py0000644000175000017500000001346213114033322016466 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Gauge chart representing values as needles on a polar scale""" from __future__ import division from pygal._compat import is_str from pygal.graph.graph import Graph from pygal.util import alter, compute_scale, cut, decorate from pygal.view import PolarThetaLogView, PolarThetaView class Gauge(Graph): """Gauge graph class""" needle_width = 1 / 20 def _set_view(self): """Assign a view to current graph""" if self.logarithmic: view_class = PolarThetaLogView else: view_class = PolarThetaView self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) def needle(self, serie): """Draw a needle for each value""" serie_node = self.svg.serie(serie) for i, theta in enumerate(serie.values): if theta is None: continue def point(x, y): return '%f %f' % self.view((x, y)) val = self._format(serie, i) metadata = serie.metadata.get(i) gauges = decorate( self.svg, self.svg.node(serie_node['plot'], class_="dots"), metadata) tolerance = 1.15 if theta < self._min: theta = self._min * tolerance if theta > self._max: theta = self._max * tolerance w = (self._box._tmax - self._box._tmin + self.view.aperture) / 4 if self.logarithmic: w = min(w, self._min - self._min * 10 ** -10) alter( self.svg.node( gauges, 'path', d='M %s L %s A %s 1 0 1 %s Z' % ( point(.85, theta), point(self.needle_width, theta - w), '%f %f' % (self.needle_width, self.needle_width), point(self.needle_width, theta + w), ), class_='line reactive tooltip-trigger'), metadata) x, y = self.view((.75, theta)) self._tooltip_data( gauges, val, x, y, xlabel=self._get_x_label(i)) self._static_value(serie_node, val, x, y, metadata) def _y_axis(self, draw_axes=True): """Override y axis to plot a polar axis""" axis = self.svg.node(self.nodes['plot'], class_="axis x gauge") for i, (label, theta) in enumerate(self._y_labels): guides = self.svg.node(axis, class_='guides') self.svg.line( guides, [self.view((.95, theta)), self.view((1, theta))], close=True, class_='line') self.svg.line( guides, [self.view((0, theta)), self.view((.95, theta))], close=True, class_='guide line %s' % ( 'major' if i in (0, len(self._y_labels) - 1) else '')) x, y = self.view((.9, theta)) self.svg.node( guides, 'text', x=x, y=y ).text = label self.svg.node( guides, 'title', ).text = self._y_format(theta) def _x_axis(self, draw_axes=True): """Override x axis to put a center circle in center""" axis = self.svg.node(self.nodes['plot'], class_="axis y gauge") x, y = self.view((0, 0)) self.svg.node(axis, 'circle', cx=x, cy=y, r=4) def _compute(self): """Compute y min and max and y scale and set labels""" self.min_ = self._min or 0 self.max_ = self._max or 0 if self.max_ - self.min_ == 0: self.min_ -= 1 self.max_ += 1 self._box.set_polar_box( 0, 1, self.min_, self.max_) def _compute_x_labels(self): pass def _compute_y_labels(self): y_pos = compute_scale( self.min_, self.max_, self.logarithmic, self.order_min, self.min_scale, self.max_scale ) if self.y_labels: self._y_labels = [] for i, y_label in enumerate(self.y_labels): if isinstance(y_label, dict): pos = self._adapt(y_label.get('value')) title = y_label.get('label', self._y_format(pos)) elif is_str(y_label): pos = self._adapt(y_pos[i]) title = y_label else: pos = self._adapt(y_label) title = self._y_format(pos) self._y_labels.append((title, pos)) self.min_ = min(self.min_, min(cut(self._y_labels, 1))) self.max_ = max(self.max_, max(cut(self._y_labels, 1))) self._box.set_polar_box( 0, 1, self.min_, self.max_) else: self._y_labels = list(zip(map(self._y_format, y_pos), y_pos)) def _plot(self): """Plot all needles""" for serie in self.series: self.needle(serie) pygal-2.4.0/pygal/graph/horizontalbar.py0000644000175000017500000000223513114033322020250 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Horizontal bar graph""" from pygal.graph.bar import Bar from pygal.graph.horizontal import HorizontalGraph class HorizontalBar(HorizontalGraph, Bar): """Horizontal Bar graph""" def _plot(self): """Draw the bars in reverse order""" for serie in self.series[::-1]: self.bar(serie) for serie in self.secondary_series[::-1]: self.bar(serie, True) pygal-2.4.0/pygal/graph/histogram.py0000644000175000017500000001035613114033322017372 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Histogram chart: like a bar chart but with data plotted along a x axis as bars of varying width. """ from __future__ import division from pygal.graph.bar import Bar from pygal.graph.dual import Dual from pygal.util import alter, cached_property, decorate class Histogram(Dual, Bar): """Histogram chart class""" _series_margin = 0 @cached_property def _values(self): """Getter for secondary series values (flattened)""" return self.yvals @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [val[0] for serie in self.secondary_series for val in serie.values if val[0] is not None] @cached_property def xvals(self): """All x values""" return [val for serie in self.all_series for dval in serie.values for val in dval[1:3] if val is not None] @cached_property def yvals(self): """All y values""" return [val[0] for serie in self.series for val in serie.values if val[0] is not None] def _bar(self, serie, parent, x0, x1, y, i, zero, secondary=False): """Internal bar drawing function""" x, y = self.view((x0, y)) x1, _ = self.view((x1, y)) width = x1 - x height = self.view.y(zero) - y series_margin = width * self._series_margin x += series_margin width -= 2 * series_margin r = serie.rounded_bars * 1 if serie.rounded_bars else 0 alter(self.svg.transposable_node( parent, 'rect', x=x, y=y, rx=r, ry=r, width=width, height=height, class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) return x, y, width, height def bar(self, serie, rescale=False): """Draw a bar graph for a serie""" serie_node = self.svg.serie(serie) bars = self.svg.node(serie_node['plot'], class_="histbars") points = serie.points for i, (y, x0, x1) in enumerate(points): if None in (x0, x1, y) or (self.logarithmic and y <= 0): continue metadata = serie.metadata.get(i) bar = decorate( self.svg, self.svg.node(bars, class_='histbar'), metadata) val = self._format(serie, i) bounds = self._bar( serie, bar, x0, x1, y, i, self.zero, secondary=rescale) self._tooltip_and_print_values( serie_node, serie, bar, i, val, metadata, *bounds) def _compute(self): """Compute x/y min and max and x/y scale and set labels""" if self.xvals: xmin = min(self.xvals) xmax = max(self.xvals) xrng = (xmax - xmin) else: xrng = None if self.yvals: ymin = min(min(self.yvals), self.zero) ymax = max(max(self.yvals), self.zero) yrng = (ymax - ymin) else: yrng = None for serie in self.all_series: serie.points = serie.values if xrng: self._box.xmin, self._box.xmax = xmin, xmax if yrng: self._box.ymin, self._box.ymax = ymin, ymax if self.range and self.range[0] is not None: self._box.ymin = self.range[0] if self.range and self.range[1] is not None: self._box.ymax = self.range[1] pygal-2.4.0/pygal/graph/base.py0000644000175000017500000002073113114033322016305 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Base for pygal charts""" from __future__ import division import os from functools import reduce from uuid import uuid4 from pygal._compat import is_list_like from pygal.adapters import decimal_to_float, not_zero, positive from pygal.config import Config, SerieConfig from pygal.serie import Serie from pygal.state import State from pygal.svg import Svg from pygal.util import compose, ident from pygal.view import Box, Margin class BaseGraph(object): """Chart internal behaviour related functions""" _adapters = [] def __init__(self, config=None, **kwargs): """Config preparation and various initialization""" if config: if isinstance(config, type): config = config() else: config = config.copy() else: config = Config() config(**kwargs) self.config = config self.state = None self.uuid = str(uuid4()) self.raw_series = [] self.xml_filters = [] def __setattr__(self, name, value): """Set an attribute on the class or in the state if there is one""" if name.startswith('__') or getattr(self, 'state', None) is None: super(BaseGraph, self).__setattr__(name, value) else: setattr(self.state, name, value) def __getattribute__(self, name): """Get an attribute from the class or from the state if there is one""" if name.startswith('__') or name == 'state' or getattr( self, 'state', None ) is None or name not in self.state.__dict__: return super(BaseGraph, self).__getattribute__(name) return getattr(self.state, name) def prepare_values(self, raw, offset=0): """Prepare the values to start with sane values""" from pygal.graph.map import BaseMap from pygal import Histogram if self.zero == 0 and isinstance(self, BaseMap): self.zero = 1 if self.x_label_rotation: self.x_label_rotation %= 360 if self.y_label_rotation: self.y_label_rotation %= 360 for key in ('x_labels', 'y_labels'): if getattr(self, key): setattr(self, key, list(getattr(self, key))) if not raw: return adapters = list(self._adapters) or [lambda x:x] if self.logarithmic: for fun in not_zero, positive: if fun in adapters: adapters.remove(fun) adapters = adapters + [positive, not_zero] adapters = adapters + [decimal_to_float] self._adapt = reduce(compose, adapters) if not self.strict else ident self._x_adapt = reduce( compose, self._x_adapters) if not self.strict and getattr( self, '_x_adapters', None) else ident series = [] raw = [( list(raw_values) if not isinstance( raw_values, dict) else raw_values, serie_config_kwargs ) for raw_values, serie_config_kwargs in raw] width = max([len(values) for values, _ in raw] + [len(self.x_labels or [])]) for raw_values, serie_config_kwargs in raw: metadata = {} values = [] if isinstance(raw_values, dict): if isinstance(self, BaseMap): raw_values = list(raw_values.items()) else: value_list = [None] * width for k, v in raw_values.items(): if k in (self.x_labels or []): value_list[self.x_labels.index(k)] = v raw_values = value_list for index, raw_value in enumerate( raw_values + ( (width - len(raw_values)) * [None] # aligning values if len(raw_values) < width else [])): if isinstance(raw_value, dict): raw_value = dict(raw_value) value = raw_value.pop('value', None) metadata[index] = raw_value else: value = raw_value # Fix this by doing this in charts class methods if isinstance(self, Histogram): if value is None: value = (None, None, None) elif not is_list_like(value): value = (value, self.zero, self.zero) elif len(value) == 2: value = (1, value[0], value[1]) value = list(map(self._adapt, value)) elif self._dual: if value is None: value = (None, None) elif not is_list_like(value): value = (value, self.zero) if self._x_adapt: value = ( self._x_adapt(value[0]), self._adapt(value[1])) if isinstance(self, BaseMap): value = (self._adapt(value[0]), value[1]) else: value = list(map(self._adapt, value)) else: value = self._adapt(value) values.append(value) serie_config = SerieConfig() serie_config(**dict((k, v) for k, v in self.state.__dict__.items() if k in dir(serie_config))) serie_config(**serie_config_kwargs) series.append( Serie(offset + len(series), values, serie_config, metadata)) return series def setup(self, **kwargs): """Set up the transient state prior rendering""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(self.x_labels) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) if isinstance(self.style, type): self.style = self.style() self.series = self.prepare_values( [rs for rs in self.raw_series if not rs[1].get('secondary')]) or [] self.secondary_series = self.prepare_values( [rs for rs in self.raw_series if rs[1].get('secondary')], len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list(filter( lambda x: x > 0, [val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render() def teardown(self): """Remove the transient state after rendering""" if os.getenv('PYGAL_KEEP_STATE'): return del self.state self.state = None def _repr_svg_(self): """Display svg in IPython notebook""" return self.render(disable_xml_declaration=True) def _repr_png_(self): """Display png in IPython notebook""" return self.render_to_png() pygal-2.4.0/pygal/graph/box.py0000644000175000017500000002500213114033322016157 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Box plot: a convenient way to display series as box with whiskers and outliers Different types are available throught the box_mode option """ from __future__ import division from bisect import bisect_left, bisect_right from pygal.graph.graph import Graph from pygal.util import alter, decorate class Box(Graph): """ Box plot For each series, shows the median value, the 25th and 75th percentiles, and the values within 1.5 times the interquartile range of the 25th and 75th percentiles. See http://en.wikipedia.org/wiki/Box_plot """ _series_margin = .06 def _value_format(self, value, serie): """ Format value for dual value display. """ if self.box_mode == "extremes": return ( 'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' % tuple( map(self._y_format, serie.points[1:6]))) elif self.box_mode in ["tukey", "stdev", "pstdev"]: return ( 'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n' 'Upper Whisker: %s\nMax: %s' % tuple(map( self._y_format, serie.points))) elif self.box_mode == '1.5IQR': # 1.5IQR mode return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map( self._y_format, serie.points[2:5])) else: return self._y_format(serie.points) def _compute(self): """ Compute parameters necessary for later steps within the rendering process """ for serie in self.series: serie.points, serie.outliers = \ self._box_points(serie.values, self.box_mode) self._x_pos = [ (i + .5) / self._order for i in range(self._order)] if self._min: self._box.ymin = min(self._min, self.zero) if self._max: self._box.ymax = max(self._max, self.zero) def _plot(self): """Plot the series data""" for serie in self.series: self._boxf(serie) @property def _len(self): """Len is always 7 here""" return 7 def _boxf(self, serie): """For a specific series, draw the box plot.""" serie_node = self.svg.serie(serie) # Note: q0 and q4 do not literally mean the zero-th quartile # and the fourth quartile, but rather the distance from 1.5 times # the inter-quartile range to Q1 and Q3, respectively. boxes = self.svg.node(serie_node['plot'], class_="boxes") metadata = serie.metadata.get(0) box = decorate( self.svg, self.svg.node(boxes, class_='box'), metadata) val = self._format(serie, 0) x_center, y_center = self._draw_box( box, serie.points[1:6], serie.outliers, serie.index, metadata) self._tooltip_data(box, val, x_center, y_center, "centered", self._get_x_label(serie.index)) self._static_value(serie_node, val, x_center, y_center, metadata) def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata): """ Return the center of a bounding box defined by a box plot. Draws a box plot on self.svg. """ width = (self.view.x(1) - self.view.x(0)) / self._order series_margin = width * self._series_margin left_edge = self.view.x(0) + width * box_index + series_margin width -= 2 * series_margin # draw lines for whiskers - bottom, median, and top for i, whisker in enumerate( (quartiles[0], quartiles[2], quartiles[4])): whisker_width = width if i == 1 else width / 2 shift = (width - whisker_width) / 2 xs = left_edge + shift xe = left_edge + width - shift alter(self.svg.line( parent_node, coords=[(xs, self.view.y(whisker)), (xe, self.view.y(whisker))], class_='reactive tooltip-trigger', attrib={'stroke-width': 3}), metadata) # draw lines connecting whiskers to box (Q1 and Q3) alter(self.svg.line( parent_node, coords=[(left_edge + width / 2, self.view.y(quartiles[0])), (left_edge + width / 2, self.view.y(quartiles[1]))], class_='reactive tooltip-trigger', attrib={'stroke-width': 2}), metadata) alter(self.svg.line( parent_node, coords=[(left_edge + width / 2, self.view.y(quartiles[4])), (left_edge + width / 2, self.view.y(quartiles[3]))], class_='reactive tooltip-trigger', attrib={'stroke-width': 2}), metadata) # box, bounded by Q1 and Q3 alter(self.svg.node( parent_node, tag='rect', x=left_edge, y=self.view.y(quartiles[1]), height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]), width=width, class_='subtle-fill reactive tooltip-trigger'), metadata) # draw outliers for o in outliers: alter(self.svg.node( parent_node, tag='circle', cx=left_edge + width / 2, cy=self.view.y(o), r=3, class_='subtle-fill reactive tooltip-trigger'), metadata) return (left_edge + width / 2, self.view.y( sum(quartiles) / len(quartiles))) @staticmethod def _box_points(values, mode='extremes'): """ Default mode: (mode='extremes' or unset) Return a 7-tuple of 2x minimum, Q1, Median, Q3, and 2x maximum for a list of numeric values. 1.5IQR mode: (mode='1.5IQR') Return a 7-tuple of min, Q1 - 1.5 * IQR, Q1, Median, Q3, Q3 + 1.5 * IQR and max for a list of numeric values. Tukey mode: (mode='tukey') Return a 7-tuple of min, q[0..4], max and a list of outliers Outliers are considered values x: x < q1 - IQR or x > q3 + IQR SD mode: (mode='stdev') Return a 7-tuple of min, q[0..4], max and a list of outliers Outliers are considered values x: x < q2 - SD or x > q2 + SD SDp mode: (mode='pstdev') Return a 7-tuple of min, q[0..4], max and a list of outliers Outliers are considered values x: x < q2 - SDp or x > q2 + SDp The iterator values may include None values. Uses quartile definition from Mendenhall, W. and Sincich, T. L. Statistics for Engineering and the Sciences, 4th ed. Prentice-Hall, 1995. """ def median(seq): n = len(seq) if n % 2 == 0: # seq has an even length return (seq[n // 2] + seq[n // 2 - 1]) / 2 else: # seq has an odd length return seq[n // 2] def mean(seq): return sum(seq) / len(seq) def stdev(seq): m = mean(seq) l = len(seq) v = sum((n - m)**2 for n in seq) / (l - 1) # variance return v**0.5 # sqrt def pstdev(seq): m = mean(seq) l = len(seq) v = sum((n - m)**2 for n in seq) / l # variance return v**0.5 # sqrt outliers = [] # sort the copy in case the originals must stay in original order s = sorted([x for x in values if x is not None]) n = len(s) if not n: return (0, 0, 0, 0, 0, 0, 0), [] elif n == 1: return (s[0], s[0], s[0], s[0], s[0], s[0], s[0]), [] else: q2 = median(s) # See 'Method 3' in http://en.wikipedia.org/wiki/Quartile if n % 2 == 0: # even q1 = median(s[:n // 2]) q3 = median(s[n // 2:]) else: # odd if n == 1: # special case q1 = s[0] q3 = s[0] elif n % 4 == 1: # n is of form 4n + 1 where n >= 1 m = (n - 1) // 4 q1 = 0.25 * s[m - 1] + 0.75 * s[m] q3 = 0.75 * s[3 * m] + 0.25 * s[3 * m + 1] else: # n is of form 4n + 3 where n >= 1 m = (n - 3) // 4 q1 = 0.75 * s[m] + 0.25 * s[m + 1] q3 = 0.25 * s[3 * m + 1] + 0.75 * s[3 * m + 2] iqr = q3 - q1 min_s = s[0] max_s = s[-1] if mode == 'extremes': q0 = min_s q4 = max_s elif mode == 'tukey': # the lowest datum still within 1.5 IQR of the lower quartile, # and the highest datum still within 1.5 IQR of the upper # quartile [Tukey box plot, Wikipedia ] b0 = bisect_left(s, q1 - 1.5 * iqr) b4 = bisect_right(s, q3 + 1.5 * iqr) q0 = s[b0] q4 = s[b4 - 1] outliers = s[:b0] + s[b4:] elif mode == 'stdev': # one standard deviation above and below the mean of the data sd = stdev(s) b0 = bisect_left(s, q2 - sd) b4 = bisect_right(s, q2 + sd) q0 = s[b0] q4 = s[b4 - 1] outliers = s[:b0] + s[b4:] elif mode == 'pstdev': # one population standard deviation above and below # the mean of the data sdp = pstdev(s) b0 = bisect_left(s, q2 - sdp) b4 = bisect_right(s, q2 + sdp) q0 = s[b0] q4 = s[b4 - 1] outliers = s[:b0] + s[b4:] elif mode == '1.5IQR': # 1.5IQR mode q0 = q1 - 1.5 * iqr q4 = q3 + 1.5 * iqr return (min_s, q0, q1, q2, q3, q4, max_s), outliers pygal-2.4.0/pygal/graph/bar.py0000644000175000017500000001220113114033322016130 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Bar chart that presents grouped data with rectangular bars with lengths proportional to the values that they represent. """ from __future__ import division from pygal.graph.graph import Graph from pygal.util import alter, decorate, ident, swap class Bar(Graph): """Bar graph class""" _series_margin = .06 _serie_margin = .06 def _bar(self, serie, parent, x, y, i, zero, secondary=False): """Internal bar drawing function""" width = (self.view.x(1) - self.view.x(0)) / self._len x, y = self.view((x, y)) series_margin = width * self._series_margin x += series_margin width -= 2 * series_margin width /= self._order if self.horizontal: serie_index = self._order - serie.index - 1 else: serie_index = serie.index x += serie_index * width serie_margin = width * self._serie_margin x += serie_margin width -= 2 * serie_margin height = self.view.y(zero) - y r = serie.rounded_bars * 1 if serie.rounded_bars else 0 alter(self.svg.transposable_node( parent, 'rect', x=x, y=y, rx=r, ry=r, width=width, height=height, class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) return x, y, width, height def _tooltip_and_print_values( self, serie_node, serie, parent, i, val, metadata, x, y, width, height): transpose = swap if self.horizontal else ident x_center, y_center = transpose((x + width / 2, y + height / 2)) x_top, y_top = transpose((x + width, y + height)) x_bottom, y_bottom = transpose((x, y)) if self._dual: v = serie.values[i][0] else: v = serie.values[i] sign = -1 if v < self.zero else 1 self._tooltip_data( parent, val, x_center, y_center, "centered", self._get_x_label(i)) if self.print_values_position == 'top': if self.horizontal: x = x_bottom + sign * self.style.value_font_size / 2 y = y_center else: x = x_center y = y_bottom - sign * self.style.value_font_size / 2 elif self.print_values_position == 'bottom': if self.horizontal: x = x_top + sign * self.style.value_font_size / 2 y = y_center else: x = x_center y = y_top - sign * self.style.value_font_size / 2 else: x = x_center y = y_center self._static_value(serie_node, val, x, y, metadata, "middle") def bar(self, serie, rescale=False): """Draw a bar graph for a serie""" serie_node = self.svg.serie(serie) bars = self.svg.node(serie_node['plot'], class_="bars") if rescale and self.secondary_series: points = self._rescale(serie.points) else: points = serie.points for i, (x, y) in enumerate(points): if None in (x, y) or (self.logarithmic and y <= 0): continue metadata = serie.metadata.get(i) val = self._format(serie, i) bar = decorate( self.svg, self.svg.node(bars, class_='bar'), metadata) x_, y_, width, height = self._bar( serie, bar, x, y, i, self.zero, secondary=rescale) self._confidence_interval( serie_node['overlay'], x_ + width / 2, y_, serie.values[i], metadata) self._tooltip_and_print_values( serie_node, serie, bar, i, val, metadata, x_, y_, width, height) def _compute(self): """Compute y min and max and y scale and set labels""" if self._min: self._box.ymin = min(self._min, self.zero) if self._max: self._box.ymax = max(self._max, self.zero) self._x_pos = [ x / self._len for x in range(self._len + 1) ] if self._len > 1 else [0, 1] # Center if only one value self._points(self._x_pos) self._x_pos = [(i + .5) / self._len for i in range(self._len)] def _plot(self): """Draw bars for series and secondary series""" for serie in self.series: self.bar(serie) for serie in self.secondary_series: self.bar(serie, True) pygal-2.4.0/pygal/graph/treemap.py0000644000175000017500000001045313114033322017030 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Treemap chart: Visualize data using nested recangles""" from __future__ import division from pygal.adapters import none_to_zero, positive from pygal.graph.graph import Graph from pygal.util import alter, cut, decorate class Treemap(Graph): """Treemap graph class""" _adapters = [positive, none_to_zero] def _rect(self, serie, serie_node, rects, val, x, y, w, h, i): rx, ry = self.view((x, y)) rw, rh = self.view((x + w, y + h)) rw -= rx rh -= ry metadata = serie.metadata.get(i) val = self._format(serie, i) rect = decorate( self.svg, self.svg.node(rects, class_="rect"), metadata) alter( self.svg.node( rect, 'rect', x=rx, y=ry, width=rw, height=rh, class_='rect reactive tooltip-trigger'), metadata) self._tooltip_data( rect, val, rx + rw / 2, ry + rh / 2, 'centered', self._get_x_label(i)) self._static_value( serie_node, val, rx + rw / 2, ry + rh / 2, metadata) def _binary_tree(self, data, total, x, y, w, h, parent=None): if total == 0: return if len(data) == 1: if parent: i, datum = data[0] serie, serie_node, rects = parent self._rect(serie, serie_node, rects, datum, x, y, w, h, i) else: datum = data[0] serie_node = self.svg.serie(datum) self._binary_tree( list(enumerate(datum.values)), total, x, y, w, h, (datum, serie_node, self.svg.node(serie_node['plot'], class_="rects"))) return midpoint = total / 2 pivot_index = 1 running_sum = 0 for i, elt in enumerate(data): if running_sum >= midpoint: pivot_index = i break running_sum += elt[1] if parent else sum(elt.values) half1 = data[:pivot_index] half2 = data[pivot_index:] if parent: half1_sum = sum(cut(half1, 1)) half2_sum = sum(cut(half2, 1)) else: half1_sum = sum(map(sum, map(lambda x: x.values, half1))) half2_sum = sum(map(sum, map(lambda x: x.values, half2))) pivot_pct = half1_sum / total if h > w: y_pivot = pivot_pct * h self._binary_tree( half1, half1_sum, x, y, w, y_pivot, parent) self._binary_tree( half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent) else: x_pivot = pivot_pct * w self._binary_tree( half1, half1_sum, x, y, x_pivot, h, parent) self._binary_tree( half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent) def _compute_x_labels(self): pass def _compute_y_labels(self): pass def _plot(self): total = sum(map(sum, map(lambda x: x.values, self.series))) if total == 0: return gw = self.width - self.margin_box.x gh = self.height - self.margin_box.y self.view.box.xmin = self.view.box.ymin = x = y = 0 self.view.box.xmax = w = (total * gw / gh) ** .5 self.view.box.ymax = h = total / w self.view.box.fix() self._binary_tree(self.series, total, x, y, w, h) pygal-2.4.0/pygal/graph/funnel.py0000644000175000017500000001001413114033322016653 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Funnel chart: Represent values as a funnel""" from __future__ import division from pygal.adapters import none_to_zero, positive from pygal.graph.graph import Graph from pygal.util import alter, cut, decorate class Funnel(Graph): """Funnel graph class""" _adapters = [positive, none_to_zero] def _value_format(self, value): """Format value for dual value display.""" return super(Funnel, self)._value_format(value and abs(value)) def funnel(self, serie): """Draw a funnel slice""" serie_node = self.svg.serie(serie) fmt = lambda x: '%f %f' % x for i, poly in enumerate(serie.points): metadata = serie.metadata.get(i) val = self._format(serie, i) funnels = decorate( self.svg, self.svg.node(serie_node['plot'], class_="funnels"), metadata) alter(self.svg.node( funnels, 'polygon', points=' '.join(map(fmt, map(self.view, poly))), class_='funnel reactive tooltip-trigger'), metadata) # Poly center from label x, y = self.view(( self._center(self._x_pos[serie.index]), sum([point[1] for point in poly]) / len(poly))) self._tooltip_data( funnels, val, x, y, 'centered', self._get_x_label(serie.index)) self._static_value(serie_node, val, x, y, metadata) def _center(self, x): return x - 1 / (2 * self._order) def _compute(self): """Compute y min and max and y scale and set labels""" self._x_pos = [ (x + 1) / self._order for x in range(self._order) ] if self._order != 1 else [.5] # Center if only one value previous = [[self.zero, self.zero] for i in range(self._len)] for i, serie in enumerate(self.series): y_height = - sum(serie.safe_values) / 2 all_x_pos = [0] + self._x_pos serie.points = [] for j, value in enumerate(serie.values): poly = [] poly.append((all_x_pos[i], previous[j][0])) poly.append((all_x_pos[i], previous[j][1])) previous[j][0] = y_height y_height = previous[j][1] = y_height + value poly.append((all_x_pos[i + 1], previous[j][1])) poly.append((all_x_pos[i + 1], previous[j][0])) serie.points.append(poly) val_max = max(list(map(sum, cut(self.series, 'values'))) + [self.zero]) self._box.ymin = -val_max self._box.ymax = val_max if self.range and self.range[0] is not None: self._box.ymin = self.range[0] if self.range and self.range[1] is not None: self._box.ymax = self.range[1] def _compute_x_labels(self): self._x_labels = list( zip(self.x_labels and map(self._x_format, self.x_labels) or [ serie.title['title'] if isinstance(serie.title, dict) else serie.title or '' for serie in self.series], map(self._center, self._x_pos))) def _plot(self): """Plot the funnel""" for serie in self.series: self.funnel(serie) pygal-2.4.0/pygal/graph/horizontal.py0000644000175000017500000000456113127125562017603 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Horizontal graph mixin""" from pygal.graph.graph import Graph from pygal.view import HorizontalLogView, HorizontalView class HorizontalGraph(Graph): """Horizontal graph mixin""" def __init__(self, *args, **kwargs): """Set the horizontal flag to True""" self.horizontal = True super(HorizontalGraph, self).__init__(*args, **kwargs) def _post_compute(self): """After computations transpose labels""" self._x_labels, self._y_labels = self._y_labels, self._x_labels self._x_labels_major, self._y_labels_major = ( self._y_labels_major, self._x_labels_major) self._x_2nd_labels, self._y_2nd_labels = ( self._y_2nd_labels, self._x_2nd_labels) self.show_y_guides, self.show_x_guides = ( self.show_x_guides, self.show_y_guides) def _axes(self): """Set the _force_vertical flag when rendering axes""" self.view._force_vertical = True super(HorizontalGraph, self)._axes() self.view._force_vertical = False def _set_view(self): """Assign a horizontal view to current graph""" if self.logarithmic: view_class = HorizontalLogView else: view_class = HorizontalView self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) def _get_x_label(self, i): """Convenience function to get the x_label of a value index""" if not self.x_labels or not self._y_labels or len(self._y_labels) <= i: return return self._y_labels[i][0] pygal-2.4.0/pygal/graph/pie.py0000644000175000017500000000674013114033322016154 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Pie chart: A circular chart divided into slice to illustrate proportions It can be made as a donut or a half pie. """ from __future__ import division from math import pi from pygal.adapters import none_to_zero, positive from pygal.graph.graph import Graph from pygal.util import alter, decorate class Pie(Graph): """Pie graph class""" _adapters = [positive, none_to_zero] def slice(self, serie, start_angle, total): """Make a serie slice""" serie_node = self.svg.serie(serie) dual = self._len > 1 and not self._order == 1 slices = self.svg.node(serie_node['plot'], class_="slices") serie_angle = 0 original_start_angle = start_angle if self.half_pie: center = ((self.width - self.margin_box.x) / 2., (self.height - self.margin_box.y) / 1.25) else: center = ((self.width - self.margin_box.x) / 2., (self.height - self.margin_box.y) / 2.) radius = min(center) for i, val in enumerate(serie.values): perc = val / total if self.half_pie: angle = 2 * pi * perc / 2 else: angle = 2 * pi * perc serie_angle += angle val = self._format(serie, i) metadata = serie.metadata.get(i) slice_ = decorate( self.svg, self.svg.node(slices, class_="slice"), metadata) if dual: small_radius = radius * .9 big_radius = radius else: big_radius = radius * .9 small_radius = radius * serie.inner_radius alter(self.svg.slice( serie_node, slice_, big_radius, small_radius, angle, start_angle, center, val, i, metadata), metadata) start_angle += angle if dual: val = self._serie_format(serie, sum(serie.values)) self.svg.slice(serie_node, self.svg.node(slices, class_="big_slice"), radius * .9, 0, serie_angle, original_start_angle, center, val, i, metadata) return serie_angle def _compute_x_labels(self): pass def _compute_y_labels(self): pass def _plot(self): """Draw all the serie slices""" total = sum(map(sum, map(lambda x: x.values, self.series))) if total == 0: return if self.half_pie: current_angle = 3 * pi / 2 else: current_angle = 0 for index, serie in enumerate(self.series): angle = self.slice(serie, current_angle, total) current_angle += angle pygal-2.4.0/pygal/graph/line.py0000644000175000017500000001730613127125562016342 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Line chart: Display series of data as markers (dots) connected by straight segments """ from __future__ import division from pygal.graph.graph import Graph from pygal.util import alter, cached_property, decorate class Line(Graph): """Line graph class""" def __init__(self, *args, **kwargs): """Set _self_close as False, it's True for Radar like Line""" self._self_close = False super(Line, self).__init__(*args, **kwargs) @cached_property def _values(self): """Getter for series values (flattened)""" return [ val[1] for serie in self.series for val in (serie.interpolated if self.interpolate else serie.points) if val[1] is not None and (not self.logarithmic or val[1] > 0)] @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [ val[1] for serie in self.secondary_series for val in (serie.interpolated if self.interpolate else serie.points) if val[1] is not None and (not self.logarithmic or val[1] > 0)] def _fill(self, values): """Add extra values to fill the line""" zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax)) # Check to see if the data has been padded with "none's" # Fill doesn't work correctly otherwise end = len(values) - 1 while end > 0: x, y = values[end] if self.missing_value_fill_truncation == "either": if x is not None and y is not None: break elif self.missing_value_fill_truncation == "x": if x is not None: break elif self.missing_value_fill_truncation == "y": if y is not None: break else: raise ValueError( "Invalid value ({}) for config key " "'missing_value_fill_truncation';" " Use 'x', 'y' or 'either'".format( self.missing_value_fill_truncation)) end -= 1 return ([(values[0][0], zero)] + values + [(values[end][0], zero)]) def line(self, serie, rescale=False): """Draw the line serie""" serie_node = self.svg.serie(serie) if rescale and self.secondary_series: points = self._rescale(serie.points) else: points = serie.points view_values = list(map(self.view, points)) if serie.show_dots: for i, (x, y) in enumerate(view_values): if None in (x, y): continue if self.logarithmic: if points[i][1] is None or points[i][1] <= 0: continue if (serie.show_only_major_dots and self.x_labels and i < len(self.x_labels) and self.x_labels[i] not in self._x_labels_major): continue metadata = serie.metadata.get(i) classes = [] if x > self.view.width / 2: classes.append('left') if y > self.view.height / 2: classes.append('top') classes = ' '.join(classes) self._confidence_interval( serie_node['overlay'], x, y, serie.values[i], metadata) dots = decorate( self.svg, self.svg.node(serie_node['overlay'], class_="dots"), metadata) val = self._format(serie, i) alter(self.svg.transposable_node( dots, 'circle', cx=x, cy=y, r=serie.dots_size, class_='dot reactive tooltip-trigger'), metadata) self._tooltip_data( dots, val, x, y, xlabel=self._get_x_label(i)) self._static_value( serie_node, val, x + self.style.value_font_size, y + self.style.value_font_size, metadata) if serie.stroke: if self.interpolate: points = serie.interpolated if rescale and self.secondary_series: points = self._rescale(points) view_values = list(map(self.view, points)) if serie.fill: view_values = self._fill(view_values) if serie.allow_interruptions: # view_values are in form [(x1, y1), (x2, y2)]. We # need to split that into multiple sequences if a # None is present here sequences = [] cur_sequence = [] for x, y in view_values: if y is None and len(cur_sequence) > 0: # emit current subsequence sequences.append(cur_sequence) cur_sequence = [] elif y is None: # just discard continue else: cur_sequence.append((x, y)) # append the element if len(cur_sequence) > 0: # emit last possible sequence sequences.append(cur_sequence) else: # plain vanilla rendering sequences = [view_values] if self.logarithmic: for seq in sequences: for ele in seq[::-1]: y = points[seq.index(ele)][1] if y is None or y <= 0: del seq[seq.index(ele)] for seq in sequences: self.svg.line( serie_node['plot'], seq, close=self._self_close, class_='line reactive' + (' nofill' if not serie.fill else '')) def _compute(self): """Compute y min and max and y scale and set labels""" # X Labels if self.horizontal: self._x_pos = [ x / (self._len - 1) for x in range(self._len) ][::-1] if self._len != 1 else [.5] # Center if only one value else: self._x_pos = [ x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value self._points(self._x_pos) if self.include_x_axis: # Y Label self._box.ymin = min(self._min or 0, 0) self._box.ymax = max(self._max or 0, 0) else: self._box.ymin = self._min self._box.ymax = self._max def _plot(self): """Plot the serie lines and secondary serie lines""" for serie in self.series: self.line(serie) for serie in self.secondary_series: self.line(serie, True) pygal-2.4.0/pygal/graph/stackedbar.py0000644000175000017500000001277013114033322017502 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Stacked Bar chart: Like a bar chart but with all series stacking on top of the others instead of being displayed side by side. """ from __future__ import division from pygal.adapters import none_to_zero from pygal.graph.bar import Bar class StackedBar(Bar): """Stacked Bar graph class""" _adapters = [none_to_zero] def _get_separated_values(self, secondary=False): """Separate values between positives and negatives stacked""" series = self.secondary_series if secondary else self.series transposed = list(zip(*[serie.values for serie in series])) positive_vals = [sum([ val for val in vals if val is not None and val >= self.zero]) for vals in transposed] negative_vals = [sum([ val for val in vals if val is not None and val < self.zero]) for vals in transposed] return positive_vals, negative_vals def _compute_box(self, positive_vals, negative_vals): """Compute Y min and max""" if self.range and self.range[0] is not None: self._box.ymin = self.range[0] else: self._box.ymin = negative_vals and min( min(negative_vals), self.zero) or self.zero if self.range and self.range[1] is not None: self._box.ymax = self.range[1] else: self._box.ymax = positive_vals and max( max(positive_vals), self.zero) or self.zero def _compute(self): """Compute y min and max and y scale and set labels""" positive_vals, negative_vals = self._get_separated_values() if self.logarithmic: positive_vals = list(filter( lambda x: x > self.zero, positive_vals)) negative_vals = list(filter( lambda x: x > self.zero, negative_vals)) self._compute_box(positive_vals, negative_vals) positive_vals = positive_vals or [self.zero] negative_vals = negative_vals or [self.zero] self._x_pos = [ x / self._len for x in range(self._len + 1) ] if self._len > 1 else [0, 1] # Center if only one value self._points(self._x_pos) self.negative_cumulation = [0] * self._len self.positive_cumulation = [0] * self._len if self.secondary_series: positive_vals, negative_vals = self._get_separated_values(True) positive_vals = positive_vals or [self.zero] negative_vals = negative_vals or [self.zero] self.secondary_negative_cumulation = [0] * self._len self.secondary_positive_cumulation = [0] * self._len self._pre_compute_secondary(positive_vals, negative_vals) self._x_pos = [(i + .5) / self._len for i in range(self._len)] def _pre_compute_secondary(self, positive_vals, negative_vals): """Compute secondary y min and max""" self._secondary_min = (negative_vals and min( min(negative_vals), self.zero)) or self.zero self._secondary_max = (positive_vals and max( max(positive_vals), self.zero)) or self.zero def _bar(self, serie, parent, x, y, i, zero, secondary=False): """Internal stacking bar drawing function""" if secondary: cumulation = (self.secondary_negative_cumulation if y < self.zero else self.secondary_positive_cumulation) else: cumulation = (self.negative_cumulation if y < self.zero else self.positive_cumulation) zero = cumulation[i] cumulation[i] = zero + y if zero == 0: zero = self.zero y -= self.zero y += zero width = (self.view.x(1) - self.view.x(0)) / self._len x, y = self.view((x, y)) y = y or 0 series_margin = width * self._series_margin x += series_margin width -= 2 * series_margin if self.secondary_series: width /= 2 x += int(secondary) * width serie_margin = width * self._serie_margin x += serie_margin width -= 2 * serie_margin height = self.view.y(zero) - y r = serie.rounded_bars * 1 if serie.rounded_bars else 0 self.svg.transposable_node( parent, 'rect', x=x, y=y, rx=r, ry=r, width=width, height=height, class_='rect reactive tooltip-trigger') return x, y, width, height def _plot(self): """Draw bars for series and secondary series""" for serie in self.series[::-1 if self.stack_from_top else 1]: self.bar(serie) for serie in self.secondary_series[::-1 if self.stack_from_top else 1]: self.bar(serie, True) pygal-2.4.0/pygal/graph/__init__.py0000644000175000017500000000147012707661226017152 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Graph package containing all builtin charts""" pygal-2.4.0/pygal/graph/horizontalstackedbar.py0000644000175000017500000000175213114033322021612 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Horizontal stacked graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.stackedbar import StackedBar class HorizontalStackedBar(HorizontalGraph, StackedBar): """Horizontal Stacked Bar graph""" pygal-2.4.0/pygal/graph/horizontalline.py0000644000175000017500000000224613114033322020435 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Horizontal line graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.line import Line class HorizontalLine(HorizontalGraph, Line): """Horizontal Line graph""" def _plot(self): """Draw the lines in reverse order""" for serie in self.series[::-1]: self.line(serie) for serie in self.secondary_series[::-1]: self.line(serie, True) pygal-2.4.0/pygal/graph/horizontalstackedline.py0000644000175000017500000000232213114033322021767 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Horizontal Stacked Line graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.stackedline import StackedLine class HorizontalStackedLine(HorizontalGraph, StackedLine): """Horizontal Stacked Line graph""" def _plot(self): """Draw the lines in reverse order""" for serie in self.series[::-1]: self.line(serie) for serie in self.secondary_series[::-1]: self.line(serie, True) pygal-2.4.0/pygal/graph/xy.py0000644000175000017500000000757113114033322016042 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ XY Line graph: Plot a set of couple data points (x, y) connected by straight segments. """ from __future__ import division from functools import reduce from pygal.graph.dual import Dual from pygal.graph.line import Line from pygal.util import cached_property, compose, ident class XY(Line, Dual): """XY Line graph class""" _x_adapters = [] @cached_property def xvals(self): """All x values""" return [val[0] for serie in self.all_series for val in serie.values if val[0] is not None] @cached_property def yvals(self): """All y values""" return [val[1] for serie in self.series for val in serie.values if val[1] is not None] @cached_property def _min(self): """Getter for the minimum series value""" return (self.range[0] if (self.range and self.range[0] is not None) else (min(self.yvals) if self.yvals else None)) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range[1] if (self.range and self.range[1] is not None) else (max(self.yvals) if self.yvals else None)) def _compute(self): """Compute x/y min and max and x/y scale and set labels""" if self.xvals: if self.xrange: x_adapter = reduce( compose, self._x_adapters) if getattr( self, '_x_adapters', None) else ident xmin = x_adapter(self.xrange[0]) xmax = x_adapter(self.xrange[1]) else: xmin = min(self.xvals) xmax = max(self.xvals) xrng = (xmax - xmin) else: xrng = None if self.yvals: ymin = self._min ymax = self._max if self.include_x_axis: ymin = min(ymin or 0, 0) ymax = max(ymax or 0, 0) yrng = (ymax - ymin) else: yrng = None for serie in self.all_series: serie.points = serie.values if self.interpolate: vals = list(zip(*sorted( filter(lambda t: None not in t, serie.points), key=lambda x: x[0]))) serie.interpolated = self._interpolate(vals[0], vals[1]) if self.interpolate: self.xvals = [val[0] for serie in self.all_series for val in serie.interpolated] self.yvals = [val[1] for serie in self.series for val in serie.interpolated] if self.xvals: xmin = min(self.xvals) xmax = max(self.xvals) xrng = (xmax - xmin) else: xrng = None # these values can also be 0 (zero), so testing explicitly for None if xrng is not None: self._box.xmin, self._box.xmax = xmin, xmax if yrng is not None: self._box.ymin, self._box.ymax = ymin, ymax pygal-2.4.0/pygal/graph/pyramid.py0000644000175000017500000000573313114033322017045 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Pyramid chart: Stacked bar chart containing only positive values divided by two axes, generally gender for age pyramid. """ from __future__ import division from pygal.adapters import positive from pygal.graph.horizontal import HorizontalGraph from pygal.graph.stackedbar import StackedBar class VerticalPyramid(StackedBar): """Vertical Pyramid graph class""" _adapters = [positive] def _value_format(self, value): """Format value for dual value display.""" return super(VerticalPyramid, self)._value_format(value and abs(value)) def _get_separated_values(self, secondary=False): """Separate values between odd and even series stacked""" series = self.secondary_series if secondary else self.series positive_vals = map(sum, zip( *[serie.safe_values for index, serie in enumerate(series) if index % 2])) negative_vals = map(sum, zip( *[serie.safe_values for index, serie in enumerate(series) if not index % 2])) return list(positive_vals), list(negative_vals) def _compute_box(self, positive_vals, negative_vals): """Compute Y min and max""" max_ = max( max(positive_vals or [self.zero]), max(negative_vals or [self.zero])) if self.range and self.range[0] is not None: self._box.ymin = self.range[0] else: self._box.ymin = - max_ if self.range and self.range[1] is not None: self._box.ymax = self.range[1] else: self._box.ymax = max_ def _pre_compute_secondary(self, positive_vals, negative_vals): """Compute secondary y min and max""" self._secondary_max = max(max(positive_vals), max(negative_vals)) self._secondary_min = - self._secondary_max def _bar(self, serie, parent, x, y, i, zero, secondary=False): """Internal stacking bar drawing function""" if serie.index % 2: y = -y return super(VerticalPyramid, self)._bar( serie, parent, x, y, i, zero, secondary) class Pyramid(HorizontalGraph, VerticalPyramid): """Horizontal Pyramid graph class like the one used by age pyramid""" pygal-2.4.0/pygal/graph/solidgauge.py0000644000175000017500000001200013127123414017512 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Solid Guage For each series a solid guage is shown on the plot area. """ from __future__ import division from math import pi, sqrt from pygal.graph.graph import Graph from pygal.util import alter, decorate class SolidGauge(Graph): def gaugify(self, serie, squares, sq_dimensions, current_square): serie_node = self.svg.serie(serie) if self.half_pie: start_angle = 3 * pi / 2 center = ( (current_square[1] * sq_dimensions[0]) - ( sq_dimensions[0] / 2.), (current_square[0] * sq_dimensions[1]) - ( sq_dimensions[1] / 4)) end_angle = pi / 2 else: start_angle = 0 center = ( (current_square[1] * sq_dimensions[0]) - ( sq_dimensions[0] / 2.), (current_square[0] * sq_dimensions[1]) - ( sq_dimensions[1] / 2.)) end_angle = 2 * pi max_value = serie.metadata.get(0, {}).get('max_value', 100) radius = min([sq_dimensions[0] / 2, sq_dimensions[1] / 2]) * .9 small_radius = radius * serie.inner_radius self.svg.gauge_background( serie_node, start_angle, center, radius, small_radius, end_angle, self.half_pie, self._serie_format(serie, max_value)) sum_ = 0 for i, value in enumerate(serie.values): if value is None: continue ratio = min(value, max_value) / max_value if self.half_pie: angle = 2 * pi * ratio / 2 else: angle = 2 * pi * ratio val = self._format(serie, i) metadata = serie.metadata.get(i) gauge_ = decorate( self.svg, self.svg.node(serie_node['plot'], class_="gauge"), metadata) alter( self.svg.solid_gauge( serie_node, gauge_, radius, small_radius, angle, start_angle, center, val, i, metadata, self.half_pie, end_angle, self._serie_format(serie, max_value)), metadata) start_angle += angle sum_ += value x, y = center self.svg.node( serie_node['text_overlay'], 'text', class_='value gauge-sum', x=x, y=y + self.style.value_font_size / 3, attrib={'text-anchor': 'middle'} ).text = self._serie_format(serie, sum_) def _compute_x_labels(self): pass def _compute_y_labels(self): pass def _plot(self): """Draw all the serie slices""" squares = self._squares() sq_dimensions = self.add_squares(squares) for index, serie in enumerate(self.series): current_square = self._current_square(squares, index) self.gaugify( serie, squares, sq_dimensions, current_square) def _squares(self): n_series_ = len(self.series) i = 2 if sqrt(n_series_).is_integer(): _x = int(sqrt(n_series_)) _y = int(sqrt(n_series_)) else: while i * i < n_series_: while n_series_ % i == 0: n_series_ = n_series_ / i i = i + 1 _y = int(n_series_) _x = int(len(self.series) / _y) if len(self.series) == 5: _x, _y = 2, 3 if abs(_x - _y) > 2: _sq = 3 while (_x * _y) - 1 < len(self.series): _x, _y = _sq, _sq _sq += 1 return (_x, _y) def _current_square(self, squares, index): current_square = [1, 1] steps = index + 1 steps_taken = 0 for i in range(squares[0] * squares[1]): steps_taken += 1 if steps_taken != steps and steps_taken % squares[0] != 0: current_square[1] += 1 elif steps_taken != steps and steps_taken % squares[0] == 0: current_square[1] = 1 current_square[0] += 1 else: return tuple(current_square) raise Exception( 'Something went wrong with the current square assignment.') pygal-2.4.0/pygal/graph/public.py0000644000175000017500000001327713114033322016660 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """pygal public api functions""" import base64 import io from pygal._compat import _ellipsis, is_list_like, u from pygal.graph.base import BaseGraph class PublicApi(BaseGraph): """Chart public functions""" def add(self, title, values, **kwargs): """Add a serie to this graph, compat api""" if not is_list_like(values) and not isinstance(values, dict): values = [values] kwargs['title'] = title self.raw_series.append((values, kwargs)) return self def __call__(self, *args, **kwargs): """Call api: chart(1, 2, 3, title='T')""" self.raw_series.append((args, kwargs)) return self def add_xml_filter(self, callback): """Add an xml filter for in tree post processing""" self.xml_filters.append(callback) return self def render(self, is_unicode=False, **kwargs): """Render the graph, and return the svg string""" self.setup(**kwargs) svg = self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) self.teardown() return svg def render_tree(self, **kwargs): """Render the graph, and return (l)xml etree""" self.setup(**kwargs) svg = self.svg.root for f in self.xml_filters: svg = f(svg) self.teardown() return svg def render_table(self, **kwargs): """Render the data as a html table""" # Import here to avoid lxml import try: from pygal.table import Table except ImportError: raise ImportError('You must install lxml to use render table') return Table(self).render(**kwargs) def render_pyquery(self, **kwargs): """Render the graph, and return a pyquery wrapped tree""" from pyquery import PyQuery as pq return pq(self.render(**kwargs), parser='html') 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') kwargs.setdefault('force_uri_protocol', 'https') open_in_browser(self.render_tree(**kwargs), encoding='utf-8') def render_response(self, **kwargs): """Render the graph, and return a Flask response""" from flask import Response return Response(self.render(**kwargs), mimetype='image/svg+xml') def render_django_response(self, **kwargs): """Render the graph, and return a Django response""" from django.http import HttpResponse return HttpResponse( self.render(**kwargs), content_type='image/svg+xml') def render_data_uri(self, **kwargs): """Output a base 64 encoded data uri""" # Force protocol as data uri have none kwargs.setdefault('force_uri_protocol', 'https') return "data:image/svg+xml;charset=utf-8;base64,%s" % ( base64.b64encode( self.render(**kwargs) ).decode('utf-8').replace('\n', '') ) 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, **kwargs)) 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(**kwargs), write_to=filename, dpi=dpi) def render_sparktext(self, relative_to=None): """Make a mini text sparkline from chart""" bars = u('▁▂▃▄▅▆▇█') if len(self.raw_series) == 0: return u('') values = list(self.raw_series[0][0]) if len(values) == 0: return u('') chart = u('') values = list(map(lambda x: max(x, 0), values)) vmax = max(values) if relative_to is None: relative_to = min(values) if (vmax - relative_to) == 0: chart = bars[0] * len(values) return chart divisions = len(bars) - 1 for value in values: chart += bars[int(divisions * (value - relative_to) / (vmax - relative_to))] return chart def render_sparkline(self, **kwargs): """Render a sparkline""" spark_options = dict( width=200, height=50, show_dots=False, show_legend=False, show_x_labels=False, show_y_labels=False, spacing=0, margin=5, min_scale=1, max_scale=2, explicit_size=True, no_data_text='', js=(), classes=(_ellipsis, 'pygal-sparkline') ) spark_options.update(kwargs) return self.render(**spark_options) pygal-2.4.0/pygal/graph/time.py0000644000175000017500000000746513127125562016356 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ XY time extensions: handle convertion of date, time, datetime, timedelta into float for xy plot and back to their type for display """ from datetime import date, datetime, time, timedelta from pygal._compat import is_str, timestamp from pygal.adapters import positive from pygal.graph.xy import XY def datetime_to_timestamp(x): """Convert a datetime into a utc float timestamp""" if isinstance(x, datetime): return timestamp(x) return x def datetime_to_time(x): """Convert a datetime into a time""" if isinstance(x, datetime): return x.time() return x def date_to_datetime(x): """Convert a date into a datetime""" if not isinstance(x, datetime) and isinstance(x, date): return datetime.combine(x, time()) return x def time_to_datetime(x): """Convert a time into a datetime""" if isinstance(x, time): return datetime.combine(date(1970, 1, 1), x) return x def timedelta_to_seconds(x): """Convert a timedelta into an amount of seconds""" if isinstance(x, timedelta): return x.total_seconds() return x def time_to_seconds(x): """Convert a time in a seconds sum""" if isinstance(x, time): return (( ((x.hour * 60) + x.minute) * 60 + x.second ) * 10 ** 6 + x.microsecond) / 10 ** 6 if is_str(x): return x # Clamp to valid time return x and max(0, min(x, 24 * 3600 - 10 ** -6)) def seconds_to_time(x): """Convert a number of second into a time""" t = int(x * 10 ** 6) ms = t % 10 ** 6 t = t // 10 ** 6 s = t % 60 t = t // 60 m = t % 60 t = t // 60 h = t return time(h, m, s, ms) class DateTimeLine(XY): """DateTime abscissa xy graph class""" _x_adapters = [datetime_to_timestamp, date_to_datetime] @property def _x_format(self): """Return the value formatter for this graph""" def datetime_to_str(x): dt = datetime.utcfromtimestamp(x) return self.x_value_formatter(dt) return datetime_to_str class DateLine(DateTimeLine): """Date abscissa xy graph class""" @property def _x_format(self): """Return the value formatter for this graph""" def date_to_str(x): d = datetime.utcfromtimestamp(x).date() return self.x_value_formatter(d) return date_to_str class TimeLine(DateTimeLine): """Time abscissa xy graph class""" _x_adapters = [positive, time_to_seconds, datetime_to_time] @property def _x_format(self): """Return the value formatter for this graph""" def date_to_str(x): t = seconds_to_time(x) return self.x_value_formatter(t) return date_to_str class TimeDeltaLine(XY): """TimeDelta abscissa xy graph class""" _x_adapters = [timedelta_to_seconds] @property def _x_format(self): """Return the value formatter for this graph""" def timedelta_to_str(x): td = timedelta(seconds=x) return self.x_value_formatter(td) return timedelta_to_str pygal-2.4.0/pygal/graph/radar.py0000644000175000017500000001637413114033322016474 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Radar chart: As known as kiviat chart or spider chart is a polar line chart useful for multivariate observation. """ from __future__ import division from math import cos, pi from pygal._compat import is_str from pygal.adapters import none_to_zero, positive from pygal.graph.line import Line from pygal.util import cached_property, compute_scale, cut, deg, truncate from pygal.view import PolarLogView, PolarView class Radar(Line): """Rada graph class""" _adapters = [positive, none_to_zero] def __init__(self, *args, **kwargs): """Init custom vars""" self._rmax = None super(Radar, self).__init__(*args, **kwargs) def _fill(self, values): """Add extra values to fill the line""" return values @cached_property def _values(self): """Getter for series values (flattened)""" if self.interpolate: return [val[0] for serie in self.series for val in serie.interpolated] else: return super(Line, self)._values def _set_view(self): """Assign a view to current graph""" if self.logarithmic: view_class = PolarLogView else: view_class = PolarView self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) def _x_axis(self, draw_axes=True): """Override x axis to make it polar""" if not self._x_labels or not self.show_x_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis x web%s" % ( ' always_show' if self.show_x_guides else '' )) format_ = lambda x: '%f %f' % x center = self.view((0, 0)) r = self._rmax # Can't simply determine truncation truncation = self.truncate_label or 25 for label, theta in self._x_labels: major = label in self._x_labels_major if not (self.show_minor_x_labels or major): continue guides = self.svg.node(axis, class_='guides') end = self.view((r, theta)) self.svg.node( guides, 'path', d='M%s L%s' % (format_(center), format_(end)), class_='%s%sline' % ( 'axis ' if label == "0" else '', 'major ' if major else '')) r_txt = (1 - self._box.__class__.margin) * self._box.ymax pos_text = self.view((r_txt, theta)) text = self.svg.node( guides, 'text', x=pos_text[0], y=pos_text[1], class_='major' if major else '') text.text = truncate(label, truncation) if text.text != label: self.svg.node(guides, 'title').text = label else: self.svg.node( guides, 'title', ).text = self._x_format(theta) angle = - theta + pi / 2 if cos(angle) < 0: angle -= pi text.attrib['transform'] = 'rotate(%f %s)' % ( self.x_label_rotation or deg(angle), format_(pos_text)) def _y_axis(self, draw_axes=True): """Override y axis to make it polar""" if not self._y_labels or not self.show_y_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis y web") for label, r in reversed(self._y_labels): major = r in self._y_labels_major if not (self.show_minor_y_labels or major): continue guides = self.svg.node(axis, class_='%sguides' % ( 'logarithmic ' if self.logarithmic else '' )) if self.show_y_guides: self.svg.line( guides, [self.view((r, theta)) for theta in self._x_pos], close=True, class_='%sguide line' % ( 'major ' if major else '')) x, y = self.view((r, self._x_pos[0])) x -= 5 text = self.svg.node( guides, 'text', x=x, y=y, class_='major' if major else '' ) text.text = label if self.y_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) self.svg.node( guides, 'title', ).text = self._y_format(r) def _compute(self): """Compute r min max and labels position""" delta = 2 * pi / self._len if self._len else 0 self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] for serie in self.all_series: serie.points = [ (v, self._x_pos[i]) for i, v in enumerate(serie.values)] if self.interpolate: extended_x_pos = ( [.5 * pi - delta] + self._x_pos) extended_vals = (serie.values[-1:] + serie.values) serie.interpolated = list( map(tuple, map(reversed, self._interpolate( extended_x_pos, extended_vals)))) # x labels space self._box.margin *= 2 self._rmin = self.zero self._rmax = self._max or 1 self._box.set_polar_box(self._rmin, self._rmax) self._self_close = True def _compute_y_labels(self): y_pos = compute_scale( self._rmin, self._rmax, self.logarithmic, self.order_min, self.min_scale, self.max_scale / 2 ) if self.y_labels: self._y_labels = [] for i, y_label in enumerate(self.y_labels): if isinstance(y_label, dict): pos = self._adapt(y_label.get('value')) title = y_label.get('label', self._y_format(pos)) elif is_str(y_label): pos = self._adapt(y_pos[i]) title = y_label else: pos = self._adapt(y_label) title = self._y_format(pos) self._y_labels.append((title, pos)) self._rmin = min(self._rmin, min(cut(self._y_labels, 1))) self._rmax = max(self._rmax, max(cut(self._y_labels, 1))) self._box.set_polar_box(self._rmin, self._rmax) else: self._y_labels = list(zip(map(self._y_format, y_pos), y_pos)) pygal-2.4.0/pygal/graph/stackedline.py0000644000175000017500000000626713114033322017671 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Stacked Line chart: Like a line chart but with all lines stacking on top of the others. Used along fill=True option. """ from __future__ import division from pygal.adapters import none_to_zero from pygal.graph.line import Line class StackedLine(Line): """Stacked Line graph class""" _adapters = [none_to_zero] def __init__(self, *args, **kwargs): """Custom variable initialization""" self._previous_line = None super(StackedLine, self).__init__(*args, **kwargs) def _value_format(self, value, serie, index): """ Display value and cumulation """ sum_ = serie.points[index][1] if serie in self.series and ( self.stack_from_top and self.series.index(serie) == self._order - 1 or not self.stack_from_top and self.series.index(serie) == 0): return super(StackedLine, self)._value_format(value) return '%s (+%s)' % ( self._y_format(sum_), self._y_format(value) ) def _fill(self, values): """Add extra values to fill the line""" if not self._previous_line: self._previous_line = values return super(StackedLine, self)._fill(values) new_values = values + list(reversed(self._previous_line)) self._previous_line = values return new_values def _points(self, x_pos): """ Convert given data values into drawable points (x, y) and interpolated points if interpolate option is specified """ for series_group in (self.series, self.secondary_series): accumulation = [0] * self._len for serie in series_group[::-1 if self.stack_from_top else 1]: accumulation = list(map(sum, zip(accumulation, serie.values))) serie.points = [ (x_pos[i], v) for i, v in enumerate(accumulation)] if serie.points and self.interpolate: serie.interpolated = self._interpolate(x_pos, accumulation) else: serie.interpolated = [] def _plot(self): """Plot stacked serie lines and stacked secondary lines""" for serie in self.series[::-1 if self.stack_from_top else 1]: self.line(serie) for serie in self.secondary_series[::-1 if self.stack_from_top else 1]: self.line(serie, True) pygal-2.4.0/pygal/graph/graph.py0000644000175000017500000010716713127125562016521 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Chart properties and drawing""" from __future__ import division from math import ceil, cos, sin, sqrt from pygal import stats from pygal._compat import is_list_like, is_str, to_str from pygal.graph.public import PublicApi from pygal.interpolate import INTERPOLATIONS from pygal.util import ( cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box, get_texts_box, majorize, rad, reverse_text_len, split_title, truncate) from pygal.view import LogView, ReverseView, View, XYLogView class Graph(PublicApi): """Graph super class containing generic common functions""" _dual = False def _decorate(self): """Draw all decorations""" self._set_view() self._make_graph() self._axes() self._legend() self._make_title() self._make_x_title() self._make_y_title() def _axes(self): """Draw axes""" self._y_axis() self._x_axis() def _set_view(self): """Assign a view to current graph""" if self.logarithmic: if self._dual: view_class = XYLogView else: view_class = LogView else: view_class = ReverseView if self.inverse_y_axis else View self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) def _make_graph(self): """Init common graph svg structure""" self.nodes['graph'] = self.svg.node( class_='graph %s-graph %s' % ( self.__class__.__name__.lower(), 'horizontal' if self.horizontal else 'vertical')) self.svg.node(self.nodes['graph'], 'rect', class_='background', x=0, y=0, width=self.width, height=self.height) self.nodes['plot'] = self.svg.node( self.nodes['graph'], class_="plot", transform="translate(%d, %d)" % ( self.margin_box.left, self.margin_box.top)) self.svg.node(self.nodes['plot'], 'rect', class_='background', x=0, y=0, width=self.view.width, height=self.view.height) self.nodes['title'] = self.svg.node( self.nodes['graph'], class_="titles") self.nodes['overlay'] = self.svg.node( self.nodes['graph'], class_="plot overlay", transform="translate(%d, %d)" % ( self.margin_box.left, self.margin_box.top)) self.nodes['text_overlay'] = self.svg.node( self.nodes['graph'], class_="plot text-overlay", transform="translate(%d, %d)" % ( self.margin_box.left, self.margin_box.top)) self.nodes['tooltip_overlay'] = self.svg.node( self.nodes['graph'], class_="plot tooltip-overlay", transform="translate(%d, %d)" % ( self.margin_box.left, self.margin_box.top)) self.nodes['tooltip'] = self.svg.node( self.nodes['tooltip_overlay'], transform='translate(0 0)', style="opacity: 0", **{'class': 'tooltip'}) self.svg.node(self.nodes['tooltip'], 'rect', rx=self.tooltip_border_radius, ry=self.tooltip_border_radius, width=0, height=0, **{'class': 'tooltip-box'}) self.svg.node(self.nodes['tooltip'], 'g', class_='text') def _x_axis(self): """Make the x axis: labels and guides""" if not self._x_labels or not self.show_x_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis x%s" % ( ' always_show' if self.show_x_guides else '' )) truncation = self.truncate_label if not truncation: if self.x_label_rotation or len(self._x_labels) <= 1: truncation = 25 else: first_label_position = self.view.x(self._x_labels[0][1]) or 0 last_label_position = self.view.x(self._x_labels[-1][1]) or 0 available_space = ( last_label_position - first_label_position) / ( len(self._x_labels) - 1) truncation = reverse_text_len( available_space, self.style.label_font_size) truncation = max(truncation, 1) lastlabel = self._x_labels[-1][0] if 0 not in [label[1] for label in self._x_labels]: self.svg.node(axis, 'path', d='M%f %f v%f' % (0, 0, self.view.height), class_='line') lastlabel = None for label, position in self._x_labels: if self.horizontal: major = position in self._x_labels_major else: major = label in self._x_labels_major if not (self.show_minor_x_labels or major): continue guides = self.svg.node(axis, class_='guides') x = self.view.x(position) if x is None: continue y = self.view.height + 5 last_guide = (self._y_2nd_labels and label == lastlabel) self.svg.node( guides, 'path', d='M%f %f v%f' % (x or 0, 0, self.view.height), class_='%s%s%sline' % ( 'axis ' if label == "0" else '', 'major ' if major else '', 'guide ' if position != 0 and not last_guide else '')) y += .5 * self.style.label_font_size + 5 text = self.svg.node( guides, 'text', x=x, y=y, class_='major' if major else '' ) text.text = truncate(label, truncation) if text.text != label: self.svg.node(guides, 'title').text = label elif self._dual: self.svg.node( guides, 'title', ).text = self._x_format(position) if self.x_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( self.x_label_rotation, x, y) if self.x_label_rotation >= 180: text.attrib['class'] = ' '.join( (text.attrib['class'] and text.attrib['class'].split( ' ') or []) + ['backwards']) if self._y_2nd_labels and 0 not in [ label[1] for label in self._x_labels]: self.svg.node(axis, 'path', d='M%f %f v%f' % ( self.view.width, 0, self.view.height), class_='line') if self._x_2nd_labels: secondary_ax = self.svg.node( self.nodes['plot'], class_="axis x x2%s" % ( ' always_show' if self.show_x_guides else '' )) for label, position in self._x_2nd_labels: major = label in self._x_labels_major if not (self.show_minor_x_labels or major): continue # it is needed, to have the same structure as primary axis guides = self.svg.node(secondary_ax, class_='guides') x = self.view.x(position) y = -5 text = self.svg.node( guides, 'text', x=x, y=y, class_='major' if major else '' ) text.text = label if self.x_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( -self.x_label_rotation, x, y) if self.x_label_rotation >= 180: text.attrib['class'] = ' '.join(( text.attrib['class'] and text.attrib['class'].split( ' ') or []) + ['backwards']) def _y_axis(self): """Make the y axis: labels and guides""" if not self._y_labels or not self.show_y_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis y%s" % ( ' always_show' if self.show_y_guides else '' )) if (0 not in [label[1] for label in self._y_labels] and self.show_y_guides): self.svg.node( axis, 'path', d='M%f %f h%f' % ( 0, 0 if self.inverse_y_axis else self.view.height, self.view.width), class_='line' ) for label, position in self._y_labels: if self.horizontal: major = label in self._y_labels_major else: major = position in self._y_labels_major if not (self.show_minor_y_labels or major): continue guides = self.svg.node(axis, class_='%sguides' % ( 'logarithmic ' if self.logarithmic else '' )) x = -5 y = self.view.y(position) if not y: continue if self.show_y_guides: self.svg.node( guides, 'path', d='M%f %f h%f' % (0, y, self.view.width), class_='%s%s%sline' % ( 'axis ' if label == "0" else '', 'major ' if major else '', 'guide ' if position != 0 else '')) text = self.svg.node( guides, 'text', x=x, y=y + .35 * self.style.label_font_size, class_='major' if major else '' ) text.text = label if self.y_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) if 90 < self.y_label_rotation < 270: text.attrib['class'] = ' '.join( (text.attrib['class'] and text.attrib['class'].split( ' ') or []) + ['backwards']) self.svg.node( guides, 'title', ).text = self._y_format(position) if self._y_2nd_labels: secondary_ax = self.svg.node( self.nodes['plot'], class_="axis y2") for label, position in self._y_2nd_labels: major = position in self._y_labels_major if not (self.show_minor_y_labels or major): continue # it is needed, to have the same structure as primary axis guides = self.svg.node(secondary_ax, class_='guides') x = self.view.width + 5 y = self.view.y(position) text = self.svg.node( guides, 'text', x=x, y=y + .35 * self.style.label_font_size, class_='major' if major else '' ) text.text = label if self.y_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) if 90 < self.y_label_rotation < 270: text.attrib['class'] = ' '.join( (text.attrib['class'] and text.attrib['class'].split( ' ') or []) + ['backwards']) def _legend(self): """Make the legend box""" if not self.show_legend: return truncation = self.truncate_legend if self.legend_at_bottom: x = self.margin_box.left + self.spacing y = (self.margin_box.top + self.view.height + self._x_title_height + self._x_labels_height + self.spacing) cols = self.legend_at_bottom_columns or ceil( sqrt(self._order)) or 1 if not truncation: available_space = self.view.width / cols - ( self.legend_box_size + 5) truncation = reverse_text_len( available_space, self.style.legend_font_size) else: x = self.spacing y = self.margin_box.top + self.spacing cols = 1 if not truncation: truncation = 15 legends = self.svg.node( self.nodes['graph'], class_='legends', transform='translate(%d, %d)' % (x, y)) h = max(self.legend_box_size, self.style.legend_font_size) x_step = self.view.width / cols if self.legend_at_bottom: secondary_legends = legends # svg node is the same else: # draw secondary axis on right x = self.margin_box.left + self.view.width + self.spacing if self._y_2nd_labels: h, w = get_texts_box( cut(self._y_2nd_labels), self.style.label_font_size) x += self.spacing + max(w * abs(cos(rad( self.y_label_rotation))), h) y = self.margin_box.top + self.spacing secondary_legends = self.svg.node( self.nodes['graph'], class_='legends', transform='translate(%d, %d)' % (x, y)) serie_number = -1 i = 0 for titles, is_secondary in ( (self._legends, False), (self._secondary_legends, True)): if not self.legend_at_bottom and is_secondary: i = 0 for title in titles: serie_number += 1 if title is None: continue col = i % cols row = i // cols legend = self.svg.node( secondary_legends if is_secondary else legends, class_='legend reactive activate-serie', id="activate-serie-%d" % serie_number) self.svg.node( legend, 'rect', x=col * x_step, y=1.5 * row * h + ( self.style.legend_font_size - self.legend_box_size if self.style.legend_font_size > self.legend_box_size else 0 ) / 2, width=self.legend_box_size, height=self.legend_box_size, class_="color-%d reactive" % serie_number ) if isinstance(title, dict): node = decorate(self.svg, legend, title) title = title['title'] else: node = legend truncated = truncate(title, truncation) self.svg.node( node, 'text', x=col * x_step + self.legend_box_size + 5, y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size ).text = truncated if truncated != title: self.svg.node(legend, 'title').text = title i += 1 def _make_title(self): """Make the title""" if self._title: for i, title_line in enumerate(self._title, 1): self.svg.node( self.nodes['title'], 'text', class_='title plot_title', x=self.width / 2, y=i * (self.style.title_font_size + self.spacing) ).text = title_line def _make_x_title(self): """Make the X-Axis title""" y = (self.height - self.margin_box.bottom + self._x_labels_height) if self._x_title: for i, title_line in enumerate(self._x_title, 1): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self.margin_box.left + self.view.width / 2, y=y + i * (self.style.title_font_size + self.spacing) ) text.text = title_line def _make_y_title(self): """Make the Y-Axis title""" if self._y_title: yc = self.margin_box.top + self.view.height / 2 for i, title_line in enumerate(self._y_title, 1): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self._legend_at_left_width, y=i * (self.style.title_font_size + self.spacing) + yc ) text.attrib['transform'] = "rotate(%d %f %f)" % ( -90, self._legend_at_left_width, yc) text.text = title_line def _interpolate(self, xs, ys): """Make the interpolation""" x = [] y = [] for i in range(len(ys)): if ys[i] is not None: x.append(xs[i]) y.append(ys[i]) interpolate = INTERPOLATIONS[self.interpolate] return list(interpolate( x, y, self.interpolation_precision, **self.interpolation_parameters)) def _rescale(self, points): """Scale for secondary""" return [ (x, self._scale_diff + (y - self._scale_min_2nd) * self._scale if y is not None else None) for x, y in points] def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None): """Insert in desc tags informations for the javascript tooltip""" self.svg.node(node, 'desc', class_="value").text = value if classes is None: classes = [] if x > self.view.width / 2: classes.append('left') if y > self.view.height / 2: classes.append('top') classes = ' '.join(classes) self.svg.node(node, 'desc', class_="x " + classes).text = to_str(x) self.svg.node(node, 'desc', class_="y " + classes).text = to_str(y) if xlabel: self.svg.node(node, 'desc', class_="x_label").text = to_str(xlabel) def _static_value(self, serie_node, value, x, y, metadata, align_text='left', classes=None): """Write the print value""" label = metadata and metadata.get('label') classes = classes and [classes] or [] if self.print_labels and label: label_cls = classes + ['label'] if self.print_values: y -= self.style.value_font_size / 2 self.svg.node( serie_node['text_overlay'], 'text', class_=' '.join(label_cls), x=x, y=y + self.style.value_font_size / 3 ).text = label y += self.style.value_font_size if self.print_values or self.dynamic_print_values: val_cls = classes + ['value'] if self.dynamic_print_values: val_cls.append('showable') self.svg.node( serie_node['text_overlay'], 'text', class_=' '.join(val_cls), x=x, y=y + self.style.value_font_size / 3, attrib={'text-anchor': align_text} ).text = value if self.print_zeroes or value != '0' else '' def _points(self, x_pos): """ Convert given data values into drawable points (x, y) and interpolated points if interpolate option is specified """ for serie in self.all_series: serie.points = [ (x_pos[i], v) for i, v in enumerate(serie.values)] if serie.points and self.interpolate: serie.interpolated = self._interpolate(x_pos, serie.values) else: serie.interpolated = [] def _compute_secondary(self): """Compute secondary axis min max and label positions""" # secondary y axis support if self.secondary_series and self._y_labels: y_pos = list(zip(*self._y_labels))[1] if self.include_x_axis: ymin = min(self._secondary_min, 0) ymax = max(self._secondary_max, 0) else: ymin = self._secondary_min ymax = self._secondary_max steps = len(y_pos) left_range = abs(y_pos[-1] - y_pos[0]) right_range = abs(ymax - ymin) or 1 scale = right_range / ((steps - 1) or 1) self._y_2nd_labels = [(self._y_format(ymin + i * scale), pos) for i, pos in enumerate(y_pos)] self._scale = left_range / right_range self._scale_diff = y_pos[0] self._scale_min_2nd = ymin def _post_compute(self): """Hook called after compute and before margin computations and plot""" pass def _get_x_label(self, i): """Convenience function to get the x_label of a value index""" if not self.x_labels or not self._x_labels or len(self._x_labels) <= i: return return self._x_labels[i][0] @property def all_series(self): """Getter for all series (nomal and secondary)""" return self.series + self.secondary_series @property def _x_format(self): """Return the abscissa value formatter (always unary)""" return self.x_value_formatter @property def _default_formatter(self): return to_str @property def _y_format(self): """Return the ordinate value formatter (always unary)""" return self.value_formatter def _value_format(self, value): """ Format value for value display. (Varies in type between chart types) """ return self._y_format(value) def _format(self, serie, i): """Format the nth value for the serie""" value = serie.values[i] metadata = serie.metadata.get(i) kwargs = { 'chart': self, 'serie': serie, 'index': i } formatter = ( (metadata and metadata.get('formatter')) or serie.formatter or self.formatter or self._value_format ) kwargs = filter_kwargs(formatter, kwargs) return formatter(value, **kwargs) def _serie_format(self, serie, value): """Format an independent value for the serie""" kwargs = { 'chart': self, 'serie': serie, 'index': None } formatter = ( serie.formatter or self.formatter or self._value_format ) kwargs = filter_kwargs(formatter, kwargs) return formatter(value, **kwargs) def _compute(self): """Initial computations to draw the graph""" def _compute_margin(self): """Compute graph margins from set texts""" self._legend_at_left_width = 0 for series_group in (self.series, self.secondary_series): if self.show_legend and series_group: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), [serie.title['title'] if isinstance(serie.title, dict) else serie.title or '' for serie in series_group]), self.style.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) cols = (self._order // self.legend_at_bottom_columns if self.legend_at_bottom_columns else ceil(sqrt(self._order)) or 1) self.margin_box.bottom += self.spacing + h_max * round( cols - 1) * 1.5 + h_max else: if series_group is self.series: legend_width = self.spacing + w + self.legend_box_size self.margin_box.left += legend_width self._legend_at_left_width += legend_width else: self.margin_box.right += ( self.spacing + w + self.legend_box_size) self._x_labels_height = 0 if (self._x_labels or self._x_2nd_labels) and self.show_x_labels: for xlabels in (self._x_labels, self._x_2nd_labels): if xlabels: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_label or 25), cut(xlabels)), self.style.label_font_size) self._x_labels_height = self.spacing + max( w * abs(sin(rad(self.x_label_rotation))), h) if xlabels is self._x_labels: self.margin_box.bottom += self._x_labels_height else: self.margin_box.top += self._x_labels_height if self.x_label_rotation: if self.x_label_rotation % 180 < 90: self.margin_box.right = max( w * abs(cos(rad(self.x_label_rotation))), self.margin_box.right) else: self.margin_box.left = max( w * abs(cos(rad(self.x_label_rotation))), self.margin_box.left) if self.show_y_labels: for ylabels in (self._y_labels, self._y_2nd_labels): if ylabels: h, w = get_texts_box( cut(ylabels), self.style.label_font_size) if ylabels is self._y_labels: self.margin_box.left += self.spacing + max( w * abs(cos(rad(self.y_label_rotation))), h) else: self.margin_box.right += self.spacing + max( w * abs(cos(rad(self.y_label_rotation))), h) self._title = split_title( self.title, self.width, self.style.title_font_size) if self.title: h, _ = get_text_box(self._title[0], self.style.title_font_size) self.margin_box.top += len(self._title) * (self.spacing + h) self._x_title = split_title( self.x_title, self.width - self.margin_box.x, self.style.title_font_size) self._x_title_height = 0 if self._x_title: h, _ = get_text_box(self._x_title[0], self.style.title_font_size) height = len(self._x_title) * (self.spacing + h) self.margin_box.bottom += height self._x_title_height = height + self.spacing self._y_title = split_title( self.y_title, self.height - self.margin_box.y, self.style.title_font_size) self._y_title_height = 0 if self._y_title: h, _ = get_text_box(self._y_title[0], self.style.title_font_size) height = len(self._y_title) * (self.spacing + h) self.margin_box.left += height self._y_title_height = height + self.spacing # Inner margin if self.print_values_position == 'top': gh = self.height - self.margin_box.y alpha = 1.1 * (self.style.value_font_size / gh) * self._box.height if self._max and self._max > 0: self._box.ymax += alpha if self._min and self._min < 0: self._box.ymin -= alpha def _confidence_interval(self, node, x, y, value, metadata): if not metadata or 'ci' not in metadata: return ci = metadata['ci'] ci['point_estimate'] = value low, high = getattr( stats, 'confidence_interval_%s' % ci.get('type', 'manual') )(**ci) self.svg.confidence_interval( node, x, # Respect some charts y modifications (pyramid, stackbar) y + (self.view.y(low) - self.view.y(value)), y + (self.view.y(high) - self.view.y(value))) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _secondary_legends(self): """Getter for series title on secondary y axis""" return [serie.title for serie in self.secondary_series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val is not None] @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [val for serie in self.secondary_series for val in serie.values if val is not None] @cached_property def _len(self): """Getter for the maximum series size""" return max([ len(serie.values) for serie in self.all_series] or [0]) @cached_property def _secondary_min(self): """Getter for the minimum series value""" return (self.secondary_range[0] if ( self.secondary_range and self.secondary_range[0] is not None) else (min(self._secondary_values) if self._secondary_values else None)) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range[0] if (self.range and self.range[0] is not None) else (min(self._values) if self._values else None)) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range[1] if (self.range and self.range[1] is not None) else (max(self._values) if self._values else None)) @cached_property def _secondary_max(self): """Getter for the maximum series value""" return (self.secondary_range[1] if ( self.secondary_range and self.secondary_range[1] is not None) else (max(self._secondary_values) if self._secondary_values else None)) @cached_property def _order(self): """Getter for the number of series""" return len(self.all_series) def _x_label_format_if_value(self, label): if not is_str(label): return self._x_format(label) return label def _compute_x_labels(self): self._x_labels = self.x_labels and list( zip(map(self._x_label_format_if_value, self.x_labels), self._x_pos)) def _compute_x_labels_major(self): if self.x_labels_major_every: self._x_labels_major = [self._x_labels[i][0] for i in range( 0, len(self._x_labels), self.x_labels_major_every)] elif self.x_labels_major_count: label_count = len(self._x_labels) major_count = self.x_labels_major_count if (major_count >= label_count): self._x_labels_major = [label[0] for label in self._x_labels] else: self._x_labels_major = [self._x_labels[ int(i * (label_count - 1) / (major_count - 1))][0] for i in range(major_count)] else: self._x_labels_major = self.x_labels_major and list( map(self._x_label_format_if_value, self.x_labels_major)) or [] def _compute_y_labels(self): y_pos = compute_scale( self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, self.min_scale, self.max_scale ) if self.y_labels: self._y_labels = [] for i, y_label in enumerate(self.y_labels): if isinstance(y_label, dict): pos = self._adapt(y_label.get('value')) title = y_label.get('label', self._y_format(pos)) elif is_str(y_label): pos = self._adapt(y_pos[i % len(y_pos)]) title = y_label else: pos = self._adapt(y_label) title = self._y_format(pos) self._y_labels.append((title, pos)) self._box.ymin = min(self._box.ymin, min(cut(self._y_labels, 1))) self._box.ymax = max(self._box.ymax, max(cut(self._y_labels, 1))) else: self._y_labels = list(zip(map(self._y_format, y_pos), y_pos)) def _compute_y_labels_major(self): if self.y_labels_major_every: self._y_labels_major = [self._y_labels[i][1] for i in range( 0, len(self._y_labels), self.y_labels_major_every)] elif self.y_labels_major_count: label_count = len(self._y_labels) major_count = self.y_labels_major_count if (major_count >= label_count): self._y_labels_major = [label[1] for label in self._y_labels] else: self._y_labels_major = [self._y_labels[ int(i * (label_count - 1) / (major_count - 1))][1] for i in range(major_count)] elif self.y_labels_major: self._y_labels_major = list(map(self._adapt, self.y_labels_major)) elif self._y_labels: self._y_labels_major = majorize(cut(self._y_labels, 1)) else: self._y_labels_major = [] def add_squares(self, squares): x_lines = squares[0] - 1 y_lines = squares[1] - 1 _current_x = 0 _current_y = 0 for line in range(x_lines): _current_x += (self.width - self.margin_box.x) / squares[0] self.svg.node( self.nodes['plot'], 'path', class_='bg-lines', d='M%s %s L%s %s' % ( _current_x, 0, _current_x, self.height - self.margin_box.y)) for line in range(y_lines): _current_y += (self.height - self.margin_box.y) / squares[1] self.svg.node( self.nodes['plot'], 'path', class_='bg-lines', d='M%s %s L%s %s' % ( 0, _current_y, self.width - self.margin_box.x, _current_y)) return ((self.width - self.margin_box.x) / squares[0], (self.height - self.margin_box.y) / squares[1]) def _draw(self): """Draw all the things""" self._compute() self._compute_x_labels() self._compute_x_labels_major() self._compute_y_labels() self._compute_y_labels_major() self._compute_secondary() self._post_compute() self._compute_margin() self._decorate() if self.series and self._has_data() and self._values: self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return any([ len([ v for a in (s[0] if is_list_like(s) else [s]) for v in (a if is_list_like(a) else [a]) if v is not None]) for s in self.raw_series ]) pygal-2.4.0/pygal/graph/map.py0000644000175000017500000001064413114033322016152 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ pygal contains no map but a base class to create extension see the pygal_maps_world package to get an exemple. https://github.com/Kozea/pygal_maps_world """ from __future__ import division from pygal.etree import etree from pygal.graph.graph import Graph from pygal.util import alter, cached_property, cut, decorate class BaseMap(Graph): """Base class for maps""" _dual = True @cached_property def _values(self): """Getter for series values (flattened)""" return [val[1] for serie in self.series for val in serie.values if val[1] is not None] def enumerate_values(self, serie): """Hook to replace default enumeration on values""" return enumerate(serie.values) def adapt_code(self, area_code): """Hook to change the area code""" return area_code def _value_format(self, value): """ Format value for map value display. """ return '%s: %s' % ( self.area_names.get(self.adapt_code(value[0]), '?'), self._y_format(value[1])) def _plot(self): """Insert a map in the chart and apply data on it""" map = etree.fromstring(self.svg_map) map.set('width', str(self.view.width)) map.set('height', str(self.view.height)) for i, serie in enumerate(self.series): safe_vals = list(filter( lambda x: x is not None, cut(serie.values, 1))) if not safe_vals: continue min_ = min(safe_vals) max_ = max(safe_vals) for j, (area_code, value) in self.enumerate_values(serie): area_code = self.adapt_code(area_code) if value is None: continue if max_ == min_: ratio = 1 else: ratio = .3 + .7 * (value - min_) / (max_ - min_) areae = map.findall( ".//*[@class='%s%s %s map-element']" % ( self.area_prefix, area_code, self.kind)) if not areae: continue for area in areae: cls = area.get('class', '').split(' ') cls.append('color-%d' % i) cls.append('serie-%d' % i) cls.append('series') area.set('class', ' '.join(cls)) area.set('style', 'fill-opacity: %f' % ratio) metadata = serie.metadata.get(j) if metadata: node = decorate(self.svg, area, metadata) if node != area: area.remove(node) for g in map: if area not in g: continue index = list(g).index(area) g.remove(area) node.append(area) g.insert(index, node) for node in area: cls = node.get('class', '').split(' ') cls.append('reactive') cls.append('tooltip-trigger') cls.append('map-area') node.set('class', ' '.join(cls)) alter(node, metadata) val = self._format(serie, j) self._tooltip_data(area, val, 0, 0, 'auto') self.nodes['plot'].append(map) def _compute_x_labels(self): pass def _compute_y_labels(self): pass pygal-2.4.0/pygal/graph/dual.py0000644000175000017500000000511413114033322016316 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Dual chart base. Dual means a chart with 2 scaled axis like xy""" from pygal._compat import is_str from pygal.graph.graph import Graph from pygal.util import compute_scale, cut class Dual(Graph): _dual = True def _value_format(self, value): """ Format value for dual value display. """ return '%s: %s' % ( self._x_format(value[0]), self._y_format(value[1])) def _compute_x_labels(self): x_pos = compute_scale( self._box.xmin, self._box.xmax, self.logarithmic, self.order_min, self.min_scale, self.max_scale ) if self.x_labels: self._x_labels = [] for i, x_label in enumerate(self.x_labels): if isinstance(x_label, dict): pos = self._x_adapt(x_label.get('value')) title = x_label.get('label', self._x_format(pos)) elif is_str(x_label): pos = self._x_adapt(x_pos[i % len(x_pos)]) title = x_label else: pos = self._x_adapt(x_label) title = self._x_format(pos) self._x_labels.append((title, pos)) self._box.xmin = min(self._box.xmin, min(cut(self._x_labels, 1))) self._box.xmax = max(self._box.xmax, max(cut(self._x_labels, 1))) else: self._x_labels = list(zip(map(self._x_format, x_pos), x_pos)) def _compute_x_labels_major(self): # In case of dual, x labels must adapters and so majors too self.x_labels_major = self.x_labels_major and list( map(self._x_adapt, self.x_labels_major)) super(Dual, self)._compute_x_labels_major() def _get_x_label(self, i): """Convenience function to get the x_label of a value index""" return pygal-2.4.0/pygal/graph/dot.py0000644000175000017500000001030613114033322016156 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Dot chart displaying values as a grid of dots, the bigger the value the bigger the dot """ from __future__ import division from math import log10 from pygal._compat import to_str from pygal.graph.graph import Graph from pygal.util import alter, cached_property, decorate, safe_enumerate from pygal.view import ReverseView, View class Dot(Graph): """Dot graph class""" def dot(self, serie, r_max): """Draw a dot line""" serie_node = self.svg.serie(serie) view_values = list(map(self.view, serie.points)) for i, value in safe_enumerate(serie.values): x, y = view_values[i] if self.logarithmic: log10min = log10(self._min) - 1 log10max = log10(self._max or 1) if value != 0: size = r_max * ( (log10(abs(value)) - log10min) / (log10max - log10min) ) else: size = 0 else: size = r_max * (abs(value) / (self._max or 1)) metadata = serie.metadata.get(i) dots = decorate( self.svg, self.svg.node(serie_node['plot'], class_="dots"), metadata) alter(self.svg.node( dots, 'circle', cx=x, cy=y, r=size, class_='dot reactive tooltip-trigger' + ( ' negative' if value < 0 else '')), metadata) val = self._format(serie, i) self._tooltip_data( dots, val, x, y, 'centered', self._get_x_label(i)) self._static_value(serie_node, val, x, y, metadata) def _compute(self): """Compute y min and max and y scale and set labels""" x_len = self._len y_len = self._order self._box.xmax = x_len self._box.ymax = y_len self._x_pos = [n / 2 for n in range(1, 2 * x_len, 2)] self._y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))] for j, serie in enumerate(self.series): serie.points = [ (self._x_pos[i], self._y_pos[j]) for i in range(x_len)] def _compute_y_labels(self): self._y_labels = list(zip( self.y_labels and map(to_str, self.y_labels) or [ serie.title['title'] if isinstance(serie.title, dict) else serie.title or '' for serie in self.series], self._y_pos)) def _set_view(self): """Assign a view to current graph""" view_class = ReverseView if self.inverse_y_axis else View self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) @cached_property def _values(self): """Getter for series values (flattened)""" return [abs(val) for val in super(Dot, self)._values if val != 0] @cached_property def _max(self): """Getter for the maximum series value""" return (self.range[1] if (self.range and self.range[1] is not None) else (max(map(abs, self._values)) if self._values else None)) def _plot(self): """Plot all dots for series""" r_max = min( self.view.x(1) - self.view.x(0), (self.view.y(0) or 0) - self.view.y(1)) / ( 2 * 1.05) for serie in self.series: self.dot(serie, r_max) pygal-2.4.0/pygal/view.py0000644000175000017500000003077313114033322015253 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Projection and bounding helpers""" from __future__ import division from math import cos, log10, pi, sin class Margin(object): """Class reprensenting a margin (top, right, left, bottom)""" def __init__(self, top, right, bottom, left): """Create the margin object from the top, right, left, bottom margin""" self.top = top self.right = right self.bottom = bottom self.left = left @property def x(self): """Helper for total x margin""" return self.left + self.right @property def y(self): """Helper for total y margin""" return self.top + self.bottom class Box(object): """Chart boundings""" margin = .02 def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): """ Create the chart bounds with min max horizontal and vertical values """ self._xmin = xmin self._ymin = ymin self._xmax = xmax self._ymax = ymax def set_polar_box(self, rmin=0, rmax=1, tmin=0, tmax=2 * pi): """Helper for polar charts""" self._rmin = rmin self._rmax = rmax self._tmin = tmin self._tmax = tmax self.xmin = self.ymin = rmin - rmax self.xmax = self.ymax = rmax - rmin @property def xmin(self): """X minimum getter""" return self._xmin @xmin.setter def xmin(self, value): """X minimum setter""" if value is not None: self._xmin = value @property def ymin(self): """Y minimum getter""" return self._ymin @ymin.setter def ymin(self, value): """Y minimum setter""" if value is not None: self._ymin = value @property def xmax(self): """X maximum getter""" return self._xmax @xmax.setter def xmax(self, value): """X maximum setter""" if value is not None: self._xmax = value @property def ymax(self): """Y maximum getter""" return self._ymax @ymax.setter def ymax(self, value): """Y maximum setter""" if value or self.ymin: self._ymax = value @property def width(self): """Helper for box width""" return self.xmax - self.xmin @property def height(self): """Helper for box height""" return self.ymax - self.ymin def swap(self): """Return the box (for horizontal graphs)""" self.xmin, self.ymin = self.ymin, self.xmin self.xmax, self.ymax = self.ymax, self.xmax def fix(self, with_margin=True): """Correct box when no values and take margin in account""" if not self.width: self.xmax = self.xmin + 1 if not self.height: self.ymin /= 2 self.ymax += self.ymin xmargin = self.margin * self.width self.xmin -= xmargin self.xmax += xmargin if with_margin: ymargin = self.margin * self.height self.ymin -= ymargin self.ymax += ymargin class View(object): """Projection base class""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.box.fix() def x(self, x): """Project x""" if x is None: return None return self.width * (x - self.box.xmin) / self.box.width def y(self, y): """Project y""" if y is None: return None return (self.height - self.height * (y - self.box.ymin) / self.box.height) def __call__(self, xy): """Project x and y""" x, y = xy return (self.x(x), self.y(y)) class ReverseView(View): """Same as view but reversed vertically""" def y(self, y): """Project reversed y""" if y is None: return None return (self.height * (y - self.box.ymin) / self.box.height) class HorizontalView(View): """Same as view but transposed""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self._force_vertical = None self.width = width self.height = height self.box = box self.box.fix() self.box.swap() def x(self, x): """Project x as y""" if x is None: return None if self._force_vertical: return super(HorizontalView, self).x(x) return super(HorizontalView, self).y(x) def y(self, y): """Project y as x""" if y is None: return None if self._force_vertical: return super(HorizontalView, self).y(y) return super(HorizontalView, self).x(y) class PolarView(View): """Polar projection for pie like graphs""" def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta return super(PolarView, self).__call__( (rho * cos(theta), rho * sin(theta))) class PolarLogView(View): """Logarithmic polar projection""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" super(PolarLogView, self).__init__(width, height, box) if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') self.log10_rmax = log10(self.box._rmax) self.log10_rmin = log10(self.box._rmin) if self.log10_rmin == self.log10_rmax: self.log10_rmax = self.log10_rmin + 1 def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta # Center case if rho == 0: return super(PolarLogView, self).__call__((0, 0)) rho = (self.box._rmax - self.box._rmin) * ( log10(rho) - self.log10_rmin) / ( self.log10_rmax - self.log10_rmin) return super(PolarLogView, self).__call__( (rho * cos(theta), rho * sin(theta))) class PolarThetaView(View): """Logarithmic polar projection""" def __init__(self, width, height, box, aperture=pi / 3): """Create the view with a width an height and a box bounds""" super(PolarThetaView, self).__init__(width, height, box) self.aperture = aperture if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta start = 3 * pi / 2 + self.aperture / 2 theta = start + (2 * pi - self.aperture) * ( theta - self.box._tmin) / ( self.box._tmax - self.box._tmin) return super(PolarThetaView, self).__call__( (rho * cos(theta), rho * sin(theta))) class PolarThetaLogView(View): """Logarithmic polar projection""" def __init__(self, width, height, box, aperture=pi / 3): """Create the view with a width an height and a box bounds""" super(PolarThetaLogView, self).__init__(width, height, box) self.aperture = aperture if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0 self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0 if self.log10_tmin == self.log10_tmax: self.log10_tmax = self.log10_tmin + 1 def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta # Center case if theta == 0: return super(PolarThetaLogView, self).__call__((0, 0)) theta = self.box._tmin + (self.box._tmax - self.box._tmin) * ( log10(theta) - self.log10_tmin) / ( self.log10_tmax - self.log10_tmin) start = 3 * pi / 2 + self.aperture / 2 theta = start + (2 * pi - self.aperture) * ( theta - self.box._tmin) / ( self.box._tmax - self.box._tmin) return super(PolarThetaLogView, self).__call__( (rho * cos(theta), rho * sin(theta))) class LogView(View): """Y Logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 if self.log10_ymin == self.log10_ymax: self.log10_ymax = self.log10_ymin + 1 self.box.fix(False) def y(self, y): """Project y""" if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0: return 0 return (self.height - self.height * (log10(y) - self.log10_ymin) / ( self.log10_ymax - self.log10_ymin)) class XLogView(View): """X logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 self.box.fix(False) def x(self, x): """Project x""" if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0: return None return (self.width * (log10(x) - self.log10_xmin) / (self.log10_xmax - self.log10_xmin)) class XYLogView(XLogView, LogView): """X and Y logarithmic projection""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 self.box.fix(False) class HorizontalLogView(XLogView): """Transposed Logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self._force_vertical = None self.width = width self.height = height self.box = box self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0 if self.log10_xmin == self.log10_xmax: self.log10_xmax = self.log10_xmin + 1 self.box.fix(False) self.box.swap() def x(self, x): """Project x as y""" if x is None: return None if self._force_vertical: return super(HorizontalLogView, self).x(x) return super(XLogView, self).y(x) def y(self, y): """Project y as x""" if y is None: return None if self._force_vertical: return super(XLogView, self).y(y) return super(HorizontalLogView, self).x(y) pygal-2.4.0/pygal/css/0000755000175000017500000000000013127127737014527 5ustar zerozero00000000000000pygal-2.4.0/pygal/css/graph.css0000644000175000017500000000535212663620674016351 0ustar zerozero00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library 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. * * This library 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 pygal. If not, see . */ {{ id }}text.no_data { text-anchor: middle; } {{ id }}.guide.line { fill: none; } {{ id }}.centered { text-anchor: middle; } {{ id }}.title { text-anchor: middle; } {{ id }}.legends .legend text { fill-opacity: 1; } {{ id }}.axis.x text { text-anchor: middle; } {{ id }}.axis.x:not(.web) text[transform] { text-anchor: start; } {{ id }}.axis.x:not(.web) text[transform].backwards { text-anchor: end; } {{ id }}.axis.y text { text-anchor: end; } {{ id }}.axis.y text[transform].backwards { text-anchor: start; } {{ id }}.axis.y2 text { text-anchor: start; } {{ id }}.axis.y2 text[transform].backwards { text-anchor: end; } {{ id }}.axis .guide.line { stroke-dasharray: {{ style.guide_stroke_dasharray }}; } {{ id }}.axis .major.guide.line { stroke-dasharray: {{ style.major_guide_stroke_dasharray }}; } {{ id }}.horizontal .axis.y .guide.line, {{ id }}.horizontal .axis.y2 .guide.line, {{ id }}.vertical .axis.x .guide.line { opacity: 0; } {{ id }}.horizontal .axis.always_show .guide.line, {{ id }}.vertical .axis.always_show .guide.line { opacity: 1 !important; } {{ id }}.axis.y .guides:hover .guide.line, {{ id }}.axis.y2 .guides:hover .guide.line, {{ id }}.axis.x .guides:hover .guide.line { opacity: 1; } {{ id }}.axis .guides:hover text { opacity: 1; } {{ id }}.nofill { fill: none; } {{ id }}.subtle-fill { fill-opacity: .2; } {{ id }}.dot { stroke-width: 1px; fill-opacity: 1; } {{ id }}.dot.active { stroke-width: 5px; } {{ id }}.dot.negative { fill: transparent; } {{ id }} text, {{ id }} tspan { stroke: none !important; } {{ id }}.series text.active { opacity: 1; } {{ id }}.tooltip rect { fill-opacity: .95; stroke-width: .5; } {{ id }}.tooltip text { fill-opacity: 1; } {{ id }}.showable { visibility: hidden; } {{ id }}.showable.shown { visibility: visible; } {{ id }}.gauge-background { fill: {{ style.value_background }}; stroke: none; } {{ id }}.bg-lines { stroke: {{ style.background }}; stroke-width: 2px; } pygal-2.4.0/pygal/css/base.css0000644000175000017500000000361012555442120016141 0ustar zerozero00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library 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. * * This library 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 pygal. If not, see . */ /* * Font-sizes from config, override with care */ {{ id }} { -webkit-user-select: none; -webkit-font-smoothing: antialiased; font-family: {{ style.font_family }}; } {{ id }}.title { font-family: {{ style.title_font_family }}; font-size: {{ style.title_font_size }}px; } {{ id }}.legends .legend text { font-family: {{ style.legend_font_family }}; font-size: {{ style.legend_font_size }}px; } {{ id }}.axis text { font-family: {{ style.label_font_family }}; font-size: {{ style.label_font_size }}px; } {{ id }}.axis text.major { font-family: {{ style.major_label_font_family }}; font-size: {{ style.major_label_font_size }}px; } {{ id }}.text-overlay text.value { font-family: {{ style.value_font_family }}; font-size: {{ style.value_font_size }}px; } {{ id }}.text-overlay text.label { font-family: {{ style.value_label_font_family }}; font-size: {{ style.value_label_font_size }}px; } {{ id }}.tooltip { font-family: {{ style.tooltip_font_family }}; font-size: {{ style.tooltip_font_size }}px; } {{ id }}text.no_data { font-family: {{ style.no_data_font_family }}; font-size: {{ style.no_data_font_size }}px; } pygal-2.4.0/pygal/css/style.css0000644000175000017500000000701212723243315016371 0ustar zerozero00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library 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. * * This library 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 pygal. If not, see . */ /* * Styles from config */ {{ id }}{ background-color: {{ style.background }}; } {{ id }}path, {{ id }}line, {{ id }}rect, {{ id }}circle { -webkit-transition: {{ style.transition }}; -moz-transition: {{ style.transition }}; transition: {{ style.transition }}; } {{ id }}.graph > .background { fill: {{ style.background }}; } {{ id }}.plot > .background { fill: {{ style.plot_background }}; } {{ id }}.graph { fill: {{ style.foreground }}; } {{ id }}text.no_data { fill: {{ style.foreground_strong }}; } {{ id }}.title { fill: {{ style.foreground_strong }}; } {{ id }}.legends .legend text { fill: {{ style.foreground }}; } {{ id }}.legends .legend:hover text { fill: {{ style.foreground_strong }}; } {{ id }}.axis .line { stroke: {{ style.foreground_strong }}; } {{ id }}.axis .guide.line { stroke: {{ style.foreground_subtle }}; } {{ id }}.axis .major.line { stroke: {{ style.foreground }}; } {{ id }}.axis text.major { fill: {{ style.foreground_strong }}; } {{ id }}.axis.y .guides:hover .guide.line, {{ id }}.line-graph .axis.x .guides:hover .guide.line, {{ id }}.stackedline-graph .axis.x .guides:hover .guide.line, {{ id }}.xy-graph .axis.x .guides:hover .guide.line { stroke: {{ style.foreground_strong }}; } {{ id }}.axis .guides:hover text { fill: {{ style.foreground_strong }}; } {{ id }}.reactive { fill-opacity: {{ style.opacity }}; stroke-opacity: {{ style.stroke_opacity }}; } {{ id }}.ci { stroke: {{ style.foreground }}; } {{ id }}.reactive.active, {{ id }}.active .reactive { fill-opacity: {{ style.opacity_hover }}; stroke-opacity: {{ style.stroke_opacity_hover }}; stroke-width: 4; } {{ id }}.ci .reactive.active { stroke-width: 1.5; } {{ id }}.series text { fill: {{ style.foreground_strong }}; } {{ id }}.tooltip rect { fill: {{ style.plot_background }}; stroke: {{ style.foreground_strong }}; -webkit-transition: opacity {{ style.transition }}; -moz-transition: opacity {{ style.transition }}; transition: opacity {{ style.transition }}; } {{ id }}.tooltip .label { fill: {{ style.foreground }}; } {{ id }}.tooltip .label { fill: {{ style.foreground }}; } {{ id }}.tooltip .legend { font-size: .8em; fill: {{ style.foreground_subtle }}; } {{ id }}.tooltip .x_label { font-size: .6em; fill: {{ style.foreground_strong }}; } {{ id }}.tooltip .xlink { font-size: .5em; text-decoration: underline; } {{ id }}.tooltip .value { font-size: 1.5em; } {{ id }}.bound { font-size: .5em; } {{ id }}.max-value { font-size: .75em; fill: {{ style.foreground_subtle }}; } {{ id }}.map-element { fill: {{ style.plot_background }}; stroke: {{ style.foreground_subtle }} !important; } {{ id }}.map-element .reactive { fill-opacity: inherit; stroke-opacity: inherit; } {{ colors }} {{ strokes }} pygal-2.4.0/pygal/formatters.py0000644000175000017500000000574613114033322016471 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Formatters to use with `value_formatter` and `x_value_formatter` configs """ from __future__ import division from datetime import date, datetime, time from math import floor, log from pygal._compat import to_str, u from pygal.util import float_format class Formatter(object): pass class HumanReadable(Formatter): """Format a number to engineer scale""" ORDERS = u("yzafpnµm kMGTPEZY") def __init__(self, none_char=u('∅')): self.none_char = none_char def __call__(self, val): if val is None: return self.none_char order = val and int(floor(log(abs(val)) / log(1000))) orders = self.ORDERS.split(" ")[int(order > 0)] if order == 0 or order > len(orders): return float_format(val / (1000 ** int(order))) return ( float_format(val / (1000 ** int(order))) + orders[int(order) - int(order > 0)]) class Significant(Formatter): """Show precision significant digit of float""" def __init__(self, precision=10): self.format = '%%.%dg' % precision def __call__(self, val): if val is None: return '' return self.format % val class Integer(Formatter): """Cast number to integer""" def __call__(self, val): if val is None: return '' return '%d' % val class Raw(Formatter): """Cast everything to string""" def __call__(self, val): if val is None: return '' return to_str(val) class IsoDateTime(Formatter): """Iso format datetimes""" def __call__(self, val): if val is None: return '' return val.isoformat() class Default(Significant, IsoDateTime, Raw): """Try to guess best format from type""" def __call__(self, val): if val is None: return '' if isinstance(val, (int, float)): return Significant.__call__(self, val) if isinstance(val, (date, time, datetime)): return IsoDateTime.__call__(self, val) return Raw.__call__(self, val) # Formatters with default options human_readable = HumanReadable() significant = Significant() integer = Integer() raw = Raw() # Default config formatter default = Default() pygal-2.4.0/pygal/state.py0000644000175000017500000000235213114033322015411 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Class holding state during render""" from pygal.util import merge class State(object): """ Class containing config values overriden by chart values overriden by keyword args """ def __init__(self, graph, **kwargs): """Create the transient state""" merge(self.__dict__, graph.config.__class__.__dict__) merge(self.__dict__, graph.config.__dict__) merge(self.__dict__, graph.__dict__) merge(self.__dict__, kwargs) pygal-2.4.0/pygal/adapters.py0000644000175000017500000000261713114033322016100 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Value adapters to use when a chart doesn't accept all value types""" from decimal import Decimal from pygal._compat import is_str def positive(x): """Return zero if value is negative""" if x is None: return if is_str(x): return x if x < 0: return 0 return x def not_zero(x): """Return None if value is zero""" if x == 0: return return x def none_to_zero(x): """Return 0 if value is None""" if x is None: return 0 return x def decimal_to_float(x): """Cast Decimal values to float""" if isinstance(x, Decimal): return float(x) return x pygal-2.4.0/pygal/table.py0000644000175000017500000001344713114033322015367 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ HTML Table maker. This class is used to render an html table from a chart data. """ import uuid from lxml.html import builder, tostring from pygal.util import template class HTML(object): """Lower case adapter of lxml builder""" def __getattribute__(self, attr): """Get the uppercase builder attribute""" return getattr(builder, attr.upper()) class Table(object): """Table generator class""" _dual = None def __init__(self, chart): """Init the table""" self.chart = chart def render(self, total=False, transpose=False, style=False): """Render the HTMTL table of the chart. `total` can be specified to include data sums `transpose` make labels becomes columns `style` include scoped style for the table """ self.chart.setup() ln = self.chart._len html = HTML() attrs = {} if style: attrs['id'] = 'table-%s' % uuid.uuid4() table = [] _ = lambda x: x if x is not None else '' if self.chart.x_labels: labels = [None] + list(self.chart.x_labels) if len(labels) < ln: labels += [None] * (ln + 1 - len(labels)) if len(labels) > ln + 1: labels = labels[:ln + 1] table.append(labels) if total: if len(table): table[0].append('Total') else: table.append([None] * (ln + 1) + ['Total']) acc = [0] * (ln + 1) for i, serie in enumerate(self.chart.all_series): row = [serie.title] if total: sum_ = 0 for j, value in enumerate(serie.values): if total: v = value or 0 acc[j] += v sum_ += v row.append(self.chart._format(serie, j)) if total: acc[-1] += sum_ row.append(self.chart._serie_format(serie, sum_)) table.append(row) width = ln + 1 if total: width += 1 table.append(['Total']) for val in acc: table[-1].append(self.chart._serie_format(serie, val)) # Align values len_ = max([len(r) for r in table] or [0]) for i, row in enumerate(table[:]): len_ = len(row) if len_ < width: table[i] = row + [None] * (width - len_) if not transpose: table = list(zip(*table)) thead = [] tbody = [] tfoot = [] if not transpose or self.chart.x_labels: # There's always series title but not always x_labels thead = [table[0]] tbody = table[1:] else: tbody = table if total: tfoot = [tbody[-1]] tbody = tbody[:-1] parts = [] if thead: parts.append( html.thead( *[html.tr( *[html.th(_(col)) for col in r] ) for r in thead] ) ) if tbody: parts.append( html.tbody( *[html.tr( *[html.td(_(col)) for col in r] ) for r in tbody] ) ) if tfoot: parts.append( html.tfoot( *[html.tr( *[html.th(_(col)) for col in r] ) for r in tfoot] ) ) table = tostring( html.table( *parts, **attrs ) ) if style: if style is True: css = ''' #{{ id }} { border-collapse: collapse; border-spacing: 0; empty-cells: show; border: 1px solid #cbcbcb; } #{{ id }} td, #{{ id }} th { border-left: 1px solid #cbcbcb; border-width: 0 0 0 1px; margin: 0; padding: 0.5em 1em; } #{{ id }} td:first-child, #{{ id }} th:first-child { border-left-width: 0; } #{{ id }} thead, #{{ id }} tfoot { color: #000; text-align: left; vertical-align: bottom; } #{{ id }} thead { background: #e0e0e0; } #{{ id }} tfoot { background: #ededed; } #{{ id }} tr:nth-child(2n-1) td { background-color: #f2f2f2; } ''' else: css = style table = tostring(html.style( template(css, **attrs), scoped='scoped')) + table table = table.decode('utf-8') self.chart.teardown() return table pygal-2.4.0/pygal/serie.py0000644000175000017500000000256513114033322015406 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Serie property holder""" from pygal.util import cached_property class Serie(object): """Serie class containing title, values and the graph serie index""" def __init__(self, index, values, config, metadata=None): """Create the serie with its options""" self.index = index self.values = values self.config = config self.__dict__.update(config.__dict__) self.metadata = metadata or {} @cached_property def safe_values(self): """Property containing all values that are not None""" return list(filter(lambda x: x is not None, self.values)) pygal-2.4.0/pygal/test/0000755000175000017500000000000013127127737014716 5ustar zerozero00000000000000pygal-2.4.0/pygal/test/test_config.py0000644000175000017500000004116513114033322017561 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Various config options tested on one chart type or more""" from tempfile import NamedTemporaryFile from pygal import ( XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge, Histogram, HorizontalBar, HorizontalLine, HorizontalStackedBar, HorizontalStackedLine, Line, Pie, Pyramid, Radar, SolidGauge, TimeDeltaLine, TimeLine, Treemap, formatters) from pygal._compat import _ellipsis, u from pygal.graph.dual import Dual from pygal.graph.horizontal import HorizontalGraph from pygal.graph.map import BaseMap from pygal.test.utils import texts def test_config_behaviours(): """Test that all different way to set config produce same results""" line1 = Line() line1.show_legend = False line1.fill = True line1.pretty_print = True line1.no_prefix = True line1.x_labels = ['a', 'b', 'c'] line1.add('_', [1, 2, 3]) l1 = line1.render() q = line1.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 1 assert len(q(".legend")) == 0 assert len(q(".x.axis .guides")) == 3 assert len(q(".y.axis .guides")) == 11 assert len(q(".dots")) == 3 assert q(".axis.x text").map(texts) == ['a', 'b', 'c'] line2 = Line( show_legend=False, fill=True, pretty_print=True, no_prefix=True, x_labels=['a', 'b', 'c']) line2.add('_', [1, 2, 3]) l2 = line2.render() assert l1 == l2 class LineConfig(Config): show_legend = False fill = True pretty_print = True no_prefix = True x_labels = ['a', 'b', 'c'] line3 = Line(LineConfig) line3.add('_', [1, 2, 3]) l3 = line3.render() assert l1 == l3 line4 = Line(LineConfig()) line4.add('_', [1, 2, 3]) l4 = line4.render() assert l1 == l4 line_config = Config() line_config.show_legend = False line_config.fill = True line_config.pretty_print = True line_config.no_prefix = True line_config.x_labels = ['a', 'b', 'c'] line5 = Line(line_config) line5.add('_', [1, 2, 3]) l5 = line5.render() assert l1 == l5 l6 = Line(line_config)(1, 2, 3, title='_').render() assert l1 == l6 def test_config_alterations_class(): """Assert a config can be changed on config class""" class LineConfig(Config): no_prefix = True show_legend = False fill = True pretty_print = True x_labels = ['a', 'b', 'c'] line1 = Line(LineConfig) line1.add('_', [1, 2, 3]) l1 = line1.render() LineConfig.stroke = False line2 = Line(LineConfig) line2.add('_', [1, 2, 3]) l2 = line2.render() assert l1 != l2 l1bis = line1.render() assert l1 == l1bis def test_config_alterations_instance(): """Assert a config can be changed on instance""" class LineConfig(Config): no_prefix = True show_legend = False fill = True pretty_print = True x_labels = ['a', 'b', 'c'] config = LineConfig() line1 = Line(config) line1.add('_', [1, 2, 3]) l1 = line1.render() config.stroke = False line2 = Line(config) line2.add('_', [1, 2, 3]) l2 = line2.render() assert l1 != l2 l1bis = line1.render() assert l1 == l1bis def test_config_alterations_kwargs(): """Assert a config can be changed with keyword args""" class LineConfig(Config): no_prefix = True show_legend = False fill = True pretty_print = True x_labels = ['a', 'b', 'c'] config = LineConfig() line1 = Line(config) line1.add('_', [1, 2, 3]) l1 = line1.render() line1.stroke = False l1bis = line1.render() assert l1 != l1bis line2 = Line(config) line2.add('_', [1, 2, 3]) l2 = line2.render() assert l1 == l2 assert l1bis != l2 line3 = Line(config, title='Title') line3.add('_', [1, 2, 3]) l3 = line3.render() assert l3 != l2 l2bis = line2.render() assert l2 == l2bis def test_logarithmic(): """Test logarithmic option""" line = Line(logarithmic=True) line.add('_', [1, 10 ** 10, 1]) q = line.render_pyquery() assert len(q(".axis.x")) == 0 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 1 assert len(q(".legend")) == 1 assert len(q(".x.axis .guides")) == 0 assert len(q(".y.axis .guides")) == 21 assert len(q(".dots")) == 3 def test_interpolation(Chart): """Test interpolation option""" chart = Chart(interpolate='cubic') chart.add('1', [1, 3, 12, 3, 4]) chart.add('2', [7, -4, 10, None, 8, 3, 1]) q = chart.render_pyquery() assert len(q(".legend")) == 2 def test_no_data_interpolation(Chart): """Test interpolation option with no data""" chart = Chart(interpolate='cubic') q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_empty_serie_interpolation(Chart): """Test interpolation option with an empty serie""" chart = Chart(interpolate='cubic') chart.add('Serie', []) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_logarithmic_bad_interpolation(): """Test interpolation option with a logarithmic chart""" line = Line(logarithmic=True, interpolate='cubic') line.add('_', [.001, .00000001, 1]) q = line.render_pyquery() assert len(q(".y.axis .guides")) == 41 def test_logarithmic_big_scale(): """Test logarithmic option with a large range of value""" line = Line(logarithmic=True) line.add('_', [10 ** -10, 10 ** 10, 1]) q = line.render_pyquery() assert len(q(".y.axis .guides")) == 21 def test_value_formatter(): """Test value formatter option""" line = Line(value_formatter=lambda x: str(x) + u('‰')) line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4]) q = line.render_pyquery() assert len(q(".y.axis .guides")) == 11 assert q(".axis.y text").map(texts) == list(map( lambda x: str(x) + u('‰'), map(float, range(20000, 240000, 20000)))) def test_logarithmic_small_scale(): """Test logarithmic with a small range of values""" line = Line(logarithmic=True) line.add('_', [1 + 10 ** 10, 3 + 10 ** 10, 2 + 10 ** 10]) q = line.render_pyquery() assert len(q(".y.axis .guides")) == 11 def test_human_readable(): """Test human readable option""" line = Line() line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4]) q = line.render_pyquery() assert q(".axis.y text").map(texts) == list(map( str, range(20000, 240000, 20000))) line.value_formatter = formatters.human_readable q = line.render_pyquery() assert q(".axis.y text").map(texts) == list(map( lambda x: '%dk' % x, range(20, 240, 20))) def test_show_legend(): """Test show legend option""" line = Line() line.add('_', [1, 2, 3]) q = line.render_pyquery() assert len(q(".legend")) == 1 line.show_legend = False q = line.render_pyquery() assert len(q(".legend")) == 0 def test_show_dots(): """Test show dots option""" line = Line() line.add('_', [1, 2, 3]) q = line.render_pyquery() assert len(q(".dots")) == 3 line.show_dots = False q = line.render_pyquery() assert len(q(".dots")) == 0 def test_no_data(): """Test no data and no data text option""" line = Line() q = line.render_pyquery() assert q(".text-overlay text").text() == "No data" line.no_data_text = u("þæ®þ怀&ij¿’€") q = line.render_pyquery() assert q(".text-overlay text").text() == u("þæ®þ怀&ij¿’€") def test_include_x_axis(Chart): """Test x axis inclusion option""" chart = Chart() if Chart in ( Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box, SolidGauge ) or issubclass(Chart, BaseMap): return if not chart._dual: data = 100, 200, 150 else: data = (1, 100), (3, 200), (2, 150) chart.add('_', data) q = chart.render_pyquery() # Ghost thing yaxis = ".axis.%s .guides text" % ( 'y' if not getattr(chart, 'horizontal', False) else 'x') if not isinstance(chart, Bar): assert '0' not in q(yaxis).map(texts) else: assert '0' in q(yaxis).map(texts) chart.include_x_axis = True q = chart.render_pyquery() assert '0' in q(yaxis).map(texts) def test_css(Chart): """Test css file option""" css = "{{ id }}text { fill: #bedead; }\n" with NamedTemporaryFile('w') as f: f.write(css) f.flush() config = Config() config.css.append('file://' + f.name) chart = Chart(config) chart.add('/', [10, 1, 5]) svg = chart.render().decode('utf-8') assert '#bedead' in svg chart = Chart(css=(_ellipsis, 'file://' + f.name)) chart.add('/', [10, 1, 5]) svg = chart.render().decode('utf-8') assert '#bedead' in svg def test_inline_css(Chart): """Test inline css option""" css = "{{ id }}text { fill: #bedead; }\n" config = Config() config.css.append('inline:' + css) chart = Chart(config) chart.add('/', [10, 1, 5]) svg = chart.render().decode('utf-8') assert '#bedead' in svg def test_meta_config(): """Test config metaclass""" from pygal.config import CONFIG_ITEMS assert all(c.name != 'Unbound' for c in CONFIG_ITEMS) def test_label_rotation(Chart): """Test label rotation option""" chart = Chart(x_label_rotation=28, y_label_rotation=76) chart.add('1', [4, -5, 123, 59, 38]) chart.add('2', [89, 0, 8, .12, 8]) if not chart._dual: chart.x_labels = ['one', 'twoooooooooooooooooooooo', 'three', '4'] q = chart.render_pyquery() if Chart in (Line, Bar): assert len(q('.axis.x text[transform^="rotate(28"]')) == 4 assert len(q('.axis.y text[transform^="rotate(76"]')) == 13 def test_legend_at_bottom(Chart): """Test legend at bottom option""" chart = Chart(legend_at_bottom=True) chart.add('1', [4, -5, 123, 59, 38]) chart.add('2', [89, 0, 8, .12, 8]) lab = chart.render() chart.legend_at_bottom = False assert lab != chart.render() def test_x_y_title(Chart): """Test x title and y title options""" chart = Chart(title='I Am A Title', x_title="I am a x title", y_title="I am a y title") chart.add('1', [4, -5, 123, 59, 38]) chart.add('2', [89, 0, 8, .12, 8]) q = chart.render_pyquery() assert len(q('.titles .title')) == 3 def test_range(Chart): """Test y label major option""" if Chart in ( Pie, Treemap, Dot, SolidGauge ) or issubclass(Chart, BaseMap): return chart = Chart() chart.range = (0, 100) chart.add('', [1, 2, 10]) q = chart.render_pyquery() axis = map(str, range(0, 101, 10)) if Chart == Radar: axis = map(str, range(100, -1, -20)) z = 'x' if getattr(chart, 'horizontal', False) or Chart == Gauge else 'y' assert [t.text for t in q('.axis.%s .guides text' % z)] == list(axis) def test_x_label_major(Chart): """Test x label major option""" if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge, Pyramid, DateTimeLine, TimeLine, DateLine, TimeDeltaLine ) or issubclass(Chart, (BaseMap, Dual, HorizontalGraph)): return chart = Chart() chart.add('test', range(12)) chart.x_labels = map(str, range(12)) q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 0 chart.x_labels_major = ['1', '5', '11', '1.0', '5.0', '11.0'] q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 3 assert len(q(".axis.x text")) == 12 chart.show_minor_x_labels = False q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 3 assert len(q(".axis.x text")) == 3 chart.show_minor_x_labels = True chart.x_labels_major = None chart.x_labels_major_every = 2 q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 6 assert len(q(".axis.x text")) == 12 chart.x_labels_major_every = None chart.x_labels_major_count = 4 q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 4 assert len(q(".axis.x text")) == 12 chart.x_labels_major_every = None chart.x_labels_major_count = 78 q = chart.render_pyquery() assert len(q(".axis.x text.major")) == 12 assert len(q(".axis.x text")) == 12 def test_y_label_major(Chart): """Test y label major option""" if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge, HorizontalBar, HorizontalStackedBar, HorizontalStackedLine, HorizontalLine, Pyramid, DateTimeLine, TimeLine, DateLine, TimeDeltaLine ) or issubclass(Chart, BaseMap): return chart = Chart() data = range(12) if Chart == XY: data = list(zip(*[range(12), range(12)])) chart.add('test', data) chart.y_labels = range(12) q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 3 chart.y_labels_major = [1.0, 5.0, 11.0] q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 3 assert len(q(".axis.y text")) == 12 chart.show_minor_y_labels = False q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 3 assert len(q(".axis.y text")) == 3 chart.show_minor_y_labels = True chart.y_labels_major = None chart.y_labels_major_every = 2 q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 6 assert len(q(".axis.y text")) == 12 chart.y_labels_major_every = None chart.y_labels_major_count = 4 q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 4 assert len(q(".axis.y text")) == 12 chart.y_labels_major_every = None chart.y_labels_major_count = 78 q = chart.render_pyquery() assert len(q(".axis.y text.major")) == 12 assert len(q(".axis.y text")) == 12 def test_no_y_labels(Chart): """Test no y labels chart""" chart = Chart() chart.y_labels = [] chart.add('_', [1, 2, 3]) chart.add('?', [10, 21, 5]) assert chart.render_pyquery() def test_fill(Chart): """Test fill option""" chart = Chart(fill=True) chart.add('_', [1, 2, 3]) chart.add('?', [10, 21, 5]) assert chart.render_pyquery() def test_render_data_uri(Chart): """Test the render data uri""" chart = Chart(fill=True) chart.add(u('ééé'), [1, 2, 3]) chart.add(u('èèè'), [10, 21, 5]) assert chart.render_data_uri().startswith( 'data:image/svg+xml;charset=utf-8;base64,') def test_formatters(Chart): """Test custom formatters""" if Chart._dual or Chart == Box: return chart = Chart( formatter=lambda x, chart, serie: '%s%s$' % (x, serie.title)) chart.add('_a', [1, 2, {'value': 3, 'formatter': lambda x: u('%s¥') % x}]) chart.add('_b', [4, 5, 6], formatter=lambda x: u('%s€') % x) chart.x_labels = [2, 4, 6] chart.x_labels_major = [4] q = chart.render_pyquery() assert set([v.text for v in q(".value")]) == set(( u('4€'), u('5€'), u('6€'), '1_a$', '2_a$', u('3¥')) + ( ('6_a$', u('15€')) if Chart in (Pie, SolidGauge) else ())) def test_classes(Chart): """Test classes option""" chart = Chart() assert chart.render_pyquery().attr('class') == 'pygal-chart' chart = Chart(classes=()) assert not chart.render_pyquery().attr('class') chart = Chart(classes=(_ellipsis,)) assert chart.render_pyquery().attr('class') == 'pygal-chart' chart = Chart(classes=('graph',)) assert chart.render_pyquery().attr('class') == 'graph' chart = Chart(classes=('pygal-chart', 'graph')) assert chart.render_pyquery().attr('class') == 'pygal-chart graph' chart = Chart(classes=(_ellipsis, 'graph')) assert chart.render_pyquery().attr('class') == 'pygal-chart graph' chart = Chart(classes=('graph', _ellipsis)) assert chart.render_pyquery().attr('class') == 'graph pygal-chart' pygal-2.4.0/pygal/test/test_stacked.py0000644000175000017500000000412113114033322017721 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Stacked chart related tests""" from pygal import StackedLine def test_stacked_line(): """Test stacked line""" stacked = StackedLine() stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)')) def test_stacked_line_reverse(): """Test stack from top stacked line""" stacked = StackedLine(stack_from_top=True) stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('11 (+1)', '14 (+2)', '10', '12')) def test_stacked_line_log(): """Test logarithmic stacked line""" stacked = StackedLine(logarithmic=True) stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)')) def test_stacked_line_interpolate(): """Test interpolated stacked line""" stacked = StackedLine(interpolate='cubic') stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)')) pygal-2.4.0/pygal/test/test_line.py0000644000175000017500000001235613114033322017243 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Line chart related tests""" from __future__ import division from math import cos, sin from pygal import Line from pygal.test.utils import texts def test_simple_line(): """Simple line test""" line = Line() rng = range(-30, 31, 5) line.add('test1', [cos(x / 10) for x in rng]) line.add('test2', [sin(x / 10) for x in rng]) line.add('test3', [cos(x / 10) - sin(x / 10) for x in rng]) line.x_labels = map(str, rng) line.title = "cos sin and cos - sin" q = line.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 3 assert len(q(".legend")) == 3 assert len(q(".x.axis .guides")) == 13 assert len(q(".y.axis .guides")) == 13 assert len(q(".dots")) == 3 * 13 assert q(".axis.x text").map(texts) == [ '-30', '-25', '-20', '-15', '-10', '-5', '0', '5', '10', '15', '20', '25', '30'] assert q(".axis.y text").map(texts) == [ '-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6', '0.8', '1', '1.2'] assert q(".title").text() == 'cos sin and cos - sin' assert q(".legend text").map(texts) == ['test1', 'test2', 'test3'] def test_line(): """Another simple line test""" line = Line() rng = [8, 12, 23, 73, 39, 57] line.add('Single serie', rng) line.title = "One serie" q = line.render_pyquery() assert len(q(".axis.x")) == 0 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 1 assert len(q(".x.axis .guides")) == 0 assert len(q(".y.axis .guides")) == 7 def test_one_dot(): """Line test with an unique value""" line = Line() line.add('one dot', [12]) line.x_labels = ['one'] q = line.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".y.axis .guides")) == 1 def test_no_dot(): """Line test with an empty serie""" line = Line() line.add('no dot', []) q = line.render_pyquery() assert q(".text-overlay text").text() == 'No data' def test_no_dot_at_all(): """Line test with no value""" q = Line().render_pyquery() assert q(".text-overlay text").text() == 'No data' def test_not_equal_x_labels(): """Test x_labels""" line = Line() line.add('test1', range(100)) line.truncate_label = -1 line.x_labels = map(str, range(11)) q = line.render_pyquery() assert len(q(".dots")) == 100 assert len(q(".axis.x")) == 1 assert q(".axis.x text").map(texts) == [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] def test_int_x_labels(): """Test x_labels""" line = Line() line.add('test1', range(100)) line.truncate_label = -1 line.x_labels = list(range(11)) q = line.render_pyquery() assert len(q(".dots")) == 100 assert len(q(".axis.x")) == 1 assert q(".axis.x text").map(texts) == [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] def test_only_major_dots_every(): """Test major dots""" line = Line(show_only_major_dots=True, x_labels_major_every=3) line.add('test', range(12)) line.x_labels = map(str, range(12)) q = line.render_pyquery() assert len(q(".dots")) == 4 def test_only_major_dots_no_labels(): """Test major dots with no labels""" line = Line(show_only_major_dots=True) line.add('test', range(12)) q = line.render_pyquery() assert len(q(".dots")) == 12 def test_only_major_dots_count(): """Test major dots with a major label count""" line = Line(show_only_major_dots=True) line.add('test', range(12)) line.x_labels = map(str, range(12)) line.x_labels_major_count = 2 q = line.render_pyquery() assert len(q(".dots")) == 2 def test_only_major_dots(): """Test major dots with specified major labels""" line = Line(show_only_major_dots=True,) line.add('test', range(12)) line.x_labels = map(str, range(12)) line.x_labels_major = ['1', '5', '11'] q = line.render_pyquery() assert len(q(".dots")) == 3 def test_line_secondary(): """Test line with a secondary serie""" line = Line() rng = [8, 12, 23, 73, 39, 57] line.add('First serie', rng) line.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True) line.title = "One serie" q = line.render_pyquery() assert len(q(".axis.x")) == 0 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 2 assert len(q(".x.axis .guides")) == 0 assert len(q(".y.axis .guides")) == 7 pygal-2.4.0/pygal/test/test_histogram.py0000644000175000017500000000215713114033322020307 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Histogram chart related tests""" from pygal import Histogram def test_histogram(): """Simple histogram test""" hist = Histogram() hist.add('1', [ (2, 0, 1), (4, 1, 3), (3, 3.5, 5), (1.5, 5, 10) ]) hist.add('2', [(2, 2, 8)], secondary=True) q = hist.render_pyquery() assert len(q('.rect')) == 5 pygal-2.4.0/pygal/test/test_maps.py0000644000175000017500000000211213114033322017241 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Map plugins tests are imported here""" import pkg_resources # Load plugins tests for entry in pkg_resources.iter_entry_points('pygal.test.test_maps'): module = entry.load() for k, v in module.__dict__.items(): if k.startswith('test_'): globals()['test_maps_' + entry.name + '_' + k[5:]] = v pygal-2.4.0/pygal/test/test_bar.py0000644000175000017500000000230513114033322017051 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Bar chart related tests""" from pygal import Bar def test_simple_bar(): """Simple bar test""" bar = Bar() rng = [-3, -32, -39] bar.add('test1', rng) bar.add('test2', map(abs, rng)) bar.x_labels = map(str, rng) bar.title = "Bar test" q = bar.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".legend")) == 2 assert len(q(".plot .series rect")) == 2 * 3 pygal-2.4.0/pygal/test/utils.py0000644000175000017500000000163413114033322016412 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Tests helpers""" from pyquery import PyQuery as pq def texts(i, e): """Helper for getting the text of an element""" return pq(e).text() pygal-2.4.0/pygal/test/test_colors.py0000644000175000017500000005301013114033322017605 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Color utility functions tests""" from __future__ import division from pygal.colors import ( darken, desaturate, hsl_to_rgb, lighten, parse_color, rgb_to_hsl, rotate, saturate, unparse_color) def test_parse_color(): """Test color parse function""" 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(): """Test color unparse function""" 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(): """Test darken color function""" 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(): """Test lighten color function""" 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' assert lighten('#121212', 1) == '#151515' assert lighten('#999999', 100) == '#ffffff' assert lighten('#1479ac', 8) == '#1893d1' def test_saturate(): """Test color saturation function""" assert saturate('#000', 20) == '#000' assert saturate('#fff', 20) == '#fff' assert saturate('#8a8', 100) == '#3f3' assert saturate('#855', 20) == '#9e3f3f' def test_desaturate(): """Test color desaturation function""" assert desaturate('#000', 20) == '#000' assert desaturate('#fff', 20) == '#fff' assert desaturate('#8a8', 100) == '#999' assert desaturate('#855', 20) == '#726b6b' def test_rotate(): """Test color rotation function""" assert rotate('#000', 45) == '#000' assert rotate('#fff', 45) == '#fff' assert rotate('#811', 45) == '#886a11' assert rotate('#8a8', 360) == '#8a8' assert rotate('#8a8', 0) == '#8a8' assert rotate('#8a8', -360) == '#8a8' def test_hsl_to_rgb_part_0(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_0(): """Test rgb to hsl color function""" assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) def test_hsl_to_rgb_part_1(): """Test hsl to rgb color function""" assert hsl_to_rgb(-360, 100, 50) == (255, 0, 0) assert hsl_to_rgb(-300, 100, 50) == (255, 255, 0) assert hsl_to_rgb(-240, 100, 50) == (0, 255, 0) assert hsl_to_rgb(-180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(-120, 100, 50) == (0, 0, 255) assert hsl_to_rgb(-60, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_1(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (-360, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (-300, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (-240, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (-180, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (-120, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (-60, 100, 50) pass def test_hsl_to_rgb_part_2(): """Test hsl to rgb color function""" assert hsl_to_rgb(360, 100, 50) == (255, 0, 0) assert hsl_to_rgb(420, 100, 50) == (255, 255, 0) assert hsl_to_rgb(480, 100, 50) == (0, 255, 0) assert hsl_to_rgb(540, 100, 50) == (0, 255, 255) assert hsl_to_rgb(600, 100, 50) == (0, 0, 255) assert hsl_to_rgb(660, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_2(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (360, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (420, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (480, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (540, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (600, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (660, 100, 50) pass def test_hsl_to_rgb_part_3(): """Test hsl to rgb color function""" assert hsl_to_rgb(6120, 100, 50) == (255, 0, 0) assert hsl_to_rgb(-9660, 100, 50) == (255, 255, 0) assert hsl_to_rgb(99840, 100, 50) == (0, 255, 0) assert hsl_to_rgb(-900, 100, 50) == (0, 255, 255) assert hsl_to_rgb(-104880, 100, 50) == (0, 0, 255) assert hsl_to_rgb(2820, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_3(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (6120, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (-9660, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (99840, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (-900, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (-104880, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (2820, 100, 50) pass def test_hsl_to_rgb_part_4(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(12, 100, 50) == (255, 51, 0) assert hsl_to_rgb(24, 100, 50) == (255, 102, 0) assert hsl_to_rgb(36, 100, 50) == (255, 153, 0) assert hsl_to_rgb(48, 100, 50) == (255, 204, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(72, 100, 50) == (204, 255, 0) assert hsl_to_rgb(84, 100, 50) == (153, 255, 0) assert hsl_to_rgb(96, 100, 50) == (102, 255, 0) assert hsl_to_rgb(108, 100, 50) == (51, 255, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) def test_rgb_to_hsl_part_4(): """Test rgb to hsl color function""" assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 51, 0) == (12, 100, 50) assert rgb_to_hsl(255, 102, 0) == (24, 100, 50) assert rgb_to_hsl(255, 153, 0) == (36, 100, 50) assert rgb_to_hsl(255, 204, 0) == (48, 100, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(204, 255, 0) == (72, 100, 50) assert rgb_to_hsl(153, 255, 0) == (84, 100, 50) assert rgb_to_hsl(102, 255, 0) == (96, 100, 50) assert rgb_to_hsl(51, 255, 0) == (108, 100, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) def test_hsl_to_rgb_part_5(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(132, 100, 50) == (0, 255, 51) assert hsl_to_rgb(144, 100, 50) == (0, 255, 102) assert hsl_to_rgb(156, 100, 50) == (0, 255, 153) assert hsl_to_rgb(168, 100, 50) == (0, 255, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(192, 100, 50) == (0, 204, 255) assert hsl_to_rgb(204, 100, 50) == (0, 153, 255) assert hsl_to_rgb(216, 100, 50) == (0, 102, 255) assert hsl_to_rgb(228, 100, 50) == (0, 51, 255) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) def test_rgb_to_hsl_part_5(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(0, 255, 51) == (132, 100, 50) assert rgb_to_hsl(0, 255, 102) == (144, 100, 50) assert rgb_to_hsl(0, 255, 153) == (156, 100, 50) assert rgb_to_hsl(0, 255, 204) == (168, 100, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(0, 204, 255) == (192, 100, 50) assert rgb_to_hsl(0, 153, 255) == (204, 100, 50) assert rgb_to_hsl(0, 102, 255) == (216, 100, 50) assert rgb_to_hsl(0, 51, 255) == (228, 100, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) def test_hsl_to_rgb_part_6(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(252, 100, 50) == (51, 0, 255) assert hsl_to_rgb(264, 100, 50) == (102, 0, 255) assert hsl_to_rgb(276, 100, 50) == (153, 0, 255) assert hsl_to_rgb(288, 100, 50) == (204, 0, 255) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) assert hsl_to_rgb(312, 100, 50) == (255, 0, 204) assert hsl_to_rgb(324, 100, 50) == (255, 0, 153) assert hsl_to_rgb(336, 100, 50) == (255, 0, 102) assert hsl_to_rgb(348, 100, 50) == (255, 0, 51) assert hsl_to_rgb(360, 100, 50) == (255, 0, 0) def test_rgb_to_hsl_part_6(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(51, 0, 255) == (252, 100, 50) assert rgb_to_hsl(102, 0, 255) == (264, 100, 50) assert rgb_to_hsl(153, 0, 255) == (276, 100, 50) assert rgb_to_hsl(204, 0, 255) == (288, 100, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) assert rgb_to_hsl(255, 0, 204) == (312, 100, 50) assert rgb_to_hsl(255, 0, 153) == (324, 100, 50) assert rgb_to_hsl(255, 0, 102) == (336, 100, 50) assert rgb_to_hsl(255, 0, 51) == (348, 100, 50) # assert rgb_to_hsl(255, 0, 0) == (360, 100, 50) def test_hsl_to_rgb_part_7(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 20, 50) == (153, 102, 102) assert hsl_to_rgb(0, 60, 50) == (204, 51, 51) assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) def test_rgb_to_hsl_part_7(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 102, 102) == (0, 20, 50) assert rgb_to_hsl(204, 51, 51) == (0, 60, 50) assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) def test_hsl_to_rgb_part_8(): """Test hsl to rgb color function""" assert hsl_to_rgb(60, 20, 50) == (153, 153, 102) assert hsl_to_rgb(60, 60, 50) == (204, 204, 51) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) def test_rgb_to_hsl_part_8(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 153, 102) == (60, 20, 50) assert rgb_to_hsl(204, 204, 51) == (60, 60, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) def test_hsl_to_rgb_part_9(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 20, 50) == (102, 153, 102) assert hsl_to_rgb(120, 60, 50) == (51, 204, 51) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) def test_rgb_to_hsl_part_9(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 153, 102) == (120, 20, 50) assert rgb_to_hsl(51, 204, 51) == (120, 60, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) def test_hsl_to_rgb_part_10(): """Test hsl to rgb color function""" assert hsl_to_rgb(180, 20, 50) == (102, 153, 153) assert hsl_to_rgb(180, 60, 50) == (51, 204, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) def test_rgb_to_hsl_part_10(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 153, 153) == (180, 20, 50) assert rgb_to_hsl(51, 204, 204) == (180, 60, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) def test_hsl_to_rgb_part_11(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 20, 50) == (102, 102, 153) assert hsl_to_rgb(240, 60, 50) == (51, 51, 204) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) def test_rgb_to_hsl_part_11(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 102, 153) == (240, 20, 50) assert rgb_to_hsl(51, 51, 204) == (240, 60, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) def test_hsl_to_rgb_part_12(): """Test hsl to rgb color function""" assert hsl_to_rgb(300, 20, 50) == (153, 102, 153) assert hsl_to_rgb(300, 60, 50) == (204, 51, 204) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_12(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 102, 153) == (300, 20, 50) assert rgb_to_hsl(204, 51, 204) == (300, 60, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) def test_hsl_to_rgb_part_13(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 0) == (0, 0, 0) assert hsl_to_rgb(0, 100, 10) == (51, 0, 0) assert hsl_to_rgb(0, 100, 20) == (102, 0, 0) assert hsl_to_rgb(0, 100, 30) == (153, 0, 0) assert hsl_to_rgb(0, 100, 40) == (204, 0, 0) assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(0, 100, 60) == (255, 51, 51) assert hsl_to_rgb(0, 100, 70) == (255, 102, 102) assert hsl_to_rgb(0, 100, 80) == (255, 153, 153) assert hsl_to_rgb(0, 100, 90) == (255, 204, 204) assert hsl_to_rgb(0, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_13(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 0, 0) == (0, 0, 0) assert rgb_to_hsl(51, 0, 0) == (0, 100, 10) assert rgb_to_hsl(102, 0, 0) == (0, 100, 20) assert rgb_to_hsl(153, 0, 0) == (0, 100, 30) assert rgb_to_hsl(204, 0, 0) == (0, 100, 40) assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 51, 51) == (0, 100, 60) assert rgb_to_hsl(255, 102, 102) == (0, 100, 70) assert rgb_to_hsl(255, 153, 153) == (0, 100, 80) assert rgb_to_hsl(255, 204, 204) == (0, 100, 90) assert rgb_to_hsl(255, 255, 255) == (0, 0, 100) def test_hsl_to_rgb_part_14(): """Test hsl to rgb color function""" assert hsl_to_rgb(60, 100, 0) == (0, 0, 0) assert hsl_to_rgb(60, 100, 10) == (51, 51, 0) assert hsl_to_rgb(60, 100, 20) == (102, 102, 0) assert hsl_to_rgb(60, 100, 30) == (153, 153, 0) assert hsl_to_rgb(60, 100, 40) == (204, 204, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(60, 100, 60) == (255, 255, 51) assert hsl_to_rgb(60, 100, 70) == (255, 255, 102) assert hsl_to_rgb(60, 100, 80) == (255, 255, 153) assert hsl_to_rgb(60, 100, 90) == (255, 255, 204) assert hsl_to_rgb(60, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_14(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (60, 100, 0) assert rgb_to_hsl(51, 51, 0) == (60, 100, 10) assert rgb_to_hsl(102, 102, 0) == (60, 100, 20) assert rgb_to_hsl(153, 153, 0) == (60, 100, 30) assert rgb_to_hsl(204, 204, 0) == (60, 100, 40) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(255, 255, 51) == (60, 100, 60) assert rgb_to_hsl(255, 255, 102) == (60, 100, 70) assert rgb_to_hsl(255, 255, 153) == (60, 100, 80) assert rgb_to_hsl(255, 255, 204) == (60, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (60, 100, 100) def test_hsl_to_rgb_part_15(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 100, 0) == (0, 0, 0) assert hsl_to_rgb(120, 100, 10) == (0, 51, 0) assert hsl_to_rgb(120, 100, 20) == (0, 102, 0) assert hsl_to_rgb(120, 100, 30) == (0, 153, 0) assert hsl_to_rgb(120, 100, 40) == (0, 204, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(120, 100, 60) == (51, 255, 51) assert hsl_to_rgb(120, 100, 70) == (102, 255, 102) assert hsl_to_rgb(120, 100, 80) == (153, 255, 153) assert hsl_to_rgb(120, 100, 90) == (204, 255, 204) assert hsl_to_rgb(120, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_15(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (120, 100, 0) assert rgb_to_hsl(0, 51, 0) == (120, 100, 10) assert rgb_to_hsl(0, 102, 0) == (120, 100, 20) assert rgb_to_hsl(0, 153, 0) == (120, 100, 30) assert rgb_to_hsl(0, 204, 0) == (120, 100, 40) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(51, 255, 51) == (120, 100, 60) assert rgb_to_hsl(102, 255, 102) == (120, 100, 70) assert rgb_to_hsl(153, 255, 153) == (120, 100, 80) assert rgb_to_hsl(204, 255, 204) == (120, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (120, 100, 100) def test_hsl_to_rgb_part_16(): """Test hsl to rgb color function""" assert hsl_to_rgb(180, 100, 0) == (0, 0, 0) assert hsl_to_rgb(180, 100, 10) == (0, 51, 51) assert hsl_to_rgb(180, 100, 20) == (0, 102, 102) assert hsl_to_rgb(180, 100, 30) == (0, 153, 153) assert hsl_to_rgb(180, 100, 40) == (0, 204, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(180, 100, 60) == (51, 255, 255) assert hsl_to_rgb(180, 100, 70) == (102, 255, 255) assert hsl_to_rgb(180, 100, 80) == (153, 255, 255) assert hsl_to_rgb(180, 100, 90) == (204, 255, 255) assert hsl_to_rgb(180, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_16(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (180, 100, 0) assert rgb_to_hsl(0, 51, 51) == (180, 100, 10) assert rgb_to_hsl(0, 102, 102) == (180, 100, 20) assert rgb_to_hsl(0, 153, 153) == (180, 100, 30) assert rgb_to_hsl(0, 204, 204) == (180, 100, 40) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(51, 255, 255) == (180, 100, 60) assert rgb_to_hsl(102, 255, 255) == (180, 100, 70) assert rgb_to_hsl(153, 255, 255) == (180, 100, 80) assert rgb_to_hsl(204, 255, 255) == (180, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (180, 100, 100) def test_hsl_to_rgb_part_17(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 100, 0) == (0, 0, 0) assert hsl_to_rgb(240, 100, 10) == (0, 0, 51) assert hsl_to_rgb(240, 100, 20) == (0, 0, 102) assert hsl_to_rgb(240, 100, 30) == (0, 0, 153) assert hsl_to_rgb(240, 100, 40) == (0, 0, 204) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(240, 100, 60) == (51, 51, 255) assert hsl_to_rgb(240, 100, 70) == (102, 102, 255) assert hsl_to_rgb(240, 100, 80) == (153, 153, 255) assert hsl_to_rgb(240, 100, 90) == (204, 204, 255) assert hsl_to_rgb(240, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_17(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (240, 100, 0) assert rgb_to_hsl(0, 0, 51) == (240, 100, 10) assert rgb_to_hsl(0, 0, 102) == (240, 100, 20) assert rgb_to_hsl(0, 0, 153) == (240, 100, 30) assert rgb_to_hsl(0, 0, 204) == (240, 100, 40) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(51, 51, 255) == (240, 100, 60) assert rgb_to_hsl(102, 102, 255) == (240, 100, 70) assert rgb_to_hsl(153, 153, 255) == (240, 100, 80) assert rgb_to_hsl(204, 204, 255) == (240, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (240, 100, 100) def test_hsl_to_rgb_part_18(): """Test hsl to rgb color function""" assert hsl_to_rgb(300, 100, 0) == (0, 0, 0) assert hsl_to_rgb(300, 100, 10) == (51, 0, 51) assert hsl_to_rgb(300, 100, 20) == (102, 0, 102) assert hsl_to_rgb(300, 100, 30) == (153, 0, 153) assert hsl_to_rgb(300, 100, 40) == (204, 0, 204) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) assert hsl_to_rgb(300, 100, 60) == (255, 51, 255) assert hsl_to_rgb(300, 100, 70) == (255, 102, 255) assert hsl_to_rgb(300, 100, 80) == (255, 153, 255) assert hsl_to_rgb(300, 100, 90) == (255, 204, 255) assert hsl_to_rgb(300, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_18(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (300, 100, 0) assert rgb_to_hsl(51, 0, 51) == (300, 100, 10) assert rgb_to_hsl(102, 0, 102) == (300, 100, 20) assert rgb_to_hsl(153, 0, 153) == (300, 100, 30) assert rgb_to_hsl(204, 0, 204) == (300, 100, 40) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) assert rgb_to_hsl(255, 51, 255) == (300, 100, 60) assert rgb_to_hsl(255, 102, 255) == (300, 100, 70) assert rgb_to_hsl(255, 153, 255) == (300, 100, 80) assert rgb_to_hsl(255, 204, 255) == (300, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (300, 100, 100) pygal-2.4.0/pygal/test/__init__.py0000644000175000017500000000336612707661227017037 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Pygal test package""" import pygal from pygal.util import cut from pygal.graph.map import BaseMap from decimal import Decimal def get_data(i): """Return sample test data for an index""" return [ [(-1, 1), (2, 0), (0, 4)], [(0, 1), (None, 2), (3, 2)], [(-3, 3), (1, 3), (1, 1)], [(1, 1), (Decimal('1.'), 1), (1, 1)], [(3, 2), (2, 1), (1., 1)]][i] def adapt(chart, data): """Adapt data to chart type""" if isinstance(chart, pygal.XY): return data data = cut(data) if isinstance(chart, BaseMap): return list( map(lambda x: chart.__class__.x_labels[ int(x) % len(chart.__class__.x_labels)] if x is not None else None, data)) return data def make_data(chart, datas): """Add sample data to the test chart""" for i, data in enumerate(datas): chart.add(data[0], adapt(chart, data[1]), secondary=bool(i % 2)) return chart pygal-2.4.0/pygal/test/test_date.py0000644000175000017500000001161213114033322017223 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Date related charts tests""" from datetime import date, datetime, time, timedelta from pygal import DateLine, DateTimeLine, TimeDeltaLine, TimeLine from pygal._compat import timestamp, utc from pygal.test.utils import texts def test_date(): """Test a simple dateline""" date_chart = DateLine(truncate_label=1000) date_chart.add('dates', [ (date(2013, 1, 2), 300), (date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672) ]) q = date_chart.render_pyquery() assert list( map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [ '2013-01-12', '2013-01-24', '2013-02-04', '2013-02-16'] def test_time(): """Test a simple timeline""" time_chart = TimeLine(truncate_label=1000) time_chart.add('times', [ (time(1, 12, 29), 2), (time(21, 2, 29), 10), (time(12, 30, 59), 7) ]) q = time_chart.render_pyquery() assert list( map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [ '02:46:40', '05:33:20', '08:20:00', '11:06:40', '13:53:20', '16:40:00', '19:26:40'] def test_datetime(): """Test a simple datetimeline""" datetime_chart = DateTimeLine(truncate_label=1000) datetime_chart.add('datetimes', [ (datetime(2013, 1, 2, 1, 12, 29), 300), (datetime(2013, 1, 12, 21, 2, 29), 412), (datetime(2013, 2, 2, 12, 30, 59), 823), (datetime(2013, 2, 22), 672) ]) q = datetime_chart.render_pyquery() assert list( map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [ '2013-01-12T14:13:20', '2013-01-24T04:00:00', '2013-02-04T17:46:40', '2013-02-16T07:33:20'] def test_timedelta(): """Test a simple timedeltaline""" timedelta_chart = TimeDeltaLine(truncate_label=1000) timedelta_chart.add('timedeltas', [ (timedelta(seconds=1), 10), (timedelta(weeks=1), 50), (timedelta(hours=3, seconds=30), 3), (timedelta(microseconds=12112), .3), ]) q = timedelta_chart.render_pyquery() assert list( t for t in q(".axis.x text").map(texts) if t != '0:00:00' ) == [ '1 day, 3:46:40', '2 days, 7:33:20', '3 days, 11:20:00', '4 days, 15:06:40', '5 days, 18:53:20', '6 days, 22:40:00'] def test_date_xrange(): """Test dateline with xrange""" datey = DateLine(truncate_label=1000) datey.add('dates', [ (date(2013, 1, 2), 300), (date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672) ]) datey.xrange = (date(2013, 1, 1), date(2013, 3, 1)) q = datey.render_pyquery() assert list( map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [ '2013-01-01', '2013-01-12', '2013-01-24', '2013-02-04', '2013-02-16', '2013-02-27'] def test_date_labels(): """Test dateline with xrange""" datey = DateLine(truncate_label=1000) datey.add('dates', [ (date(2013, 1, 2), 300), (date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672) ]) datey.x_labels = [ date(2013, 1, 1), date(2013, 2, 1), date(2013, 3, 1) ] q = datey.render_pyquery() assert list( map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [ '2013-01-01', '2013-02-01', '2013-03-01'] def test_utc_timestamping(): assert timestamp( datetime(2017, 7, 14, 2, 40).replace(tzinfo=utc) ) == 1500000000 for d in ( datetime.now(), datetime.utcnow(), datetime(1999, 12, 31, 23, 59, 59), datetime(2000, 1, 1, 0, 0, 0) ): assert datetime.utcfromtimestamp( timestamp(d)) - d < timedelta(microseconds=10) pygal-2.4.0/pygal/test/test_util.py0000644000175000017500000001443613114033322017272 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Utility functions tests""" import sys from pytest import raises from pygal._compat import _ellipsis, u from pygal.util import ( _swap_curly, majorize, mergextend, minify_css, round_to_float, round_to_int, template, truncate) def test_round_to_int(): """Test round to int function""" assert round_to_int(154231, 1000) == 154000 assert round_to_int(154231, 10) == 154230 assert round_to_int(154231, 100000) == 200000 assert round_to_int(154231, 50000) == 150000 assert round_to_int(154231, 500) == 154000 assert round_to_int(154231, 200) == 154200 assert round_to_int(154361, 200) == 154400 def test_round_to_float(): """Test round to float function""" assert round_to_float(12.01934, .01) == 12.02 assert round_to_float(12.01134, .01) == 12.01 assert round_to_float(12.1934, .1) == 12.2 assert round_to_float(12.1134, .1) == 12.1 assert round_to_float(12.1134, .001) == 12.113 assert round_to_float(12.1134, .00001) == 12.1134 assert round_to_float(12.1934, .5) == 12.0 assert round_to_float(12.2934, .5) == 12.5 def test_swap_curly(): """Test swap curly function""" for str in ( 'foo', u('foo foo foo bar'), 'foo béè b¡ð/ijə˘©þß®~¯æ', u('foo béè b¡ð/ijə˘©þß®~¯æ')): assert _swap_curly(str) == str assert _swap_curly('foo{bar}baz') == 'foo{{bar}}baz' assert _swap_curly('foo{{bar}}baz') == 'foo{bar}baz' assert _swap_curly('{foo}{{bar}}{baz}') == '{{foo}}{bar}{{baz}}' assert _swap_curly('{foo}{{{bar}}}{baz}') == '{{foo}}{{{bar}}}{{baz}}' assert _swap_curly('foo{ bar }baz') == 'foo{{ bar }}baz' assert _swap_curly('foo{ bar}baz') == 'foo{{ bar}}baz' assert _swap_curly('foo{bar }baz') == 'foo{{bar }}baz' assert _swap_curly('foo{{ bar }}baz') == 'foo{bar}baz' assert _swap_curly('foo{{bar }}baz') == 'foo{bar}baz' assert _swap_curly('foo{{ bar}}baz') == 'foo{bar}baz' def test_format(): """Test format function""" assert template('foo {{ baz }}', baz='bar') == 'foo bar' with raises(KeyError): assert template('foo {{ baz }}') == 'foo baz' class Object(object): pass obj = Object() obj.a = 1 obj.b = True obj.c = '3' assert template( 'foo {{ o.a }} {{o.b}}-{{o.c}}', o=obj) == 'foo 1 True-3' def test_truncate(): """Test truncate function""" assert truncate('1234567890', 50) == '1234567890' assert truncate('1234567890', 5) == u('1234…') assert truncate('1234567890', 1) == u('…') assert truncate('1234567890', 9) == u('12345678…') assert truncate('1234567890', 10) == '1234567890' assert truncate('1234567890', 0) == '1234567890' assert truncate('1234567890', -1) == '1234567890' def test_minify_css(): """Test css minifier function""" css = ''' /* * Font-sizes from config, override with care */ .title { font-family: sans; font-size: 12 ; } .legends .legend text { font-family: monospace; font-size: 14 ;} ''' assert minify_css(css) == ( '.title{font-family:sans;font-size:12}' '.legends .legend text{font-family:monospace;font-size:14}') def test_majorize(): """Test majorize function""" assert majorize(()) == [] assert majorize((0,)) == [] assert majorize((0, 1)) == [] assert majorize((0, 1, 2)) == [] assert majorize((-1, 0, 1, 2)) == [0] assert majorize((0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1)) == [0, .5, 1] assert majorize((0, .2, .4, .6, .8, 1)) == [0, 1] assert majorize((-.4, -.2, 0, .2, .4, .6, .8, 1)) == [0, 1] assert majorize( (-1, -.8, -.6, -.4, -.2, 0, .2, .4, .6, .8, 1)) == [-1, 0, 1] assert majorize((0, .2, .4, .6, .8, 1, 1.2, 1.4, 1.6)) == [0, 1] assert majorize((0, .2, .4, .6, .8, 1, 1.2, 1.4, 1.6, 1.8, 2)) == [0, 1, 2] assert majorize( (0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120)) == [0, 50, 100] assert majorize( (0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36)) == [0, 10, 20, 30] assert majorize((0, 1, 2, 3, 4, 5)) == [0, 5] assert majorize((-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5)) == [-5, 0, 5] assert majorize((-5, 5, -4, 4, 0, 1, -1, 3, -2, 2, -3)) == [-5, 0, 5] assert majorize((0, 1, 2, 3, 4)) == [0] assert majorize((3, 4, 5, 6)) == [5] assert majorize((0, 1, 2, 3, 4, 5, 6, 7, 8)) == [0, 5] assert majorize((-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5)) == [-5, 0, 5] assert majorize((-6, -5, -4, -3, -2, -1, 0, 1, 2, 3)) == [-5, 0] assert majorize((-6, -5, -4, -3)) == [-5] assert majorize((1, 10, 100, 1000, 10000, 100000)) == [] assert majorize(range(30, 70, 5)) == [30, 40, 50, 60] assert majorize(range(20, 55, 2)) == [20, 30, 40, 50] assert majorize(range(21, 83, 3)) == [30, 45, 60, 75] # TODO: handle crazy cases # assert majorize(range(20, 83, 3)) == [20, 35, 50, 65, 80] def test_mergextend(): """Test mergextend function""" assert mergextend(['a', 'b'], ['c', 'd']) == ['a', 'b'] assert mergextend([], ['c', 'd']) == [] assert mergextend(['a', 'b'], []) == ['a', 'b'] assert mergextend([_ellipsis], ['c', 'd']) == ['c', 'd'] assert mergextend([_ellipsis, 'b'], ['c', 'd']) == ['c', 'd', 'b'] assert mergextend(['a', _ellipsis], ['c', 'd']) == ['a', 'c', 'd'] assert mergextend(['a', _ellipsis, 'b'], ['c', 'd']) == [ 'a', 'c', 'd', 'b'] if sys.version_info[0] >= 3: # For @#! sake it's 2016 now assert eval("mergextend(['a', ..., 'b'], ['c', 'd'])") == [ 'a', 'c', 'd', 'b'] pygal-2.4.0/pygal/test/test_table.py0000644000175000017500000000234013127115716017407 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Box chart related tests""" from pyquery import PyQuery as pq from pygal import Pie def test_pie_table(): """Test rendering a table for a pie""" chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage in February 2012 (in %)' chart.add('IE', 19.5) chart.add('Firefox', 36.6) chart.add('Chrome', 36.3) chart.add('Safari', 4.5) chart.add('Opera', 2.3) q = pq(chart.render_table()) assert len(q('table')) == 1 pygal-2.4.0/pygal/test/test_box.py0000644000175000017500000001201313114033322017072 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Box chart related tests""" from pygal.graph.box import Box def test_quartiles(): """Test box points for the 1.5IQR computation method""" a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='1.5IQR') assert q1 == 7.0 / 4.0 assert q2 == 4.0 assert q3 == 23 / 4.0 assert q0 == 7.0 / 4.0 - 6.0 # q1 - 1.5 * iqr assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr b = [1.0, 4.0, 6.0, 8.0] # even test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='1.5IQR') assert q2 == 5.0 c = [2.0, None, 4.0, 6.0, None] # odd with None elements (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='1.5IQR') assert q2 == 4.0 d = [4] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( d, mode='1.5IQR') assert q0 == 4 assert q1 == 4 assert q2 == 4 assert q3 == 4 assert q4 == 4 def test_quartiles_min_extremes(): """Test box points for the extremes computation method""" a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='extremes') assert q1 == 7.0 / 4.0 assert q2 == 4.0 assert q3 == 23 / 4.0 assert q0 == -2.0 # min assert q4 == 8.0 # max b = [1.0, 4.0, 6.0, 8.0] # even test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='extremes') assert q2 == 5.0 c = [2.0, None, 4.0, 6.0, None] # odd with None elements (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='extremes') assert q2 == 4.0 d = [4] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( d, mode='extremes') assert q0 == 4 assert q1 == 4 assert q2 == 4 assert q3 == 4 assert q4 == 4 def test_quartiles_tukey(): """Test box points for the tukey computation method""" a = [] # empty data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='tukey') assert min_s == q0 == q1 == q2 == q3 == q4 == 0 assert outliers == [] # https://en.wikipedia.org/wiki/Quartile example 1 b = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='tukey') assert min_s == q0 == 6 assert q1 == 20.25 assert q2 == 40 assert q3 == 42.75 assert max_s == q4 == 49 assert outliers == [] # previous test with added outlier 75 c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='tukey') assert min_s == q0 == 6 assert q1 == 25.5 assert q2 == (40 + 41) / 2.0 assert q3 == 45 assert max_s == 75 assert outliers == [75] # one more outlier, 77 c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75, 77] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='tukey') assert min_s == q0 == 6 assert q1 == 30.75 assert q2 == 41 assert q3 == 47.5 assert max_s == 77 assert 75 in outliers assert 77 in outliers def test_quartiles_stdev(): """Test box points for the stdev computation method""" a = [35, 42, 35, 41, 36, 6, 12, 51, 33, 27, 46, 36, 44, 53, 75, 46, 16, 51, 45, 29, 25, 26, 54, 61, 27, 40, 23, 34, 51, 37] SD = 14.67 (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='stdev') assert min_s == min(a) assert max_s == max(a) assert q2 == 36.5 assert q4 <= q2 + SD assert q0 >= q2 - SD assert all(n in outliers for n in [6, 12, 16, 53, 54, 61, 75]) b = [5] # test for posible zero division (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='stdev') assert min_s == q0 == q1 == q2 == q3 == q4 == max_s == b[0] assert outliers == [] def test_simple_box(): """Simple box test""" box = Box() box.add('test1', [-1, 2, 3, 3.1, 3.2, 4, 5]) box.add('test2', [2, 3, 5, 6, 6, 4]) box.title = 'Box test' q = box.render_pyquery() assert len(q(".axis.y")) == 1 assert len(q(".legend")) == 2 assert len(q(".plot .series rect")) == 2 pygal-2.4.0/pygal/test/test_pie.py0000644000175000017500000000413513114033322017065 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Donut chart related tests""" from pygal import Pie def test_donut(): """Test a donut pie chart""" chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage in February 2012 (in %)' chart.add('IE', 19.5) chart.add('Firefox', 36.6) chart.add('Chrome', 36.3) chart.add('Safari', 4.5) chart.add('Opera', 2.3) assert chart.render() def test_multiseries_donut(): """Test a donut pie chart with multiserie""" # this just demos that the multiseries pie does not respect # the inner_radius chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage by version in February 2012 (in %)' chart.add('IE', [5.7, 10.2, 2.6, 1]) chart.add('Firefox', [.6, 16.8, 7.4, 2.2, 1.2, 1, 1, 1.1, 4.3, 1]) chart.add('Chrome', [.3, .9, 17.1, 15.3, .6, .5, 1.6]) chart.add('Safari', [4.4, .1]) chart.add('Opera', [.1, 1.6, .1, .5]) assert chart.render() def test_half_pie(): """Test a half pie chart""" pie = Pie() pie.add('IE', 19.5) pie.add('Firefox', 36.6) pie.add('Chrome', 36.3) pie.add('Safari', 4.5) pie.add('Opera', 2.3) half = Pie(half_pie=True) half.add('IE', 19.5) half.add('Firefox', 36.6) half.add('Chrome', 36.3) half.add('Safari', 4.5) half.add('Opera', 2.3) assert pie.render() != half.render() pygal-2.4.0/pygal/test/test_formatters.py0000644000175000017500000000503113114033322020472 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Test formatters""" from pygal import formatters from pygal._compat import u def test_human_readable(): """Test human_readable formatter""" f = formatters.human_readable assert f(1) == '1' assert f(1.) == '1' assert f(10) == '10' assert f(12.5) == '12.5' assert f(1000) == '1k' assert f(5000) == '5k' assert f(100000) == '100k' assert f(1253) == '1.253k' assert f(1250) == '1.25k' assert f(0.1) == '100m' assert f(0.01) == '10m' assert f(0.001) == '1m' assert f(0.002) == '2m' assert f(0.0025) == '2.5m' assert f(0.0001) == u('100µ') assert f(0.000123) == u('123µ') assert f(0.00001) == u('10µ') assert f(0.000001) == u('1µ') assert f(0.0000001) == u('100n') assert f(0.0000000001) == u('100p') assert f(0) == '0' assert f(0.) == '0' assert f(-1337) == '-1.337k' assert f(-.000000042) == '-42n' def test_human_readable_custom(): """Test human_readable formatter option""" f = formatters.HumanReadable() assert f(None) == u('∅') f = formatters.HumanReadable(none_char='/') assert f(None) == '/' def test_significant(): """Test significant formatter""" f = formatters.significant assert f(1) == '1' assert f(1.) == '1' assert f(-1.) == '-1' assert f(10) == '10' assert f(10000000000) == '1e+10' assert f(100000000000) == '1e+11' assert f(120000000000) == '1.2e+11' assert f(.1) == '0.1' assert f(.01) == '0.01' assert f(.0000000001) == '1e-10' assert f(-.0000000001) == '-1e-10' assert f(.0000000001002) == '1.002e-10' assert f(.0000000001002) == '1.002e-10' assert f(.12345678912345) == '0.1234567891' assert f(.012345678912345) == '0.01234567891' assert f(12345678912345) == '1.234567891e+13' pygal-2.4.0/pygal/test/test_interpolate.py0000644000175000017500000000712513114033322020640 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Interpolations tests""" from pygal.test import make_data def test_cubic(Chart, datas): """Test cubic interpolation""" chart = Chart(interpolate='cubic') chart = make_data(chart, datas) assert chart.render() def test_cubic_prec(Chart, datas): """Test cubic interpolation precision""" chart = Chart(interpolate='cubic', interpolation_precision=200) chart = make_data(chart, datas) chart_low = Chart(interpolate='cubic', interpolation_precision=5) chart_low = make_data(chart, datas) assert len(chart.render()) >= len(chart_low.render()) def test_quadratic(Chart, datas): """Test quadratic interpolation""" chart = Chart(interpolate='quadratic') chart = make_data(chart, datas) assert chart.render() def test_lagrange(Chart, datas): """Test lagrange interpolation""" chart = Chart(interpolate='lagrange') chart = make_data(chart, datas) assert chart.render() def test_trigonometric(Chart, datas): """Test trigonometric interpolation""" chart = Chart(interpolate='trigonometric') chart = make_data(chart, datas) assert chart.render() def test_hermite(Chart, datas): """Test hermite interpolation""" chart = Chart(interpolate='hermite') chart = make_data(chart, datas) assert chart.render() def test_hermite_finite(Chart, datas): """Test hermite finite difference interpolation""" chart = Chart(interpolate='hermite', interpolation_parameters={'type': 'finite_difference'}) chart = make_data(chart, datas) assert chart.render() def test_hermite_cardinal(Chart, datas): """Test hermite cardinal interpolation""" chart = Chart(interpolate='hermite', interpolation_parameters={'type': 'cardinal', 'c': .75}) chart = make_data(chart, datas) assert chart.render() def test_hermite_catmull_rom(Chart, datas): """Test hermite catmull rom interpolation""" chart = Chart(interpolate='hermite', interpolation_parameters={'type': 'catmull_rom'}) chart = make_data(chart, datas) assert chart.render() def test_hermite_kochanek_bartels(Chart, datas): """Test hermite kochanek bartels interpolation""" chart = Chart(interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1}) chart = make_data(chart, datas) assert chart.render() chart = Chart(interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': -1, 'c': -8, 't': 0}) chart = make_data(chart, datas) assert chart.render() chart = Chart(interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': 0, 'c': 10, 't': -1}) chart = make_data(chart, datas) assert chart.render() pygal-2.4.0/pygal/test/test_view.py0000644000175000017500000000200613114033322017255 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """View related tests""" # TODO def test_all_logarithmic(Chart): """Test logarithmic view rendering""" chart = Chart(logarithmic=True) chart.add('1', [1, 30, 8, 199, -23]) chart.add('2', [87, 42, .9, 189, 81]) assert chart.render() pygal-2.4.0/pygal/test/test_graph.py0000644000175000017500000003143513114033322017414 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Generate tests for different chart types with different data""" import io import os import sys import uuid import pytest import pygal from pygal._compat import u from pygal.graph.map import BaseMap from pygal.test import make_data from pygal.util import cut try: import cairosvg except ImportError: cairosvg = None def test_multi_render(Chart, datas): """Check that a chart always render the same""" chart = Chart() chart = make_data(chart, datas) svg = chart.render() for i in range(2): assert svg == chart.render() def test_render_to_file(Chart, datas): """Test in file rendering""" file_name = '/tmp/test_graph-%s.svg' % uuid.uuid4() if os.path.exists(file_name): os.remove(file_name) chart = Chart() chart = make_data(chart, datas) chart.render_to_file(file_name) with io.open(file_name, encoding="utf-8") as f: assert 'pygal' in f.read() os.remove(file_name) @pytest.mark.skipif(not cairosvg, reason="CairoSVG not installed") def test_render_to_png(Chart, datas): """Test in file png rendering""" file_name = '/tmp/test_graph-%s.png' % uuid.uuid4() if os.path.exists(file_name): os.remove(file_name) chart = Chart() chart = make_data(chart, datas) chart.render_to_png(file_name) png = chart._repr_png_() with open(file_name, 'rb') as f: assert png == f.read() os.remove(file_name) def test_metadata(Chart): """Test metadata values""" chart = Chart() v = range(7) if Chart in (pygal.Box,): return # summary charts cannot display per-value metadata elif Chart == pygal.XY: v = list(map(lambda x: (x, x + 1), v)) elif issubclass(Chart, BaseMap): v = [(k, i) for i, k in enumerate(Chart.x_labels) if k not in [ 'oecd', 'nafta', 'eur']] chart.add('Serie with metadata', [ v[0], {'value': v[1]}, {'value': v[2], 'label': 'Three'}, {'value': v[3], 'xlink': 'http://4.example.com/'}, {'value': v[4], 'xlink': 'http://5.example.com/', 'label': 'Five'}, {'value': v[5], 'xlink': { 'href': 'http://6.example.com/'}, 'label': 'Six'}, {'value': v[6], 'xlink': { 'href': 'http://7.example.com/', 'target': '_blank'}, 'label': 'Seven'} ]) q = chart.render_pyquery() for md in ('Three', 'Five', 'Seven'): assert md in cut(q('desc'), 'text') for md in ('http://7.example.com/', 'http://4.example.com/'): assert md in [e.attrib.get('xlink:href') for e in q('a')] if Chart in (pygal.Pie, pygal.Treemap, pygal.SolidGauge): # Slices with value 0 are not rendered assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) elif not issubclass(Chart, BaseMap): # Tooltip are not working on maps assert len(v) == len(q('.tooltip-trigger').siblings('.value')) def test_empty_lists(Chart): """Test chart rendering with an empty serie""" chart = Chart() chart.add('A', [1, 2]) chart.add('B', []) if not chart._dual: chart.x_labels = ('red', 'green', 'blue') q = chart.render_pyquery() assert len(q(".legend")) == 2 def test_empty_lists_with_nones(Chart): """Test chart rendering with a None filled serie""" chart = Chart() chart.add('A', [None, None]) chart.add('B', [None, 4, 4]) q = chart.render_pyquery() assert len(q(".legend")) == 2 def test_only_one_value(Chart): """Test chart rendering with only one value""" chart = Chart() chart.add('S', [1]) q = chart.render_pyquery() assert len(q(".legend")) == 1 def test_only_one_value_log(Chart): """Test logarithmic chart rendering with only one value""" chart = Chart(logarithmic=True) chart.add('S', [1]) if not chart._dual: chart.x_labels = ('single') q = chart.render_pyquery() assert len(q(".legend")) == 1 def test_only_one_value_intrp(Chart): """Test interpolated chart rendering with only one value""" chart = Chart(interpolate='cubic') chart.add('S', [1]) q = chart.render_pyquery() assert len(q(".legend")) == 1 def test_non_iterable_value(Chart): """Test serie as non iterable""" chart = Chart(no_prefix=True) chart.add('A', 1) chart.add('B', 2) if not chart._dual: chart.x_labels = ('red', 'green', 'blue') chart1 = chart.render() chart = Chart(no_prefix=True) chart.add('A', [1]) chart.add('B', [2]) if not chart._dual: chart.x_labels = ('red', 'green', 'blue') chart2 = chart.render() assert chart1 == chart2 def test_iterable_types(Chart): """Test serie as various iterable""" chart = Chart(no_prefix=True) chart.add('A', [1, 2]) chart.add('B', []) if not chart._dual: chart.x_labels = ('red', 'green', 'blue') chart1 = chart.render() chart = Chart(no_prefix=True) chart.add('A', (1, 2)) chart.add('B', tuple()) if not chart._dual: chart.x_labels = ('red', 'green', 'blue') chart2 = chart.render() assert chart1 == chart2 def test_values_by_dict(Chart): """Test serie as dict""" chart1 = Chart(no_prefix=True) chart2 = Chart(no_prefix=True) if not issubclass(Chart, BaseMap) and not Chart._dual: chart1.add('A', {'red': 10, 'green': 12, 'blue': 14}) chart1.add('B', {'green': 11, 'red': 7}) chart1.add('C', {'blue': 7}) chart1.add('D', {}) chart1.add('E', {'blue': 2, 'red': 13}) chart1.x_labels = ('red', 'green', 'blue') chart2.add('A', [10, 12, 14]) chart2.add('B', [7, 11]) chart2.add('C', [None, None, 7]) chart2.add('D', []) chart2.add('E', [13, None, 2]) chart2.x_labels = ('red', 'green', 'blue') elif not Chart._dual: chart1.add('A', {'fr': 10, 'us': 12, 'jp': 14}) chart1.add('B', {'cn': 99}) chart1.add('C', {}) chart2.add('A', [('fr', 10), ('us', 12), ('jp', 14)]) chart2.add('B', [('cn', 99)]) chart2.add('C', [None, (None, None)]) assert chart1.render() == chart2.render() def test_no_data_with_no_values(Chart): """Test no data""" chart = Chart() q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_no_values_with_include_x_axis(Chart): """Test no data and include_x_axis""" chart = Chart(include_x_axis=True) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_empty_serie(Chart): """Test no data for empty serie""" chart = Chart() chart.add('Serie', []) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_empty_series(Chart): """Test no data for 2 empty series""" chart = Chart() chart.add('Serie1', []) chart.add('Serie2', []) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_none(Chart): """Test no data for a None containing serie""" chart = Chart() chart.add('Serie', None) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_list_of_none(Chart): """Test no data for a None containing serie""" chart = Chart() chart.add('Serie', [None]) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_no_data_with_lists_of_nones(Chart): """Test no data for several None containing series""" chart = Chart() chart.add('Serie1', [None, None, None, None]) chart.add('Serie2', [None, None, None]) q = chart.render_pyquery() assert q(".text-overlay text").text() == "No data" def test_unicode_labels_decode(Chart): """Test unicode labels""" chart = Chart() chart.add(u('Série1'), [{ 'value': 1, 'xlink': 'http://1/', 'label': u('{\}°ijæð©&×&<—×€¿_…\{_…') }, { 'value': 2, 'xlink': { 'href': 'http://6.example.com/' }, 'label': u('æÂ°€≠|€æÂ°€əæ') }, { 'value': 3, 'label': 'unicode <3' }]) if not chart._dual: chart.x_labels = [u('&œ'), u('¿?'), u('††††††††'), 'unicode <3'] chart.render_pyquery() def test_unicode_labels_python2(Chart): """Test unicode labels in python 2""" if sys.version_info[0] == 3: return chart = Chart() chart.add(u('Série1'), [{ 'value': 1, 'xlink': 'http://1/', 'label': eval("u'{\}°ijæð©&×&<—×€¿_…\{_…'") }, { 'value': 2, 'xlink': { 'href': 'http://6.example.com/' }, 'label': eval("u'æÂ°€≠|€æÂ°€əæ'") }, { 'value': 3, 'label': eval("'unicode <3'") }]) if not chart._dual: chart.x_labels = eval("[u'&œ', u'¿?', u'††††††††', 'unicode <3']") chart.render_pyquery() def test_unicode_labels_python3(Chart): """Test unicode labels in python 3""" if sys.version_info[0] == 2: return chart = Chart() chart.add(u('Série1'), [{ 'value': 1, 'xlink': 'http://1/', 'label': eval("'{\}°ijæð©&×&<—×€¿_…\{_…'") }, { 'value': 2, 'xlink': { 'href': 'http://6.example.com/' }, 'label': eval("'æÂ°€≠|€æÂ°€əæ'") }, { 'value': 3, 'label': eval("b'unicode <3'") }]) if not chart._dual: chart.x_labels = eval("['&œ', '¿?', '††††††††', 'unicode <3']") chart.render_pyquery() def test_labels_with_links(Chart): """Test values with links""" chart = Chart() # link on chart and label chart.add({ 'title': 'Red', 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'} }, [{ 'value': 2, 'label': 'This is red', 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'}}]) # link on chart only chart.add('Green', [{ 'value': 4, 'label': 'This is green', 'xlink': { 'href': 'http://en.wikipedia.org/wiki/Green', 'target': '_top'}}]) # link on label only opens in new tab chart.add({'title': 'Yellow', 'xlink': { 'href': 'http://en.wikipedia.org/wiki/Yellow', 'target': '_blank'}}, 7) # link on chart only chart.add('Blue', [{ 'value': 5, 'xlink': { 'href': 'http://en.wikipedia.org/wiki/Blue', 'target': '_blank'}}]) # link on label and chart with diffrent behaviours chart.add({ 'title': 'Violet', 'xlink': 'http://en.wikipedia.org/wiki/Violet_(color)' }, [{ 'value': 3, 'label': 'This is violet', 'xlink': { 'href': 'http://en.wikipedia.org/wiki/Violet_(color)', 'target': '_self'}}]) q = chart.render_pyquery() links = q('a') assert len(links) == 7 or isinstance(chart, BaseMap) and len(links) == 3 def test_sparkline(Chart, datas): """Test sparkline""" chart = Chart() chart = make_data(chart, datas) assert chart.render_sparkline() def test_secondary(Chart): """Test secondary chart""" chart = Chart() rng = [83, .12, -34, 59] chart.add('First serie', rng) chart.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True) assert chart.render_pyquery() def test_ipython_notebook(Chart, datas): """Test ipython notebook""" chart = Chart() chart = make_data(chart, datas) assert chart._repr_svg_() def test_long_title(Chart, datas): """Test chart rendering with a long title""" chart = Chart( title="A chart is a graphical representation of data, in which " "'the data is represented by symbols, such as bars in a bar chart, " "lines in a line chart, or slices in a pie chart'. A chart can " "represent tabular numeric data, functions or some kinds of " "qualitative structure and provides different info.") chart = make_data(chart, datas) q = chart.render_pyquery() assert len(q('.titles text')) == 5 pygal-2.4.0/pygal/test/test_line_log_none_max_solved.py0000644000175000017500000000074013127125562023352 0ustar zerozero00000000000000# This file is test file for NoneMaxSolved # I have modified the line.py and passed other test # This test is for us to test whether the none value # in the Log graph will be max or not (issue #309) from __future__ import division from pygal import Line chart = Line(title='test', logarithmic=True) chart.add('test 1', [None, -38, 48, 4422, 35586, 1003452, 225533]) chart.add('test 2', [1, 40, 20, 38, 2937, 20399, 3947]) q = chart.render_pyquery() assert len(q(".dots")) == 12 pygal-2.4.0/pygal/test/test_style.py0000644000175000017500000000313713114033322017451 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Style related tests""" from pygal import Line from pygal.style import ( DarkenStyle, DesaturateStyle, LightenStyle, LightStyle, RotateStyle, SaturateStyle) STYLES = LightenStyle, DarkenStyle, SaturateStyle, DesaturateStyle, RotateStyle def test_parametric_styles(): """Test that no parametric produce the same result""" chart = None for style in STYLES: line = Line(style=style('#f4e83a')) line.add('_', [1, 2, 3]) line.x_labels = 'abc' new_chart = line.render() assert chart != new_chart chart = new_chart def test_parametric_styles_with_parameters(): """Test a parametric style with parameters""" line = Line(style=RotateStyle( '#de3804', step=12, max_=180, base_style=LightStyle)) line.add('_', [1, 2, 3]) line.x_labels = 'abc' assert line.render() pygal-2.4.0/pygal/test/test_serie_config.py0000644000175000017500000000437013114033322020745 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Test per serie configuration""" from pygal import Line s1 = [1, 3, 12, 3, 4] s2 = [7, -4, 10, None, 8, 3, 1] def test_no_serie_config(): """Test per serie no configuration""" chart = Line() chart.add('1', s1) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 1 assert len(q('.serie-1 .line')) == 1 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_global_config(): """Test global configuration""" chart = Line(stroke=False) chart.add('1', s1) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 0 assert len(q('.serie-1 .line')) == 0 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_serie_config(): """Test per serie configuration""" chart = Line() chart.add('1', s1, stroke=False) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 0 assert len(q('.serie-1 .line')) == 1 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_serie_precedence_over_global_config(): """Test that per serie configuration overide global configuration""" chart = Line(stroke=False) chart.add('1', s1, stroke=True) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 1 assert len(q('.serie-1 .line')) == 0 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 pygal-2.4.0/pygal/test/test_xml_filters.py0000644000175000017500000000427713114033322020647 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Xml filter tests""" from pygal import Bar class ChangeBarsXMLFilter(object): """xml filter that insert a subplot""" def __init__(self, a, b): """Generate data""" self.data = [b[i] - a[i] for i in range(len(a))] def __call__(self, T): """Apply the filter on the tree""" subplot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=150) subplot.add("Difference", self.data) subplot = subplot.render_tree() subplot = subplot.findall("g")[0] T.insert(2, subplot) T.findall("g")[1].set('transform', 'translate(0,150), scale(1,0.75)') return T def test_xml_filters_round_trip(): """Ensure doing nothing does nothing""" plot = Bar() plot.add("A", [60, 75, 80, 78, 83, 90]) plot.add("B", [92, 87, 81, 73, 68, 55]) before = plot.render() plot.add_xml_filter(lambda T: T) after = plot.render() assert before == after def test_xml_filters_change_bars(): """Test the use a xml filter""" plot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=600) A = [60, 75, 80, 78, 83, 90] B = [92, 87, 81, 73, 68, 55] plot.add("A", A) plot.add("B", B) plot.add_xml_filter(ChangeBarsXMLFilter(A, B)) q = plot.render_tree() assert len(q.findall("g")) == 2 assert q.findall("g")[1].attrib[ "transform"] == "translate(0,150), scale(1,0.75)" pygal-2.4.0/pygal/test/test_sparktext.py0000644000175000017500000000523713114033322020341 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Test sparktext rendering""" from pygal import Bar, Line from pygal._compat import u def test_basic_sparktext(): """Test basic sparktext""" chart = Line() chart.add('_', [1, 5, 22, 13, 53]) assert chart.render_sparktext() == u('▁▁▃▂█') def test_all_sparktext(): """Test all character sparktext""" chart = Line() chart.add('_', range(8)) assert chart.render_sparktext() == u('▁▂▃▄▅▆▇█') def test_shifted_sparktext(): """Test relative_to option in sparktext""" chart = Line() chart.add('_', list(map(lambda x: x + 10000, range(8)))) assert chart.render_sparktext() == u('▁▂▃▄▅▆▇█') assert chart.render_sparktext(relative_to=0) == u('▇▇▇▇▇▇▇█') def test_another_sparktext(): """Test that same data produces same sparktext""" chart = Line() chart.add('_', [0, 30, 55, 80, 33, 150]) assert chart.render_sparktext() == u('▁▂▃▄▂█') assert chart.render_sparktext() == chart.render_sparktext() chart2 = Bar() chart2.add('_', [0, 30, 55, 80, 33, 150]) assert chart2.render_sparktext() == chart.render_sparktext() def test_negative_and_float__sparktext(): """Test negative values""" """Test negative values""" chart = Line() chart.add('_', [0.1, 0.2, 0.9, -0.5]) assert chart.render_sparktext() == u('▁▂█▁') def test_no_data_sparktext(): """Test no data sparktext""" chart2 = Line() chart2.add('_', []) assert chart2.render_sparktext() == u('') chart3 = Line() assert chart3.render_sparktext() == u('') def test_same_max_and_relative_values_sparktext(): """Test flat sparktexts""" chart = Line() chart.add('_', [0, 0, 0, 0, 0]) assert chart.render_sparktext() == u('▁▁▁▁▁') chart2 = Line() chart2.add('_', [1, 1, 1, 1, 1]) assert chart2.render_sparktext(relative_to=1) == u('▁▁▁▁▁') pygal-2.4.0/pygal/test/conftest.py0000644000175000017500000000340613114033322017076 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """pytest fixtures""" import sys import pytest import pygal from pygal.etree import etree from . import get_data @pytest.fixture def etreefx(request): """Fixture allowing to test with builtin etree and lxml""" if request.param == 'etree': etree.to_etree() if request.param == 'lxml': etree.to_lxml() def pytest_generate_tests(metafunc): """Generate the tests for etree and lxml""" if etree._lxml_etree: metafunc.fixturenames.append('etreefx') metafunc.parametrize('etreefx', ['lxml', 'etree'], indirect=True) if not hasattr(sys, 'pypy_version_info'): etree.to_lxml() if hasattr(sys, 'pypy_version_info'): etree.to_etree() if "Chart" in metafunc.funcargnames: metafunc.parametrize("Chart", pygal.CHARTS) if "datas" in metafunc.funcargnames: metafunc.parametrize( "datas", [ [("Serie %d" % i, get_data(i)) for i in range(s)] for s in (5, 1, 0) ]) pygal-2.4.0/pygal/interpolate.py0000644000175000017500000002052413114033322016620 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """ Interpolation functions These functions takes two lists of points x and y and returns an iterator over the interpolation between all these points with `precision` interpolated points between each of them """ from __future__ import division from math import sin def quadratic_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using a quadratic algorithm https://en.wikipedia.org/wiki/Spline_(mathematics) """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] delta_y = [y2 - y1 for y1, y2 in zip(y, y[1:])] slope = [delta_y[i] / delta_x[i] if delta_x[i] else 1 for i in range(n)] # Quadratic spline: a + bx + cx² a = y b = [0] * (n + 1) c = [0] * (n + 1) for i in range(1, n): b[i] = 2 * slope[i - 1] - b[i - 1] c = [(slope[i] - b[i]) / delta_x[i] if delta_x[i] else 0 for i in range(n)] for i in range(n + 1): yield x[i], a[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = s * delta_x[i] / precision X2 = X * X yield x[i] + X, a[i] + b[i] * X + c[i] * X2 def cubic_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using a cubic algorithm https://en.wikipedia.org/wiki/Spline_interpolation """ n = len(x) - 1 # Spline equation is a + bx + cx² + dx³ # ie: Spline part i equation is a[i] + b[i]x + c[i]x² + d[i]x³ a = y b = [0] * (n + 1) c = [0] * (n + 1) d = [0] * (n + 1) m = [0] * (n + 1) z = [0] * (n + 1) h = [x2 - x1 for x1, x2 in zip(x, x[1:])] k = [a2 - a1 for a1, a2 in zip(a, a[1:])] g = [k[i] / h[i] if h[i] else 1 for i in range(n)] for i in range(1, n): j = i - 1 l = 1 / (2 * (x[i + 1] - x[j]) - h[j] * m[j]) if x[i + 1] - x[j] else 0 m[i] = h[i] * l z[i] = (3 * (g[i] - g[j]) - h[j] * z[j]) * l for j in reversed(range(n)): if h[j] == 0: continue c[j] = z[j] - (m[j] * c[j + 1]) b[j] = g[j] - (h[j] * (c[j + 1] + 2 * c[j])) / 3 d[j] = (c[j + 1] - c[j]) / (3 * h[j]) for i in range(n + 1): yield x[i], a[i] if i == n or h[i] == 0: continue for s in range(1, precision): X = s * h[i] / precision X2 = X * X X3 = X2 * X yield x[i] + X, a[i] + b[i] * X + c[i] * X2 + d[i] * X3 def hermite_interpolate(x, y, precision=250, type='cardinal', c=None, b=None, t=None): """ Interpolate x, y using the hermite method. See https://en.wikipedia.org/wiki/Cubic_Hermite_spline This interpolation is configurable and contain 4 subtypes: * Catmull Rom * Finite Difference * Cardinal * Kochanek Bartels The cardinal subtype is customizable with a parameter: * c: tension (0, 1) This last type is also customizable using 3 parameters: * c: continuity (-1, 1) * b: bias (-1, 1) * t: tension (-1, 1) """ n = len(x) - 1 m = [1] * (n + 1) w = [1] * (n + 1) delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] if type == 'catmull_rom': type = 'cardinal' c = 0 if type == 'finite_difference': for i in range(1, n): m[i] = w[i] = .5 * ( (y[i + 1] - y[i]) / (x[i + 1] - x[i]) + (y[i] - y[i - 1]) / ( x[i] - x[i - 1]) ) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0 elif type == 'kochanek_bartels': c = c or 0 b = b or 0 t = t or 0 for i in range(1, n): m[i] = .5 * ((1 - t) * (1 + b) * (1 + c) * (y[i] - y[i - 1]) + (1 - t) * (1 - b) * (1 - c) * (y[i + 1] - y[i])) w[i] = .5 * ((1 - t) * (1 + b) * (1 - c) * (y[i] - y[i - 1]) + (1 - t) * (1 - b) * (1 + c) * (y[i + 1] - y[i])) if type == 'cardinal': c = c or 0 for i in range(1, n): m[i] = w[i] = (1 - c) * ( y[i + 1] - y[i - 1]) / ( x[i + 1] - x[i - 1]) if x[i + 1] - x[i - 1] else 0 def p(i, x_): t = (x_ - x[i]) / delta_x[i] t2 = t * t t3 = t2 * t h00 = 2 * t3 - 3 * t2 + 1 h10 = t3 - 2 * t2 + t h01 = - 2 * t3 + 3 * t2 h11 = t3 - t2 return (h00 * y[i] + h10 * m[i] * delta_x[i] + h01 * y[i + 1] + h11 * w[i + 1] * delta_x[i]) for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision yield X, p(i, X) def lagrange_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using Lagrange polynomials https://en.wikipedia.org/wiki/Lagrange_polynomial """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision s = 0 for k in range(n + 1): p = 1 for m in range(n + 1): if m == k: continue if x[k] - x[m]: p *= (X - x[m]) / (x[k] - x[m]) s += y[k] * p yield X, s def trigonometric_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using trigonometric As per http://en.wikipedia.org/wiki/Trigonometric_interpolation """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision s = 0 for k in range(n + 1): p = 1 for m in range(n + 1): if m == k: continue if sin(0.5 * (x[k] - x[m])): p *= sin(0.5 * (X - x[m])) / sin(0.5 * (x[k] - x[m])) s += y[k] * p yield X, s INTERPOLATIONS = { 'quadratic': quadratic_interpolate, 'cubic': cubic_interpolate, 'hermite': hermite_interpolate, 'lagrange': lagrange_interpolate, 'trigonometric': trigonometric_interpolate } if __name__ == '__main__': from pygal import XY points = [(.1, 7), (.3, -4), (.6, 10), (.9, 8), (1.4, 3), (1.7, 1)] xy = XY(show_dots=False) xy.add('normal', points) xy.add('quadratic', quadratic_interpolate(*zip(*points))) xy.add('cubic', cubic_interpolate(*zip(*points))) xy.add('lagrange', lagrange_interpolate(*zip(*points))) xy.add('trigonometric', trigonometric_interpolate(*zip(*points))) xy.add('hermite catmul_rom', hermite_interpolate( *zip(*points), type='catmul_rom')) xy.add('hermite finite_difference', hermite_interpolate( *zip(*points), type='finite_difference')) xy.add('hermite cardinal -.5', hermite_interpolate( *zip(*points), type='cardinal', c=-.5)) xy.add('hermite cardinal .5', hermite_interpolate( *zip(*points), type='cardinal', c=.5)) xy.add('hermite kochanek_bartels .5 .75 -.25', hermite_interpolate( *zip(*points), type='kochanek_bartels', c=.5, b=.75, t=-.25)) xy.add('hermite kochanek_bartels .25 -.75 .5', hermite_interpolate( *zip(*points), type='kochanek_bartels', c=.25, b=-.75, t=.5)) xy.render_in_browser() pygal-2.4.0/pygal/config.py0000644000175000017500000004010413114033322015533 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Config module holding all options and their default values.""" from copy import deepcopy from pygal import formatters from pygal.interpolate import INTERPOLATIONS from pygal.style import DefaultStyle, Style CONFIG_ITEMS = [] callable = type(lambda: 1) class Key(object): """ Represents a config parameter. A config parameter has a name, a default value, a type, a category, a documentation, an optional longer documentatation and an optional subtype for list style option. Most of these informations are used in cabaret to auto generate forms representing these options. """ _categories = [] def __init__( self, default_value, type_, category, doc, subdoc="", subtype=None): """Create a configuration key""" self.value = default_value self.type = type_ self.doc = doc self.category = category self.subdoc = subdoc self.subtype = subtype self.name = "Unbound" if category not in self._categories: self._categories.append(category) CONFIG_ITEMS.append(self) def __repr__(self): """ Make a documentation repr. This is a hack to generate doc from inner doc """ return """ Type: %s%s      Default: %r      %s%s """ % ( self.type.__name__, (' of %s' % self.subtype.__name__) if self.subtype else '', self.value, self.doc, (' %s' % self.subdoc) if self.subdoc else '' ) @property def is_boolean(self): """Return `True` if this parameter is a boolean""" return self.type == bool @property def is_numeric(self): """Return `True` if this parameter is numeric (int or float)""" return self.type in (int, float) @property def is_string(self): """Return `True` if this parameter is a string""" return self.type == str @property def is_dict(self): """Return `True` if this parameter is a mapping""" return self.type == dict @property def is_list(self): """Return `True` if this parameter is a list""" return self.type == list def coerce(self, value): """Cast a string into this key type""" if self.type == Style: return value elif self.type == list: return self.type( map( self.subtype, map( lambda x: x.strip(), value.split(',')))) elif self.type == dict: rv = {} for pair in value.split(','): key, val = pair.split(':') key = key.strip() val = val.strip() try: rv[key] = self.subtype(val) except: rv[key] = val return rv return self.type(value) class MetaConfig(type): """Config metaclass. Used to get the key name and set it on the value.""" def __new__(mcs, classname, bases, classdict): """Get the name of the key and set it on the key""" for k, v in classdict.items(): if isinstance(v, Key): v.name = k return type.__new__(mcs, classname, bases, classdict) class BaseConfig(MetaConfig('ConfigBase', (object,), {})): """ This class holds the common method for configs. A config object can be instanciated with keyword arguments and updated on call with keyword arguments. """ def __init__(self, **kwargs): """Can be instanciated with config kwargs""" for k in dir(self): v = getattr(self, k) if (k not in self.__dict__ and not k.startswith('_') and not hasattr(v, '__call__')): if isinstance(v, Key): if v.is_list and v.value is not None: v = list(v.value) else: v = v.value setattr(self, k, v) self._update(kwargs) def __call__(self, **kwargs): """Can be updated with kwargs""" self._update(kwargs) def _update(self, kwargs): """Update the config with the given dictionary""" from pygal.util import merge dir_self_set = set(dir(self)) merge( self.__dict__, dict([ (k, v) for (k, v) in kwargs.items() if not k.startswith('_') and k in dir_self_set])) def to_dict(self): """Export a JSON serializable dictionary of the config""" config = {} for attr in dir(self): if not attr.startswith('__'): value = getattr(self, attr) if hasattr(value, 'to_dict'): config[attr] = value.to_dict() elif not hasattr(value, '__call__'): config[attr] = value return config def copy(self): """Copy this config object into another""" return deepcopy(self) class CommonConfig(BaseConfig): """Class holding options used in both chart and serie configuration""" stroke = Key( True, bool, "Look", "Line dots (set it to false to get a scatter plot)") show_dots = Key(True, bool, "Look", "Set to false to remove dots") show_only_major_dots = Key( False, bool, "Look", "Set to true to show only major dots according to their majored label") dots_size = Key(2.5, float, "Look", "Radius of the dots") fill = Key( False, bool, "Look", "Fill areas under lines") stroke_style = Key(None, dict, "Look", "Stroke style of serie element.", "This is a dict which can contain a " "'width', 'linejoin', 'linecap', 'dasharray' " "and 'dashoffset'") rounded_bars = Key( None, int, "Look", "Set this to the desired radius in px (for Bar-like charts)") inner_radius = Key( 0, float, "Look", "Piechart inner radius (donut), must be <.9") allow_interruptions = Key( False, bool, "Look", "Break lines on None values") formatter = Key( None, callable, "Value", "A function to convert raw value to strings for this chart or serie", "Default to value_formatter in most charts, it depends on dual charts." "(Can be overriden by value with the formatter metadata.)") class Config(CommonConfig): """Class holding config values""" style = Key( DefaultStyle, Style, "Style", "Style holding values injected in css") css = Key( ('file://style.css', 'file://graph.css'), list, "Style", "List of css file", "It can be any uri from file:///tmp/style.css to //domain/style.css", str) classes = Key( ('pygal-chart',), list, "Style", "Classes of the root svg node", str) defs = Key( [], list, "Misc", "Extraneous defs to be inserted in svg", "Useful for adding gradients / patterns…", str) # Look # title = Key( None, str, "Look", "Graph title.", "Leave it to None to disable title.") x_title = Key( None, str, "Look", "Graph X-Axis title.", "Leave it to None to disable X-Axis title.") y_title = Key( None, str, "Look", "Graph Y-Axis title.", "Leave it to None to disable Y-Axis title.") width = Key( 800, int, "Look", "Graph width") height = Key( 600, int, "Look", "Graph height") show_x_guides = Key(False, bool, "Look", "Set to true to always show x guide lines") show_y_guides = Key(True, bool, "Look", "Set to false to hide y guide lines") show_legend = Key( True, bool, "Look", "Set to false to remove legend") legend_at_bottom = Key( False, bool, "Look", "Set to true to position legend at bottom") legend_at_bottom_columns = Key( None, int, "Look", "Set to true to position legend at bottom") legend_box_size = Key( 12, int, "Look", "Size of legend boxes") rounded_bars = Key( None, int, "Look", "Set this to the desired radius in px") stack_from_top = Key( False, bool, "Look", "Stack from top to zero, this makes the stacked " "data match the legend order") spacing = Key( 10, int, "Look", "Space between titles/legend/axes") margin = Key( 20, int, "Look", "Margin around chart") margin_top = Key( None, int, "Look", "Margin around top of chart") margin_right = Key( None, int, "Look", "Margin around right of chart") margin_bottom = Key( None, int, "Look", "Margin around bottom of chart") margin_left = Key( None, int, "Look", "Margin around left of chart") tooltip_border_radius = Key(0, int, "Look", "Tooltip border radius") tooltip_fancy_mode = Key( True, bool, "Look", "Fancy tooltips", "Print legend, x label in tooltip and use serie color for value.") inner_radius = Key( 0, float, "Look", "Piechart inner radius (donut), must be <.9") half_pie = Key( False, bool, "Look", "Create a half-pie chart") x_labels = Key( None, list, "Label", "X labels, must have same len than data.", "Leave it to None to disable x labels display.", str) x_labels_major = Key( None, list, "Label", "X labels that will be marked major.", subtype=str) x_labels_major_every = Key( None, int, "Label", "Mark every n-th x label as major.") x_labels_major_count = Key( None, int, "Label", "Mark n evenly distributed labels as major.") show_x_labels = Key( True, bool, "Label", "Set to false to hide x-labels") show_minor_x_labels = Key( True, bool, "Label", "Set to false to hide x-labels not marked major") y_labels = Key( None, list, "Label", "You can specify explicit y labels", "Must be a list of numbers", float) y_labels_major = Key( None, list, "Label", "Y labels that will be marked major. Default: auto", subtype=str) y_labels_major_every = Key( None, int, "Label", "Mark every n-th y label as major.") y_labels_major_count = Key( None, int, "Label", "Mark n evenly distributed y labels as major.") show_minor_y_labels = Key( True, bool, "Label", "Set to false to hide y-labels not marked major") show_y_labels = Key( True, bool, "Label", "Set to false to hide y-labels") x_label_rotation = Key( 0, int, "Label", "Specify x labels rotation angles", "in degrees") y_label_rotation = Key( 0, int, "Label", "Specify y labels rotation angles", "in degrees") missing_value_fill_truncation = Key( "x", str, "Look", "Filled series with missing x and/or y values at the end of a series " "are closed at the first value with a missing " "'x' (default), 'y' or 'either'") # Value # x_value_formatter = Key( formatters.default, callable, "Value", "A function to convert abscissa numeric value to strings " "(used in XY and Date charts)") value_formatter = Key( formatters.default, callable, "Value", "A function to convert ordinate numeric value to strings") logarithmic = Key( False, bool, "Value", "Display values in logarithmic scale") interpolate = Key( None, str, "Value", "Interpolation", "May be %s" % ' or '.join(INTERPOLATIONS)) interpolation_precision = Key( 250, int, "Value", "Number of interpolated points between two values") interpolation_parameters = Key( {}, dict, "Value", "Various parameters for parametric interpolations", "ie: For hermite interpolation, you can set the cardinal tension with" "{'type': 'cardinal', 'c': .5}", int) box_mode = Key( 'extremes', str, "Value", "Sets the mode to be used. " "(Currently only supported on box plot)", "May be %s" % ' or '.join([ "1.5IQR", "extremes", "tukey", "stdev", "pstdev"])) order_min = Key( None, int, "Value", "Minimum order of scale, defaults to None") min_scale = Key( 4, int, "Value", "Minimum number of scale graduation for auto scaling") max_scale = Key( 16, int, "Value", "Maximum number of scale graduation for auto scaling") range = Key( None, list, "Value", "Explicitly specify min and max of values", "(ie: (0, 100))", int) secondary_range = Key( None, list, "Value", "Explicitly specify min and max of secondary values", "(ie: (0, 100))", int) xrange = Key( None, list, "Value", "Explicitly specify min and max of x values " "(used in XY and Date charts)", "(ie: (0, 100))", int) include_x_axis = Key( False, bool, "Value", "Always include x axis") zero = Key( 0, int, "Value", "Set the ordinate zero value", "Useful for filling to another base than abscissa") # Text # no_data_text = Key( "No data", str, "Text", "Text to display when no data is given") print_values = Key( False, bool, "Text", "Display values as text over plot") dynamic_print_values = Key( False, bool, "Text", "Show values only on hover") print_values_position = Key( 'center', str, "Text", "Customize position of `print_values`. " "(For bars: `top`, `center` or `bottom`)") print_zeroes = Key( True, bool, "Text", "Display zero values as well") print_labels = Key( False, bool, "Text", "Display value labels") truncate_legend = Key( None, int, "Text", "Legend string length truncation threshold", "None = auto, Negative for none") truncate_label = Key( None, int, "Text", "Label string length truncation threshold", "None = auto, Negative for none") # Misc # js = Key( ('//kozea.github.io/pygal.js/2.0.x/pygal-tooltips.min.js',), list, "Misc", "List of js file", "It can be any uri from file:///tmp/ext.js to //domain/ext.js", str) disable_xml_declaration = Key( False, bool, "Misc", "Don't write xml declaration and return str instead of string", "useful for writing output directly in html") force_uri_protocol = Key( 'https', str, "Misc", "Default uri protocol", "Default protocol for external files. " "Can be set to None to use a // uri") explicit_size = Key( False, bool, "Misc", "Write width and height attributes") pretty_print = Key( False, bool, "Misc", "Pretty print the svg") strict = Key( False, bool, "Misc", "If True don't try to adapt / filter wrong values") no_prefix = Key( False, bool, "Misc", "Don't prefix css") inverse_y_axis = Key(False, bool, "Misc", "Inverse Y axis direction") class SerieConfig(CommonConfig): """Class holding serie config values""" title = Key( None, str, "Look", "Serie title.", "Leave it to None to disable title.") secondary = Key( False, bool, "Misc", "Set it to put the serie in a second axis") pygal-2.4.0/pygal/util.py0000644000175000017500000002624313114033322015253 0ustar zerozero00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . """Various utility functions""" from __future__ import division import re from decimal import Decimal from math import ceil, cos, floor, log10, pi, sin from pygal._compat import _ellipsis, to_unicode, u def float_format(number): """Format a float to a precision of 3, without zeroes or dots""" return ("%.3f" % number).rstrip('0').rstrip('.') def majorize(values): """Filter sequence to return only major considered numbers""" sorted_values = sorted(values) if len(values) <= 3 or ( abs(2 * sorted_values[1] - sorted_values[0] - sorted_values[2]) > abs(1.5 * (sorted_values[1] - sorted_values[0]))): return [] values_step = sorted_values[1] - sorted_values[0] full_range = sorted_values[-1] - sorted_values[0] step = 10 ** int(log10(full_range)) if step == values_step: step *= 10 step_factor = 10 ** (int(log10(step)) + 1) if round(step * step_factor) % (round(values_step * step_factor) or 1): # TODO: Find lower common multiple instead step *= values_step if full_range <= 2 * step: step *= .5 elif full_range >= 5 * step: step *= 5 major_values = [ value for value in values if value / step == round(value / step)] return [value for value in sorted_values if value in major_values] def round_to_int(number, precision): """Round a number to a precision""" precision = int(precision) rounded = (int(number) + precision / 2) // precision * precision return rounded def round_to_float(number, precision): """Round a float to a precision""" rounded = Decimal( str(floor((number + precision / 2) // precision)) ) * Decimal(str(precision)) return float(rounded) def round_to_scale(number, precision): """Round a number or a float to a precision""" if precision < 1: return round_to_float(number, precision) return round_to_int(number, precision) def cut(list_, index=0): """Cut a list by index or arg""" if isinstance(index, int): cut_ = lambda x: x[index] else: cut_ = lambda x: getattr(x, index) return list(map(cut_, list_)) def rad(degrees): """Convert degrees in radiants""" return pi * degrees / 180 def deg(radiants): """Convert radiants in degrees""" return 180 * radiants / pi def _swap_curly(string): """Swap single and double curly brackets""" return (string .replace('{{ ', '{{') .replace('{{', '\x00') .replace('{', '{{') .replace('\x00', '{') .replace(' }}', '}}') .replace('}}', '\x00') .replace('}', '}}') .replace('\x00', '}')) def template(string, **kwargs): """Format a string using double braces""" return _swap_curly(string).format(**kwargs) swap = lambda tuple_: tuple(reversed(tuple_)) ident = lambda x: x def compute_logarithmic_scale(min_, max_, min_scale, max_scale): """Compute an optimal scale for logarithmic""" if max_ <= 0 or min_ <= 0: return [] min_order = int(floor(log10(min_))) max_order = int(ceil(log10(max_))) positions = [] amplitude = max_order - min_order if amplitude <= 1: return [] detail = 10. while amplitude * detail < min_scale * 5: detail *= 2 while amplitude * detail > max_scale * 3: detail /= 2 for order in range(min_order, max_order + 1): for i in range(int(detail)): tick = (10 * i / detail or 1) * 10 ** order tick = round_to_scale(tick, tick) if min_ <= tick <= max_ and tick not in positions: positions.append(tick) return positions def compute_scale( min_, max_, logarithmic, order_min, min_scale, max_scale): """Compute an optimal scale between min and max""" if min_ == 0 and max_ == 0: return [0] if max_ - min_ == 0: return [min_] if logarithmic: log_scale = compute_logarithmic_scale( min_, max_, min_scale, max_scale) if log_scale: return log_scale # else we fallback to normal scalling order = round(log10(max(abs(min_), abs(max_)))) - 1 if order_min is not None and order < order_min: order = order_min else: while ((max_ - min_) / (10 ** order) < min_scale and (order_min is None or order > order_min)): order -= 1 step = float(10 ** order) while (max_ - min_) / step > max_scale: step *= 2. positions = [] position = round_to_scale(min_, step) while position < (max_ + step): rounded = round_to_scale(position, step) if min_ <= rounded <= max_: if rounded not in positions: positions.append(rounded) position += step if len(positions) < 2: return [min_, max_] return positions def text_len(length, fs): """Approximation of text width""" return length * 0.6 * fs def reverse_text_len(width, fs): """Approximation of text length""" return int(width / (0.6 * fs)) def get_text_box(text, fs): """Approximation of text bounds""" return (fs, text_len(len(text), fs)) def get_texts_box(texts, fs): """Approximation of multiple texts bounds""" max_len = max(map(len, texts)) return (fs, text_len(max_len, fs)) def decorate(svg, node, metadata): """Add metedata next to a node""" if not metadata: return node xlink = metadata.get('xlink') if xlink: if not isinstance(xlink, dict): xlink = {'href': xlink, 'target': '_blank'} node = svg.node(node, 'a', **xlink) svg.node(node, 'desc', class_='xlink').text = to_unicode( xlink.get('href')) if 'tooltip' in metadata: svg.node(node, 'title').text = to_unicode( metadata['tooltip']) if 'color' in metadata: color = metadata.pop('color') node.attrib['style'] = 'fill: %s; stroke: %s' % ( color, color) if 'style' in metadata: node.attrib['style'] = metadata.pop('style') if 'label' in metadata: svg.node(node, 'desc', class_='label').text = to_unicode( metadata['label']) return node def alter(node, metadata): """Override nodes attributes from metadata node mapping""" if node is not None and metadata and 'node' in metadata: node.attrib.update( dict((k, str(v)) for k, v in metadata['node'].items())) def truncate(string, index): """Truncate a string at index and add ...""" if len(string) > index and index > 0: string = string[:index - 1] + u('…') return string # # Stolen partly from brownie http://packages.python.org/Brownie/ class cached_property(object): """Memoize a property""" def __init__(self, getter, doc=None): """Initialize the decorator""" self.getter = getter self.__module__ = getter.__module__ self.__name__ = getter.__name__ self.__doc__ = doc or getter.__doc__ def __get__(self, obj, type_=None): """ Get descriptor calling the property function and replacing it with its value or on state if we are in the transient state. """ if obj is None: return self value = self.getter(obj) if hasattr(obj, 'state'): setattr(obj.state, self.__name__, value) else: obj.__dict__[self.__name__] = self.getter(obj) return value css_comments = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL) def minify_css(css): """Little css minifier""" # Inspired by slimmer by Peter Bengtsson remove_next_comment = 1 for css_comment in css_comments.findall(css): if css_comment[-3:] == '\*/': remove_next_comment = 0 continue if remove_next_comment: css = css.replace(css_comment, '') else: remove_next_comment = 1 # >= 2 whitespace becomes one whitespace css = re.sub(r'\s\s+', ' ', css) # no whitespace before end of line css = re.sub(r'\s+\n', '', css) # Remove space before and after certain chars for char in ('{', '}', ':', ';', ','): css = re.sub(char + r'\s', char, css) css = re.sub(r'\s' + char, char, css) css = re.sub(r'}\s(#|\w)', r'}\1', css) # no need for the ; before end of attributes css = re.sub(r';}', r'}', css) css = re.sub(r'}//-->', r'}\n//-->', css) return css.strip() def compose(f, g): """Chain functions""" fun = lambda *args, **kwargs: f(g(*args, **kwargs)) fun.__name__ = "%s o %s" % (f.__name__, g.__name__) return fun def safe_enumerate(iterable): """Enumerate which does not yield None values""" for i, v in enumerate(iterable): if v is not None: yield i, v def split_title(title, width, title_fs): """Split a string for a specified width and font size""" titles = [] if not title: return titles size = reverse_text_len(width, title_fs * 1.1) title_lines = title.split("\n") for title_line in title_lines: while len(title_line) > size: title_part = title_line[:size] i = title_part.rfind(' ') if i == -1: i = len(title_part) titles.append(title_part[:i]) title_line = title_line[i:].strip() titles.append(title_line) return titles def filter_kwargs(fun, kwargs): if not hasattr(fun, '__code__'): return {} args = fun.__code__.co_varnames[1:] return dict((k, v) for k, v in kwargs.items() if k in args) def coord_project(rho, alpha): return rho * sin(-alpha), rho * cos(-alpha) def coord_diff(x, y): return (x[0] - y[0], x[1] - y[1]) def coord_format(x): return '%f %f' % x def coord_dual(r): return coord_format((r, r)) def coord_abs_project(center, rho, theta): return coord_format(coord_diff(center, coord_project(rho, theta))) def mergextend(list1, list2): if list1 is None or _ellipsis not in list1: return list1 index = list1.index(_ellipsis) return list(list1[:index]) + list(list2) + list(list1[index + 1:]) def merge(dict1, dict2): from pygal.config import CONFIG_ITEMS, Key _list_items = [item.name for item in CONFIG_ITEMS if item.type == list] for key, val in dict2.items(): if isinstance(val, Key): val = val.value if key in _list_items: dict1[key] = mergextend(val, dict1.get(key, ())) else: dict1[key] = val pygal-2.4.0/setup.py0000644000175000017500000000605213127127430014326 0ustar zerozero00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library 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. # # This library 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 pygal. If not, see . import os import sys from setuptools import find_packages, setup from setuptools.command.test import test as TestCommand class PyTest(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = ['-x', 'build/lib/pygal'] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.test_args) sys.exit(errno) ROOT = os.path.dirname(__file__) # Explicitly specify the encoding of pygal/__init__.py if we're on py3. kwargs = {} if sys.version_info[0] == 3: kwargs['encoding'] = 'utf-8' cairosvg = 'cairosvg' else: cairosvg = 'cairosvg==0.5' tests_requirements = [ "pyquery", "flask", cairosvg, 'lxml', 'pygal_maps_world', 'pygal_maps_fr', 'pygal_maps_ch', 'coveralls', 'pytest-runner', 'pytest-cov', 'pytest-flake8', 'pytest-isort', 'pytest' ] about = {} with open(os.path.join( os.path.dirname(__file__), "pygal", "__about__.py")) as f: exec(f.read(), about) setup( name=about['__title__'], version=about['__version__'], description=about['__summary__'], url=about['__uri__'], author=about['__author__'], author_email=about['__email__'], license=about['__license__'], platforms="Any", packages=find_packages(), provides=['pygal'], scripts=["pygal_gen.py"], keywords=[ "svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"], setup_requires=['pytest-runner'], test_requires=tests_requirements, cmdclass={'test': PyTest}, package_data={'pygal': ['css/*', 'graph/maps/*.svg']}, extras_require={ 'lxml': ['lxml'], 'docs': ['sphinx', 'sphinx_rtd_theme', 'pygal_sphinx_directives'], 'png': [cairosvg], 'test': tests_requirements }, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: " "GNU Lesser General Public License v3 or later (LGPLv3+)", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: Presentation"]) pygal-2.4.0/PKG-INFO0000644000175000017500000000134613127127737013724 0ustar zerozero00000000000000Metadata-Version: 1.1 Name: pygal Version: 2.4.0 Summary: A python svg graph plotting library Home-page: http://pygal.org/ Author: Florian Mounier Author-email: florian.mounier@kozea.fr License: GNU LGPL v3+ Description: UNKNOWN Keywords: svg,chart,graph,diagram,plot,histogram,kiviat Platform: Any Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics :: Presentation Provides: pygal pygal-2.4.0/pygal.egg-info/0000755000175000017500000000000013127127737015431 5ustar zerozero00000000000000pygal-2.4.0/pygal.egg-info/requires.txt0000644000175000017500000000035313127127737020032 0ustar zerozero00000000000000 [docs] sphinx sphinx_rtd_theme pygal_sphinx_directives [lxml] lxml [png] cairosvg [test] pyquery flask cairosvg lxml pygal_maps_world pygal_maps_fr pygal_maps_ch coveralls pytest-runner pytest-cov pytest-flake8 pytest-isort pytest pygal-2.4.0/pygal.egg-info/SOURCES.txt0000644000175000017500000000341713127127737017322 0ustar zerozero00000000000000README pygal_gen.py setup.cfg setup.py pygal/__about__.py pygal/__init__.py pygal/_compat.py pygal/adapters.py pygal/colors.py pygal/config.py pygal/etree.py pygal/formatters.py pygal/interpolate.py pygal/serie.py pygal/state.py pygal/stats.py pygal/style.py pygal/svg.py pygal/table.py pygal/util.py pygal/view.py pygal.egg-info/PKG-INFO pygal.egg-info/SOURCES.txt pygal.egg-info/dependency_links.txt pygal.egg-info/requires.txt pygal.egg-info/top_level.txt pygal/css/base.css pygal/css/graph.css pygal/css/style.css pygal/graph/__init__.py pygal/graph/bar.py pygal/graph/base.py pygal/graph/box.py pygal/graph/dot.py pygal/graph/dual.py pygal/graph/funnel.py pygal/graph/gauge.py pygal/graph/graph.py pygal/graph/histogram.py pygal/graph/horizontal.py pygal/graph/horizontalbar.py pygal/graph/horizontalline.py pygal/graph/horizontalstackedbar.py pygal/graph/horizontalstackedline.py pygal/graph/line.py pygal/graph/map.py pygal/graph/pie.py pygal/graph/public.py pygal/graph/pyramid.py pygal/graph/radar.py pygal/graph/solidgauge.py pygal/graph/stackedbar.py pygal/graph/stackedline.py pygal/graph/time.py pygal/graph/treemap.py pygal/graph/xy.py pygal/maps/__init__.py pygal/test/__init__.py pygal/test/conftest.py pygal/test/test_bar.py pygal/test/test_box.py pygal/test/test_colors.py pygal/test/test_config.py pygal/test/test_date.py pygal/test/test_formatters.py pygal/test/test_graph.py pygal/test/test_histogram.py pygal/test/test_interpolate.py pygal/test/test_line.py pygal/test/test_line_log_none_max_solved.py pygal/test/test_maps.py pygal/test/test_pie.py pygal/test/test_serie_config.py pygal/test/test_sparktext.py pygal/test/test_stacked.py pygal/test/test_style.py pygal/test/test_table.py pygal/test/test_util.py pygal/test/test_view.py pygal/test/test_xml_filters.py pygal/test/utils.pypygal-2.4.0/pygal.egg-info/top_level.txt0000644000175000017500000000000613127127737020157 0ustar zerozero00000000000000pygal pygal-2.4.0/pygal.egg-info/dependency_links.txt0000644000175000017500000000000113127127737021477 0ustar zerozero00000000000000 pygal-2.4.0/pygal.egg-info/PKG-INFO0000644000175000017500000000134613127127737016532 0ustar zerozero00000000000000Metadata-Version: 1.1 Name: pygal Version: 2.4.0 Summary: A python svg graph plotting library Home-page: http://pygal.org/ Author: Florian Mounier Author-email: florian.mounier@kozea.fr License: GNU LGPL v3+ Description: UNKNOWN Keywords: svg,chart,graph,diagram,plot,histogram,kiviat Platform: Any Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics :: Presentation Provides: pygal