Import python-pygal_2.4.0.orig.tar.gz

[dgit import orig python-pygal_2.4.0.orig.tar.gz]
This commit is contained in:
Ulises Vitulli 2017-09-16 01:03:42 +02:00
commit 5dd79b8c89
82 changed files with 11831 additions and 0 deletions

20
PKG-INFO Normal file
View File

@ -0,0 +1,20 @@
Metadata-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

67
README Normal file
View File

@ -0,0 +1,67 @@
# 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 <http://www.gnu.org/licenses/>.

20
pygal.egg-info/PKG-INFO Normal file
View File

@ -0,0 +1,20 @@
Metadata-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

View File

@ -0,0 +1,81 @@
README
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.py

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,26 @@
[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

View File

@ -0,0 +1 @@
pygal

15
pygal/__about__.py Normal file
View File

@ -0,0 +1,15 @@
__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__'
]

109
pygal/__init__.py Normal file
View File

@ -0,0 +1,109 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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()]

103
pygal/_compat.py Normal file
View File

@ -0,0 +1,103 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

54
pygal/adapters.py Normal file
View File

@ -0,0 +1,54 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

204
pygal/colors.py Normal file
View File

@ -0,0 +1,204 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

545
pygal/config.py Normal file
View File

@ -0,0 +1,545 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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")

69
pygal/css/base.css Normal file
View File

@ -0,0 +1,69 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* 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;
}

152
pygal/css/graph.css Normal file
View File

@ -0,0 +1,152 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
{{ 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;
}

174
pygal/css/style.css Normal file
View File

@ -0,0 +1,174 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* 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 }}

65
pygal/etree.py Normal file
View File

@ -0,0 +1,65 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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()

113
pygal/formatters.py Normal file
View File

@ -0,0 +1,113 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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()

20
pygal/graph/__init__.py Normal file
View File

@ -0,0 +1,20 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""Graph package containing all builtin charts"""

148
pygal/graph/bar.py Normal file
View File

@ -0,0 +1,148 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

234
pygal/graph/base.py Normal file
View File

@ -0,0 +1,234 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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()

287
pygal/graph/box.py Normal file
View File

@ -0,0 +1,287 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

127
pygal/graph/dot.py Normal file
View File

@ -0,0 +1,127 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

71
pygal/graph/dual.py Normal file
View File

@ -0,0 +1,71 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

111
pygal/graph/funnel.py Normal file
View File

@ -0,0 +1,111 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

177
pygal/graph/gauge.py Normal file
View File

@ -0,0 +1,177 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

945
pygal/graph/graph.py Normal file
View File

@ -0,0 +1,945 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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
])

133
pygal/graph/histogram.py Normal file
View File

@ -0,0 +1,133 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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]

66
pygal/graph/horizontal.py Normal file
View File

@ -0,0 +1,66 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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]

View File

@ -0,0 +1,35 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

View File

@ -0,0 +1,35 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

View File

@ -0,0 +1,28 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""Horizontal stacked graph"""
from pygal.graph.horizontal import HorizontalGraph
from pygal.graph.stackedbar import StackedBar
class HorizontalStackedBar(HorizontalGraph, StackedBar):
"""Horizontal Stacked Bar graph"""

View File

@ -0,0 +1,35 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

210
pygal/graph/line.py Normal file
View File

@ -0,0 +1,210 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

132
pygal/graph/map.py Normal file
View File

@ -0,0 +1,132 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

106
pygal/graph/pie.py Normal file
View File

@ -0,0 +1,106 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

167
pygal/graph/public.py Normal file
View File

@ -0,0 +1,167 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

86
pygal/graph/pyramid.py Normal file
View File

@ -0,0 +1,86 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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"""

209
pygal/graph/radar.py Normal file
View File

@ -0,0 +1,209 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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))

153
pygal/graph/solidgauge.py Normal file
View File

@ -0,0 +1,153 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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.')

146
pygal/graph/stackedbar.py Normal file
View File

@ -0,0 +1,146 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -0,0 +1,89 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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)

149
pygal/graph/time.py Normal file
View File

@ -0,0 +1,149 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

143
pygal/graph/treemap.py Normal file
View File

@ -0,0 +1,143 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

125
pygal/graph/xy.py Normal file
View File

@ -0,0 +1,125 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

264
pygal/interpolate.py Normal file
View File

@ -0,0 +1,264 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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()

20
pygal/maps/__init__.py Normal file
View File

@ -0,0 +1,20 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""Maps extensions namespace module"""

39
pygal/serie.py Normal file
View File

@ -0,0 +1,39 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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))

38
pygal/state.py Normal file
View File

@ -0,0 +1,38 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

73
pygal/stats.py Normal file
View File

@ -0,0 +1,73 @@
from 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)

532
pygal/style.py Normal file
View File

@ -0,0 +1,532 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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
}

498
pygal/svg.py Normal file
View File

@ -0,0 +1,498 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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"<?xml version='1.0' encoding='utf-8'?>\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)

205
pygal/table.py Normal file
View File

@ -0,0 +1,205 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
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

58
pygal/test/__init__.py Normal file
View File

@ -0,0 +1,58 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

61
pygal/test/conftest.py Normal file
View File

@ -0,0 +1,61 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)
])

37
pygal/test/test_bar.py Normal file
View File

@ -0,0 +1,37 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

169
pygal/test/test_box.py Normal file
View File

@ -0,0 +1,169 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

546
pygal/test/test_colors.py Normal file
View File

@ -0,0 +1,546 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

573
pygal/test/test_config.py Normal file
View File

@ -0,0 +1,573 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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('')) + (
('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'

177
pygal/test/test_date.py Normal file
View File

@ -0,0 +1,177 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

View File

@ -0,0 +1,88 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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('')
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'

442
pygal/test/test_graph.py Normal file
View File

@ -0,0 +1,442 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -0,0 +1,37 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -0,0 +1,113 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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()

173
pygal/test/test_line.py Normal file
View File

@ -0,0 +1,173 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -0,0 +1,13 @@
# 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

29
pygal/test/test_maps.py Normal file
View File

@ -0,0 +1,29 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

66
pygal/test/test_pie.py Normal file
View File

@ -0,0 +1,66 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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()

View File

@ -0,0 +1,73 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -0,0 +1,85 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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('▁▁▁▁▁')

View File

@ -0,0 +1,62 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)'))

48
pygal/test/test_style.py Normal file
View File

@ -0,0 +1,48 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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()

37
pygal/test/test_table.py Normal file
View File

@ -0,0 +1,37 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

176
pygal/test/test_util.py Normal file
View File

@ -0,0 +1,176 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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']

31
pygal/test/test_view.py Normal file
View File

@ -0,0 +1,31 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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()

View File

@ -0,0 +1,68 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)"

27
pygal/test/utils.py Normal file
View File

@ -0,0 +1,27 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""Tests helpers"""
from pyquery import PyQuery as pq
def texts(i, e):
"""Helper for getting the text of an element"""
return pq(e).text()

386
pygal/util.py Normal file
View File

@ -0,0 +1,386 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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

423
pygal/view.py Normal file
View File

@ -0,0 +1,423 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""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)

68
pygal_gen.py Executable file
View File

@ -0,0 +1,68 @@
#!/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 <http://www.gnu.org/licenses/>.
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)

14
setup.cfg Normal file
View File

@ -0,0 +1,14 @@
[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

96
setup.py Normal file
View File

@ -0,0 +1,96 @@
#!/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 <http://www.gnu.org/licenses/>.
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"])