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:
commit
5dd79b8c89
|
@ -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
|
|
@ -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/>.
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
pygal
|
|
@ -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__'
|
||||
]
|
|
@ -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()]
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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")
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 }}
|
|
@ -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()
|
|
@ -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()
|
|
@ -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"""
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
])
|
|
@ -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]
|
|
@ -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]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"""
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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"""
|
|
@ -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))
|
|
@ -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.')
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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"""
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
])
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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('3¥')) + (
|
||||
('6_a$', u('15€')) if Chart in (Pie, SolidGauge) else ()))
|
||||
|
||||
|
||||
def test_classes(Chart):
|
||||
"""Test classes option"""
|
||||
chart = Chart()
|
||||
assert chart.render_pyquery().attr('class') == 'pygal-chart'
|
||||
|
||||
chart = Chart(classes=())
|
||||
assert not chart.render_pyquery().attr('class')
|
||||
|
||||
chart = Chart(classes=(_ellipsis,))
|
||||
assert chart.render_pyquery().attr('class') == 'pygal-chart'
|
||||
|
||||
chart = Chart(classes=('graph',))
|
||||
assert chart.render_pyquery().attr('class') == 'graph'
|
||||
|
||||
chart = Chart(classes=('pygal-chart', 'graph'))
|
||||
assert chart.render_pyquery().attr('class') == 'pygal-chart graph'
|
||||
|
||||
chart = Chart(classes=(_ellipsis, 'graph'))
|
||||
assert chart.render_pyquery().attr('class') == 'pygal-chart graph'
|
||||
|
||||
chart = Chart(classes=('graph', _ellipsis))
|
||||
assert chart.render_pyquery().attr('class') == 'graph pygal-chart'
|
|
@ -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)
|
|
@ -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('1µ')
|
||||
assert f(0.0000001) == u('100n')
|
||||
assert f(0.0000000001) == u('100p')
|
||||
|
||||
assert f(0) == '0'
|
||||
assert f(0.) == '0'
|
||||
assert f(-1337) == '-1.337k'
|
||||
assert f(-.000000042) == '-42n'
|
||||
|
||||
|
||||
def test_human_readable_custom():
|
||||
"""Test human_readable formatter option"""
|
||||
f = formatters.HumanReadable()
|
||||
assert f(None) == u('∅')
|
||||
f = formatters.HumanReadable(none_char='/')
|
||||
assert f(None) == '/'
|
||||
|
||||
|
||||
def test_significant():
|
||||
"""Test significant formatter"""
|
||||
f = formatters.significant
|
||||
assert f(1) == '1'
|
||||
assert f(1.) == '1'
|
||||
assert f(-1.) == '-1'
|
||||
assert f(10) == '10'
|
||||
assert f(10000000000) == '1e+10'
|
||||
assert f(100000000000) == '1e+11'
|
||||
assert f(120000000000) == '1.2e+11'
|
||||
|
||||
assert f(.1) == '0.1'
|
||||
assert f(.01) == '0.01'
|
||||
assert f(.0000000001) == '1e-10'
|
||||
assert f(-.0000000001) == '-1e-10'
|
||||
assert f(.0000000001002) == '1.002e-10'
|
||||
|
||||
assert f(.0000000001002) == '1.002e-10'
|
||||
|
||||
assert f(.12345678912345) == '0.1234567891'
|
||||
assert f(.012345678912345) == '0.01234567891'
|
||||
|
||||
assert f(12345678912345) == '1.234567891e+13'
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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('▁▁▁▁▁')
|
|
@ -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)'))
|
|
@ -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()
|
|
@ -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
|
|
@ -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']
|
|
@ -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()
|
|
@ -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)"
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
@ -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"])
|
Loading…
Reference in New Issue