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