././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0658777 pygal-3.0.5/0000775000175000017500000000000000000000000013416 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/COPYING0000664000175000017500000001674200000000000014463 0ustar00gillesgilles00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0658777 pygal-3.0.5/PKG-INFO0000644000175000017500000000666300000000000014524 0ustar00gillesgilles00000000000000Metadata-Version: 2.1 Name: pygal Version: 3.0.5 Summary: A Python svg graph plotting library Home-page: https://www.pygal.org/ Author: Florian Mounier / Kozea Author-email: community@kozea.fr License: GNU LGPL v3+ 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 :: 3 Classifier: Topic :: Multimedia :: Graphics :: Presentation Provides: pygal Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: COPYING Requires-Dist: importlib-metadata Provides-Extra: lxml Requires-Dist: lxml; extra == "lxml" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Requires-Dist: pygal_sphinx_directives; extra == "docs" Provides-Extra: png Requires-Dist: cairosvg; extra == "png" Provides-Extra: test Requires-Dist: cairosvg; extra == "test" Requires-Dist: coveralls; extra == "test" Requires-Dist: lxml; extra == "test" Requires-Dist: pyquery; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: ruff>=0.5.6; extra == "test" Provides-Extra: moulinrouge Requires-Dist: flask; extra == "moulinrouge" Requires-Dist: pygal_maps_ch; extra == "moulinrouge" Requires-Dist: pygal_maps_fr; extra == "moulinrouge" Requires-Dist: pygal_maps_world; extra == "moulinrouge" # 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 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/README0000664000175000017500000000353500000000000014304 0ustar00gillesgilles00000000000000# 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 . ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0578778 pygal-3.0.5/pygal/0000775000175000017500000000000000000000000014532 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474480.0 pygal-3.0.5/pygal/__about__.py0000664000175000017500000000064000000000000017012 0ustar00gillesgilles00000000000000__title__ = "pygal" __version__ = "3.0.5" __summary__ = "A Python svg graph plotting library" __uri__ = "https://www.pygal.org/" __author__ = "Florian Mounier / Kozea" __email__ = "community@kozea.fr" __license__ = "GNU LGPL v3+" __copyright__ = "Copyright 2020 %s" % __author__ __all__ = [ '__title__', '__version__', '__summary__', '__uri__', '__author__', '__email__', '__license__', '__copyright__' ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/__init__.py0000664000175000017500000000706000000000000016646 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Main pygal package. This package holds all available charts in pygal, the Config class and the maps extensions namespace module. """ from .__about__ import * # noqa: F401,F403 isort: skip import sys import traceback import warnings from importlib_metadata import entry_points from pygal import maps from pygal.config import Config 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.graph import Graph from pygal.graph.histogram import Histogram from pygal.graph.horizontalbar import HorizontalBar from pygal.graph.horizontalline import HorizontalLine from pygal.graph.horizontalstackedbar import HorizontalStackedBar from pygal.graph.horizontalstackedline import HorizontalStackedLine from pygal.graph.line import Line from pygal.graph.pie import Pie from pygal.graph.pyramid import Pyramid, VerticalPyramid from pygal.graph.radar import Radar from pygal.graph.solidgauge import SolidGauge from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedline import StackedLine from pygal.graph.time import DateLine, DateTimeLine, TimeDeltaLine, TimeLine from pygal.graph.treemap import Treemap from pygal.graph.xy import XY 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 entry_points(group="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()] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/_compat.py0000664000175000017500000000236200000000000016531 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Various hacks for former transparent python 2 / python 3 support""" import datetime from collections.abc import Iterable def is_list_like(value): """Return whether value is an iterable but not a mapping / string""" return isinstance(value, Iterable) and not isinstance(value, (str, dict)) def timestamp(x): """Get a timestamp from a date""" if x.tzinfo is None: # Naive dates to utc x = x.replace(tzinfo=datetime.timezone.utc) return x.timestamp() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/adapters.py0000664000175000017500000000256600000000000016720 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Value adapters to use when a chart doesn't accept all value types""" from decimal import Decimal def positive(x): """Return zero if value is negative""" if x is None: return if isinstance(x, str): 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/colors.py0000664000175000017500000001316500000000000016413 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ This package is an utility package oriented on color alteration. This is used by the :py:mod:`pygal.style` package to generate parametric styles. """ from __future__ import division def normalize_float(f): """Round float errors""" if abs(f - round(f)) < .0000000000001: return round(f) return f def rgb_to_hsl(r, g, b): """Convert a color in r, g, b to a color in h, s, l""" r = r or 0 g = g or 0 b = b or 0 r /= 255 g /= 255 b /= 255 max_ = max((r, g, b)) min_ = min((r, g, b)) d = max_ - min_ if not d: h = 0 elif r is max_: h = 60 * (g - b) / d elif g is max_: h = 60 * (b - r) / d + 120 else: h = 60 * (r - g) / d + 240 l = .5 * (max_ + min_) if not d: s = 0 elif l < 0.5: s = .5 * d / l else: s = .5 * d / (1 - l) return tuple(map(normalize_float, (h % 360, s * 100, l * 100))) def hsl_to_rgb(h, s, l): """Convert a color in h, s, l to a color in r, g, b""" h /= 360 s /= 100 l /= 100 m2 = l * (s + 1) if l <= .5 else l + s - l * s m1 = 2 * l - m2 def h_to_rgb(h): h = h % 1 if 6 * h < 1: return m1 + 6 * h * (m2 - m1) if 2 * h < 1: return m2 if 3 * h < 2: return m1 + 6 * (2 / 3 - h) * (m2 - m1) return m1 r, g, b = map( lambda x: round(x * 255), map(h_to_rgb, (h + 1 / 3, h, h - 1 / 3)) ) return r, g, b def parse_color(color): """Take any css color definition and give back a tuple containing the r, g, b, a values along with a type which can be: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb, rgba """ r = g = b = a = type = None if color.startswith('#'): color = color[1:] if len(color) == 3: type = '#rgb' color = color + 'f' if len(color) == 4: type = type or '#rgba' color = ''.join([c * 2 for c in color]) if len(color) == 6: type = type or '#rrggbb' color = color + 'ff' assert len(color) == 8 type = type or '#rrggbbaa' r, g, b, a = [ int(''.join(c), 16) for c in zip(color[::2], color[1::2]) ] a /= 255 elif color.startswith('rgb('): type = 'rgb' color = color[4:-1] r, g, b, a = [int(c) for c in color.split(',')] + [1] elif color.startswith('rgba('): type = 'rgba' color = color[5:-1] r, g, b, a = [int(c) for c in color.split(',')[:-1] ] + [float(color.split(',')[-1])] return r, g, b, a, type def unparse_color(r, g, b, a, type): """ Take the r, g, b, a color values and give back a type css color string. This is the inverse function of parse_color """ if type == '#rgb': # Don't lose precision on rgb shortcut if r % 17 == 0 and g % 17 == 0 and b % 17 == 0: return '#%x%x%x' % (int(r / 17), int(g / 17), int(b / 17)) type = '#rrggbb' if type == '#rgba': if r % 17 == 0 and g % 17 == 0 and b % 17 == 0: return '#%x%x%x%x' % ( int(r / 17), int(g / 17), int(b / 17), int(a * 15) ) type = '#rrggbbaa' if type == '#rrggbb': return '#%02x%02x%02x' % (r, g, b) if type == '#rrggbbaa': return '#%02x%02x%02x%02x' % (r, g, b, int(a * 255)) if type == 'rgb': return 'rgb(%d, %d, %d)' % (r, g, b) if type == 'rgba': return 'rgba(%d, %d, %d, %g)' % (r, g, b, a) def is_foreground_light(color): """ Determine if the background color need a light or dark foreground color """ return rgb_to_hsl(*parse_color(color)[:3])[2] < 17.9 _clamp = lambda x: max(0, min(100, x)) def _adjust(hsl, attribute, percent): """Internal adjust function""" hsl = list(hsl) if attribute > 0: hsl[attribute] = _clamp(hsl[attribute] + percent) else: hsl[attribute] += percent return hsl def adjust(color, attribute, percent): """Adjust an attribute of color by a percent""" r, g, b, a, type = parse_color(color) r, g, b = hsl_to_rgb(*_adjust(rgb_to_hsl(r, g, b), attribute, percent)) return unparse_color(r, g, b, a, type) def rotate(color, percent): """Rotate a color by changing its hue value by percent""" return adjust(color, 0, percent) def saturate(color, percent): """Saturate a color by increasing its saturation by percent""" return adjust(color, 1, percent) def desaturate(color, percent): """Desaturate a color by decreasing its saturation by percent""" return adjust(color, 1, -percent) def lighten(color, percent): """Lighten a color by increasing its lightness by percent""" return adjust(color, 2, percent) def darken(color, percent): """Darken a color by decreasing its lightness by percent""" return adjust(color, 2, -percent) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/config.py0000664000175000017500000003742500000000000016364 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Config module holding all options and their default values.""" from copy import deepcopy from pygal import formatters from pygal.interpolate import INTERPOLATIONS from pygal.style import DefaultStyle, Style CONFIG_ITEMS = [] callable = type(lambda: 1) class Key(object): """ Represents a config parameter. A config parameter has a name, a default value, a type, a category, a documentation, an optional longer documentatation and an optional subtype for list style option. Most of these informations are used in cabaret to auto generate forms representing these options. """ _categories = [] def __init__( self, default_value, type_, category, doc, subdoc="", subtype=None ): """Create a configuration key""" self.value = default_value self.type = type_ self.doc = doc self.category = category self.subdoc = subdoc self.subtype = subtype self.name = "Unbound" if category not in self._categories: self._categories.append(category) CONFIG_ITEMS.append(self) def __repr__(self): """ Make a documentation repr. This is a hack to generate doc from inner doc """ return """ Type: %s%s Default: %r %s%s """ % ( self.type.__name__, (' of %s' % self.subtype.__name__) if self.subtype else '', self.value, self.doc, (' %s' % self.subdoc) if self.subdoc else '' ) @property def is_boolean(self): """Return `True` if this parameter is a boolean""" return self.type == bool @property def is_numeric(self): """Return `True` if this parameter is numeric (int or float)""" return self.type in (int, float) @property def is_string(self): """Return `True` if this parameter is a string""" return self.type == str @property def is_dict(self): """Return `True` if this parameter is a mapping""" return self.type == dict @property def is_list(self): """Return `True` if this parameter is a list""" return self.type == list def coerce(self, value): """Cast a string into this key type""" if self.type == Style: return value elif self.type == list: return self.type( map(self.subtype, map(lambda x: x.strip(), value.split(','))) ) elif self.type == dict: rv = {} for pair in value.split(','): key, val = pair.split(':') key = key.strip() val = val.strip() try: rv[key] = self.subtype(val) except Exception: 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" ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0578778 pygal-3.0.5/pygal/css/0000775000175000017500000000000000000000000015322 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/css/base.css0000664000175000017500000000361000000000000016746 0ustar00gillesgilles00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with pygal. If not, see . */ /* * Font-sizes from config, override with care */ {{ id }} { -webkit-user-select: none; -webkit-font-smoothing: antialiased; font-family: {{ style.font_family }}; } {{ id }}.title { font-family: {{ style.title_font_family }}; font-size: {{ style.title_font_size }}px; } {{ id }}.legends .legend text { font-family: {{ style.legend_font_family }}; font-size: {{ style.legend_font_size }}px; } {{ id }}.axis text { font-family: {{ style.label_font_family }}; font-size: {{ style.label_font_size }}px; } {{ id }}.axis text.major { font-family: {{ style.major_label_font_family }}; font-size: {{ style.major_label_font_size }}px; } {{ id }}.text-overlay text.value { font-family: {{ style.value_font_family }}; font-size: {{ style.value_font_size }}px; } {{ id }}.text-overlay text.label { font-family: {{ style.value_label_font_family }}; font-size: {{ style.value_label_font_size }}px; } {{ id }}.tooltip { font-family: {{ style.tooltip_font_family }}; font-size: {{ style.tooltip_font_size }}px; } {{ id }}text.no_data { font-family: {{ style.no_data_font_family }}; font-size: {{ style.no_data_font_size }}px; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/css/graph.css0000664000175000017500000000560500000000000017143 0ustar00gillesgilles00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with pygal. If not, see . */ {{ id }}text.no_data { text-anchor: middle; } {{ id }}.guide.line { fill: none; } {{ id }}.centered { text-anchor: middle; } {{ id }}.title { text-anchor: middle; } {{ id }}.legends .legend text { fill-opacity: 1; } {{ id }}.axis.x text { text-anchor: middle; } {{ id }}.axis.x:not(.web) text[transform] { text-anchor: start; } {{ id }}.axis.x:not(.web) text[transform].backwards { text-anchor: end; } {{ id }}.axis.y text { text-anchor: end; } {{ id }}.axis.y text[transform].backwards { text-anchor: start; } {{ id }}.axis.y2 text { text-anchor: start; } {{ id }}.axis.y2 text[transform].backwards { text-anchor: end; } {{ id }}.axis .guide.line { stroke-dasharray: {{ style.guide_stroke_dasharray }}; stroke: {{ style.guide_stroke_color }}; } {{ id }}.axis .major.guide.line { stroke-dasharray: {{ style.major_guide_stroke_dasharray }}; stroke: {{ style.major_guide_stroke_color }}; } {{ 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: {{ style.dot_opacity }}; stroke-opacity: {{ style.dot_opacity }}; } {{ 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; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/css/style.css0000664000175000017500000000712100000000000017175 0ustar00gillesgilles00000000000000/* * This file is part of pygal * * A python svg graph plotting library * Copyright © 2012 Kozea * This library is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with pygal. If not, see . */ /* * Styles from config */ {{ id }}{ background-color: {{ style.background }}; } {{ id }}path, {{ id }}line, {{ id }}rect, {{ id }}circle { -webkit-transition: {{ style.transition }}; -moz-transition: {{ style.transition }}; transition: {{ style.transition }}; } {{ id }}.graph > .background { fill: {{ style.background }}; } {{ id }}.plot > .background { fill: {{ style.plot_background }}; } {{ id }}.graph { fill: {{ style.foreground }}; } {{ id }}text.no_data { fill: {{ style.foreground_strong }}; } {{ id }}.title { fill: {{ style.foreground_strong }}; } {{ id }}.legends .legend text { fill: {{ style.foreground }}; } {{ id }}.legends .legend:hover text { fill: {{ style.foreground_strong }}; } {{ id }}.axis .line { stroke: {{ style.foreground_strong }}; } {{ id }}.axis .guide.line { stroke: {{ style.foreground_subtle }}; } {{ id }}.axis .major.line { stroke: {{ style.foreground }}; } {{ id }}.axis text.major { fill: {{ style.foreground_strong }}; } {{ id }}.axis.y .guides:hover .guide.line, {{ id }}.line-graph .axis.x .guides:hover .guide.line, {{ id }}.stackedline-graph .axis.x .guides:hover .guide.line, {{ id }}.xy-graph .axis.x .guides:hover .guide.line { stroke: {{ style.foreground_strong }}; } {{ id }}.axis .guides:hover text { fill: {{ style.foreground_strong }}; } {{ id }}.reactive { fill-opacity: {{ style.opacity }}; stroke-opacity: {{ style.stroke_opacity }}; stroke-width: {{ style.stroke_width }}; } {{ id }}.ci { stroke: {{ style.foreground }}; } {{ id }}.reactive.active, {{ id }}.active .reactive { fill-opacity: {{ style.opacity_hover }}; stroke-opacity: {{ style.stroke_opacity_hover }}; stroke-width: {{ style.stroke_width_hover }}; } {{ 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 }} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/etree.py0000664000175000017500000000410200000000000016205 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Wrapper for seamless lxml.etree / xml.etree usage depending on whether lxml is installed or not. """ import os class Etree(object): """Etree wrapper using lxml.etree or standard xml.etree""" def __init__(self): """Create the wrapper""" from xml.etree import ElementTree as _py_etree self._py_etree = _py_etree try: from lxml import etree as _lxml_etree self._lxml_etree = _lxml_etree except ImportError: self._lxml_etree = None if os.getenv('NO_LXML', None): self._etree = self._py_etree else: self._etree = self._lxml_etree or self._py_etree self.lxml = self._etree is self._lxml_etree def __getattribute__(self, attr): """Retrieve attr from current active etree implementation""" if (attr not in object.__getattribute__(self, '__dict__') and attr not in Etree.__dict__): return object.__getattribute__(self._etree, attr) return object.__getattribute__(self, attr) def to_lxml(self): """Force lxml.etree to be used""" self._etree = self._lxml_etree self.lxml = True def to_etree(self): """Force xml.etree to be used""" self._etree = self._py_etree self.lxml = False etree = Etree() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/formatters.py0000664000175000017500000000567700000000000017311 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Formatters to use with `value_formatter` and `x_value_formatter` configs """ from __future__ import division from datetime import date, datetime, time from math import floor, log from pygal.util import float_format class Formatter(object): pass class HumanReadable(Formatter): """Format a number to engineer scale""" ORDERS = "yzafpnµm kMGTPEZY" def __init__(self, none_char='∅'): 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 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() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0618777 pygal-3.0.5/pygal/graph/0000775000175000017500000000000000000000000015633 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/__init__.py0000664000175000017500000000146700000000000017754 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Graph package containing all builtin charts""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/bar.py0000664000175000017500000001236300000000000016756 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Bar chart that presents grouped data with rectangular bars with lengths proportional to the values that they represent. """ from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/base.py0000664000175000017500000002076400000000000017130 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Base for pygal charts""" 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 import Histogram from pygal.graph.map import BaseMap 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/box.py0000664000175000017500000002547300000000000017010 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Box plot: a convenient way to display series as box with whiskers and outliers Different types are available throught the box_mode option """ from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/dot.py0000664000175000017500000001053500000000000016777 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Dot chart displaying values as a grid of dots, the bigger the value the bigger the dot """ from math import log10 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): if self.y_labels: y_labels = [str(label) for label in self.y_labels] else: y_labels = [ ( serie.title['title'] if isinstance(serie.title, dict) else serie.title ) or '' for serie in self.series ] self._y_labels = list(zip(y_labels, 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/dual.py0000664000175000017500000000504300000000000017134 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Dual chart base. Dual means a chart with 2 scaled axis like xy""" from pygal.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 isinstance(x_label, str): 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/funnel.py0000664000175000017500000001012700000000000017475 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Funnel chart: Represent values as a funnel""" from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/gauge.py0000664000175000017500000001326300000000000017302 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Gauge chart representing values as needles on a polar scale""" from 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 isinstance(y_label, str): 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/graph.py0000664000175000017500000011154000000000000017310 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Chart properties and drawing""" from math import ceil, cos, sin, sqrt from pygal import stats from pygal._compat import is_list_like 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 = str(x) self.svg.node(node, 'desc', class_="y " + classes).text = str(y) if xlabel: self.svg.node(node, 'desc', class_="x_label").text = 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 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 isinstance(label, str): 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 isinstance(y_label, str): 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 ]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/histogram.py0000664000175000017500000001045000000000000020202 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Histogram chart: like a bar chart but with data plotted along a x axis as bars of varying width. """ from 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] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/horizontal.py0000664000175000017500000000461000000000000020377 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Horizontal graph mixin""" from pygal.graph.graph import Graph from pygal.view import HorizontalLogView, HorizontalView class HorizontalGraph(Graph): """Horizontal graph mixin""" def __init__(self, *args, **kwargs): """Set the horizontal flag to True""" self.horizontal = True super(HorizontalGraph, self).__init__(*args, **kwargs) def _post_compute(self): """After computations transpose labels""" self._x_labels, self._y_labels = self._y_labels, self._x_labels self._x_labels_major, self._y_labels_major = ( self._y_labels_major, self._x_labels_major ) self._x_2nd_labels, self._y_2nd_labels = ( self._y_2nd_labels, self._x_2nd_labels ) self.show_y_guides, self.show_x_guides = ( self.show_x_guides, self.show_y_guides ) def _axes(self): """Set the _force_vertical flag when rendering axes""" self.view._force_vertical = True super(HorizontalGraph, self)._axes() self.view._force_vertical = False def _set_view(self): """Assign a horizontal view to current graph""" if self.logarithmic: view_class = HorizontalLogView else: view_class = HorizontalView self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, self._box ) def _get_x_label(self, i): """Convenience function to get the x_label of a value index""" if not self.x_labels or not self._y_labels or len(self._y_labels) <= i: return return self._y_labels[i][0] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/horizontalbar.py0000664000175000017500000000223300000000000021063 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Horizontal bar graph""" from pygal.graph.bar import Bar from pygal.graph.horizontal import HorizontalGraph class HorizontalBar(HorizontalGraph, Bar): """Horizontal Bar graph""" def _plot(self): """Draw the bars in reverse order""" for serie in self.series[::-1]: self.bar(serie) for serie in self.secondary_series[::-1]: self.bar(serie, True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/horizontalline.py0000664000175000017500000000224400000000000021250 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Horizontal line graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.line import Line class HorizontalLine(HorizontalGraph, Line): """Horizontal Line graph""" def _plot(self): """Draw the lines in reverse order""" for serie in self.series[::-1]: self.line(serie) for serie in self.secondary_series[::-1]: self.line(serie, True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/horizontalstackedbar.py0000664000175000017500000000175000000000000022425 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Horizontal stacked graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.stackedbar import StackedBar class HorizontalStackedBar(HorizontalGraph, StackedBar): """Horizontal Stacked Bar graph""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/horizontalstackedline.py0000664000175000017500000000232000000000000022602 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Horizontal Stacked Line graph""" from pygal.graph.horizontal import HorizontalGraph from pygal.graph.stackedline import StackedLine class HorizontalStackedLine(HorizontalGraph, StackedLine): """Horizontal Stacked Line graph""" def _plot(self): """Draw the lines in reverse order""" for serie in self.series[::-1]: self.line(serie) for serie in self.secondary_series[::-1]: self.line(serie, True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/line.py0000664000175000017500000001750600000000000017145 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Line chart: Display series of data as markers (dots) connected by straight segments """ from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/map.py0000664000175000017500000001057500000000000016772 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ pygal contains no map but a base class to create extension see the pygal_maps_world package to get an exemple. https://github.com/Kozea/pygal_maps_world """ from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/pie.py0000664000175000017500000000675100000000000016773 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Pie chart: A circular chart divided into slice to illustrate proportions It can be made as a donut or a half pie. """ from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/public.py0000664000175000017500000001327700000000000017475 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """pygal public api functions""" import base64 import io from pygal._compat import is_list_like 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 = '▁▂▃▄▅▆▇█' if len(self.raw_series) == 0: return '' values = list(self.raw_series[0][0]) if len(values) == 0: return '' chart = '' 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/pyramid.py0000664000175000017500000000615000000000000017654 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Pyramid chart: Stacked bar chart containing only positive values divided by two axes, generally gender for age pyramid. """ from 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""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/radar.py0000664000175000017500000001650300000000000017303 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Radar chart: As known as kiviat chart or spider chart is a polar line chart useful for multivariate observation. """ from math import cos, pi 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 isinstance(y_label, str): 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)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/solidgauge.py0000664000175000017500000001175400000000000020340 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Solid Guage For each series a solid guage is shown on the plot area. """ from 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.' ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/stackedbar.py0000664000175000017500000001311700000000000020313 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Stacked Bar chart: Like a bar chart but with all series stacking on top of the others instead of being displayed side by side. """ from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/stackedline.py0000664000175000017500000000613100000000000020474 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Stacked Line chart: Like a line chart but with all lines stacking on top of the others. Used along fill=True option. """ from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/time.py0000664000175000017500000000744600000000000017156 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ XY time extensions: handle convertion of date, time, datetime, timedelta into float for xy plot and back to their type for display """ from datetime import date, datetime, time, timedelta from pygal._compat import 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 isinstance(x, str): 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/treemap.py0000664000175000017500000001032700000000000017645 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Treemap chart: Visualize data using nested recangles""" from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/graph/xy.py0000664000175000017500000000766000000000000016656 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ XY Line graph: Plot a set of couple data points (x, y) connected by straight segments. """ from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/interpolate.py0000664000175000017500000002064600000000000017442 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ Interpolation functions These functions takes two lists of points x and y and returns an iterator over the interpolation between all these points with `precision` interpolated points between each of them """ from __future__ import division from math import sin def quadratic_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using a quadratic algorithm https://en.wikipedia.org/wiki/Spline_(mathematics) """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] delta_y = [y2 - y1 for y1, y2 in zip(y, y[1:])] slope = [delta_y[i] / delta_x[i] if delta_x[i] else 1 for i in range(n)] # Quadratic spline: a + bx + cx² a = y b = [0] * (n + 1) c = [0] * (n + 1) for i in range(1, n): b[i] = 2 * slope[i - 1] - b[i - 1] c = [(slope[i] - b[i]) / delta_x[i] if delta_x[i] else 0 for i in range(n)] for i in range(n + 1): yield x[i], a[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = s * delta_x[i] / precision X2 = X * X yield x[i] + X, a[i] + b[i] * X + c[i] * X2 def cubic_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using a cubic algorithm https://en.wikipedia.org/wiki/Spline_interpolation """ n = len(x) - 1 # Spline equation is a + bx + cx² + dx³ # ie: Spline part i equation is a[i] + b[i]x + c[i]x² + d[i]x³ a = y b = [0] * (n + 1) c = [0] * (n + 1) d = [0] * (n + 1) m = [0] * (n + 1) z = [0] * (n + 1) h = [x2 - x1 for x1, x2 in zip(x, x[1:])] k = [a2 - a1 for a1, a2 in zip(a, a[1:])] g = [k[i] / h[i] if h[i] else 1 for i in range(n)] for i in range(1, n): j = i - 1 l = 1 / (2 * (x[i + 1] - x[j]) - h[j] * m[j]) if x[i + 1] - x[j] else 0 m[i] = h[i] * l z[i] = (3 * (g[i] - g[j]) - h[j] * z[j]) * l for j in reversed(range(n)): if h[j] == 0: continue c[j] = z[j] - (m[j] * c[j + 1]) b[j] = g[j] - (h[j] * (c[j + 1] + 2 * c[j])) / 3 d[j] = (c[j + 1] - c[j]) / (3 * h[j]) for i in range(n + 1): yield x[i], a[i] if i == n or h[i] == 0: continue for s in range(1, precision): X = s * h[i] / precision X2 = X * X X3 = X2 * X yield x[i] + X, a[i] + b[i] * X + c[i] * X2 + d[i] * X3 def hermite_interpolate( x, y, precision=250, type='cardinal', c=None, b=None, t=None ): """ Interpolate x, y using the hermite method. See https://en.wikipedia.org/wiki/Cubic_Hermite_spline This interpolation is configurable and contain 4 subtypes: * Catmull Rom * Finite Difference * Cardinal * Kochanek Bartels The cardinal subtype is customizable with a parameter: * c: tension (0, 1) This last type is also customizable using 3 parameters: * c: continuity (-1, 1) * b: bias (-1, 1) * t: tension (-1, 1) """ n = len(x) - 1 m = [1] * (n + 1) w = [1] * (n + 1) delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] if type == 'catmull_rom': type = 'cardinal' c = 0 if type == 'finite_difference': for i in range(1, n): m[i] = w[i] = .5 * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) + (y[i] - y[i - 1]) / (x[i] - x[i - 1]) ) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0 elif type == 'kochanek_bartels': c = c or 0 b = b or 0 t = t or 0 for i in range(1, n): m[i] = .5 * ((1 - t) * (1 + b) * (1 + c) * (y[i] - y[i - 1]) + (1 - t) * (1 - b) * (1 - c) * (y[i + 1] - y[i])) w[i] = .5 * ((1 - t) * (1 + b) * (1 - c) * (y[i] - y[i - 1]) + (1 - t) * (1 - b) * (1 + c) * (y[i + 1] - y[i])) if type == 'cardinal': c = c or 0 for i in range(1, n): m[i] = w[i] = (1 - c) * (y[i + 1] - y[i - 1]) / ( x[i + 1] - x[i - 1] ) if x[i + 1] - x[i - 1] else 0 def p(i, x_): t = (x_ - x[i]) / delta_x[i] t2 = t * t t3 = t2 * t h00 = 2 * t3 - 3 * t2 + 1 h10 = t3 - 2 * t2 + t h01 = -2 * t3 + 3 * t2 h11 = t3 - t2 return ( h00 * y[i] + h10 * m[i] * delta_x[i] + h01 * y[i + 1] + h11 * w[i + 1] * delta_x[i] ) for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision yield X, p(i, X) def lagrange_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using Lagrange polynomials https://en.wikipedia.org/wiki/Lagrange_polynomial """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision s = 0 for k in range(n + 1): p = 1 for m in range(n + 1): if m == k: continue if x[k] - x[m]: p *= (X - x[m]) / (x[k] - x[m]) s += y[k] * p yield X, s def trigonometric_interpolate(x, y, precision=250, **kwargs): """ Interpolate x, y using trigonometric As per http://en.wikipedia.org/wiki/Trigonometric_interpolation """ n = len(x) - 1 delta_x = [x2 - x1 for x1, x2 in zip(x, x[1:])] for i in range(n + 1): yield x[i], y[i] if i == n or delta_x[i] == 0: continue for s in range(1, precision): X = x[i] + s * delta_x[i] / precision s = 0 for k in range(n + 1): p = 1 for m in range(n + 1): if m == k: continue if sin(0.5 * (x[k] - x[m])): p *= sin(0.5 * (X - x[m])) / sin(0.5 * (x[k] - x[m])) s += y[k] * p yield X, s INTERPOLATIONS = { 'quadratic': quadratic_interpolate, 'cubic': cubic_interpolate, 'hermite': hermite_interpolate, 'lagrange': lagrange_interpolate, 'trigonometric': trigonometric_interpolate } if __name__ == '__main__': from pygal import XY points = [(.1, 7), (.3, -4), (.6, 10), (.9, 8), (1.4, 3), (1.7, 1)] xy = XY(show_dots=False) xy.add('normal', points) xy.add('quadratic', quadratic_interpolate(*zip(*points))) xy.add('cubic', cubic_interpolate(*zip(*points))) xy.add('lagrange', lagrange_interpolate(*zip(*points))) xy.add('trigonometric', trigonometric_interpolate(*zip(*points))) xy.add( 'hermite catmul_rom', hermite_interpolate(*zip(*points), type='catmul_rom') ) xy.add( 'hermite finite_difference', hermite_interpolate(*zip(*points), type='finite_difference') ) xy.add( 'hermite cardinal -.5', hermite_interpolate(*zip(*points), type='cardinal', c=-.5) ) xy.add( 'hermite cardinal .5', hermite_interpolate(*zip(*points), type='cardinal', c=.5) ) xy.add( 'hermite kochanek_bartels .5 .75 -.25', hermite_interpolate( *zip(*points), type='kochanek_bartels', c=.5, b=.75, t=-.25 ) ) xy.add( 'hermite kochanek_bartels .25 -.75 .5', hermite_interpolate( *zip(*points), type='kochanek_bartels', c=.25, b=-.75, t=.5 ) ) xy.render_in_browser() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0618777 pygal-3.0.5/pygal/maps/0000775000175000017500000000000000000000000015472 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/maps/__init__.py0000664000175000017500000000145400000000000017607 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Maps extensions namespace module""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/serie.py0000664000175000017500000000256400000000000016222 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Serie property holder""" from pygal.util import cached_property class Serie(object): """Serie class containing title, values and the graph serie index""" def __init__(self, index, values, config, metadata=None): """Create the serie with its options""" self.index = index self.values = values self.config = config self.__dict__.update(config.__dict__) self.metadata = metadata or {} @cached_property def safe_values(self): """Property containing all values that are not None""" return list(filter(lambda x: x is not None, self.values)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/state.py0000664000175000017500000000235000000000000016224 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Class holding state during render""" from pygal.util import merge class State(object): """ Class containing config values overriden by chart values overriden by keyword args """ def __init__(self, graph, **kwargs): """Create the transient state""" merge(self.__dict__, graph.config.__class__.__dict__) merge(self.__dict__, graph.config.__dict__) merge(self.__dict__, graph.__dict__) merge(self.__dict__, kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/stats.py0000664000175000017500000000425200000000000016245 0ustar00gillesgilles00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/style.py0000664000175000017500000003635300000000000016256 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Charts styling classes""" from __future__ import division from itertools import chain from pygal import colors from pygal.colors import darken, is_foreground_light, lighten class Style(object): """Styling class containing colors for the css generation""" plot_background = 'rgba(255, 255, 255, 1)' background = 'rgba(249, 249, 249, 1)' value_background = 'rgba(229, 229, 229, 1)' foreground = 'rgba(0, 0, 0, .87)' foreground_strong = 'rgba(0, 0, 0, 1)' foreground_subtle = 'rgba(0, 0, 0, .54)' # Monospaced font is highly encouraged font_family = ('Consolas, "Liberation Mono", Menlo, Courier, monospace') label_font_family = None major_label_font_family = None value_font_family = None value_label_font_family = None tooltip_font_family = None title_font_family = None legend_font_family = None no_data_font_family = None label_font_size = 10 major_label_font_size = 10 value_font_size = 16 value_label_font_size = 10 tooltip_font_size = 14 title_font_size = 16 legend_font_size = 14 no_data_font_size = 64 # Guide line dash array style guide_stroke_dasharray = '4,4' major_guide_stroke_dasharray = '6,6' guide_stroke_color = 'black' major_guide_stroke_color = 'black' opacity = '.7' opacity_hover = '.8' stroke_opacity = '.8' stroke_width = '1' stroke_opacity_hover = '.9' stroke_width_hover = '4' dot_opacity = '1' 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 } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/svg.py0000664000175000017500000004441600000000000015714 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Svg helper""" from __future__ import division import io import json import os from datetime import date, datetime from math import pi from numbers import Number from urllib.parse import quote_plus from pygal import __version__ 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( '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('http://pygal.org')) self.root.append(etree.Comment('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('xml-stylesheet', '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] = str(value) if key.endswith('_'): attrib[key.rstrip('_')] = attrib[key] del attrib[key] elif key == 'href': attrib[etree.QName('http://www.w3.org/1999/xlink', key)] = attrib[key] del attrib[key] return etree.SubElement(parent, tag, attrib) def transposable_node(self, parent=None, tag='g', attrib=None, **extras): """Make a new svg node which can be transposed if horizontal""" if self.graph.horizontal: for key1, key2 in (('x', 'y'), ('width', 'height'), ('cx', 'cy')): attr1 = extras.get(key1, None) attr2 = extras.get(key2, None) if attr2: extras[key1] = attr2 elif attr1: del extras[key1] if attr1: extras[key2] = attr1 elif attr2: del extras[key2] return self.node(parent, tag, attrib, **extras) def serie(self, serie): """Make serie node""" return dict( plot=self.node( self.graph.nodes['plot'], class_='series serie-%d color-%d' % (serie.index, serie.index) ), overlay=self.node( self.graph.nodes['overlay'], class_='series serie-%d color-%d' % (serie.index, serie.index) ), text_overlay=self.node( self.graph.nodes['text_overlay'], class_='series serie-%d color-%d' % (serie.index, serie.index) ) ) def line(self, node, coords, close=False, **kwargs): """Draw a svg line""" line_len = len(coords) if len([c for c in coords if c[1] is not None]) < 2: return root = 'M%s L%s Z' if close else 'M%s L%s' origin_index = 0 while origin_index < line_len and None in coords[origin_index]: origin_index += 1 if origin_index == line_len: return if self.graph.horizontal: coord_format = lambda xy: '%f %f' % (xy[1], xy[0]) else: coord_format = lambda xy: '%f %f' % xy origin = coord_format(coords[origin_index]) line = ' '.join([ coord_format(c) for c in coords[origin_index + 1:] if None not in c ]) return self.node(node, 'path', d=root % (origin, line), **kwargs) def slice( self, serie_node, node, radius, small_radius, angle, start_angle, center, val, i, metadata ): """Draw a pie slice""" if angle == 2 * pi: angle = nearly_2pi if angle > 0: to = [ coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle) ] rv = self.node( node, 'path', d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( to[0], coord_dual(radius), int(angle > pi), to[1], to[2], coord_dual(small_radius), int(angle > pi), to[3] ), class_='slice reactive tooltip-trigger' ) else: rv = None x, y = coord_diff( center, coord_project((radius + small_radius) / 2, start_angle + angle / 2) ) self.graph._tooltip_data( node, val, x, y, "centered", self.graph._x_labels and self.graph._x_labels[i][0] ) if angle >= 0.3: # 0.3 radians is about 17 degrees self.graph._static_value(serie_node, val, x, y, metadata) return rv def gauge_background( self, serie_node, start_angle, center, radius, small_radius, end_angle, half_pie, max_value ): if end_angle == 2 * pi: end_angle = nearly_2pi to_shade = [ coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, end_angle), coord_abs_project(center, small_radius, end_angle), coord_abs_project(center, small_radius, start_angle) ] self.node( serie_node['plot'], 'path', d='M%s A%s 0 1 1 %s L%s A%s 0 1 0 %s z' % ( to_shade[0], coord_dual(radius), to_shade[1], to_shade[2], coord_dual(small_radius), to_shade[3] ), class_='gauge-background reactive' ) if half_pie: begin_end = [ coord_diff( center, coord_project( radius - (radius - small_radius) / 2, start_angle ) ), coord_diff( center, coord_project( radius - (radius - small_radius) / 2, end_angle ) ) ] pos = 0 for i in begin_end: self.node( serie_node['plot'], 'text', class_='y-{} bound reactive'.format(pos), x=i[0], y=i[1] + 10, attrib={ 'text-anchor': 'middle' } ).text = '{}'.format(0 if pos == 0 else max_value) pos += 1 else: middle_radius = .5 * (radius + small_radius) # Correct text vertical alignment middle_radius -= .1 * (radius - small_radius) to_labels = [ coord_abs_project(center, middle_radius, 0), coord_abs_project(center, middle_radius, nearly_2pi) ] self.node( self.defs, 'path', id='valuePath-%s%s' % center, d='M%s A%s 0 1 1 %s' % (to_labels[0], coord_dual(middle_radius), to_labels[1]) ) text_ = self.node(serie_node['text_overlay'], 'text') self.node( text_, 'textPath', class_='max-value reactive', attrib={ 'href': '#valuePath-%s%s' % center, 'startOffset': '99%', 'text-anchor': 'end' } ).text = max_value def solid_gauge( self, serie_node, node, radius, small_radius, angle, start_angle, center, val, i, metadata, half_pie, end_angle, max_value ): """Draw a solid gauge slice and background slice""" if angle == 2 * pi: angle = nearly_2pi if angle > 0: to = [ coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, small_radius, start_angle) ] self.node( node, 'path', d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( to[0], coord_dual(radius), int(angle > pi), to[1], to[2], coord_dual(small_radius), int(angle > pi), to[3] ), class_='slice reactive tooltip-trigger' ) else: return x, y = coord_diff( center, coord_project((radius + small_radius) / 2, start_angle + angle / 2) ) self.graph._static_value(serie_node, val, x, y, metadata, 'middle') self.graph._tooltip_data( node, val, x, y, "centered", self.graph._x_labels and self.graph._x_labels[i][0] ) def confidence_interval(self, node, x, low, high, width=7): if self.graph.horizontal: fmt = lambda xy: '%f %f' % (xy[1], xy[0]) else: fmt = coord_format shr = lambda xy: (xy[0] + width, xy[1]) shl = lambda xy: (xy[0] - width, xy[1]) top = (x, high) bottom = (x, low) ci = self.node(node, class_="ci") self.node( ci, 'path', d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple( map( fmt, ( top, shr(top), top, shl(top), top, bottom, shr(bottom), bottom, shl(bottom) ) ) ), class_='nofill reactive' ) def pre_render(self): """Last things to do before rendering""" self.add_styles() self.add_scripts() self.root.set( 'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height) ) if self.graph.explicit_size: self.root.set('width', str(self.graph.width)) self.root.set('height', str(self.graph.height)) def draw_no_data(self): """Write the no data text to the svg""" no_data = self.node( self.graph.nodes['text_overlay'], 'text', x=self.graph.view.width / 2, y=self.graph.view.height / 2, class_='no_data' ) no_data.text = self.graph.no_data_text def render(self, is_unicode=False, pretty_print=False): """Last thing to do before rendering""" for f in self.graph.xml_filters: self.root = f(self.root) args = {'encoding': 'utf-8'} svg = b'' if etree.lxml: args['pretty_print'] = pretty_print if not self.graph.disable_xml_declaration: svg = b"\n" if not self.graph.disable_xml_declaration: svg += b'\n'.join([ etree.tostring(pi, **args) for pi in self.processing_instructions ]) svg += etree.tostring(self.root, **args) if self.graph.disable_xml_declaration or is_unicode: svg = svg.decode('utf-8') return svg def get_strokes(self): """Return a css snippet containing all stroke style options""" def stroke_dict_to_css(stroke, i=None): """Return a css style for the given option""" css = [ '%s.series%s {\n' % (self.id, '.serie-%d' % i if i is not None else '') ] for key in ('width', 'linejoin', 'linecap', 'dasharray', 'dashoffset'): if stroke.get(key): css.append(' stroke-%s: %s;\n' % (key, stroke[key])) css.append('}') return '\n'.join(css) css = [] if self.graph.stroke_style is not None: css.append(stroke_dict_to_css(self.graph.stroke_style)) for serie in self.graph.series: if serie.stroke_style is not None: css.append(stroke_dict_to_css(serie.stroke_style, serie.index)) for secondary_serie in self.graph.secondary_series: if secondary_serie.stroke_style is not None: css.append( stroke_dict_to_css( secondary_serie.stroke_style, secondary_serie.index ) ) return '\n'.join(css) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/table.py0000664000175000017500000001314400000000000016176 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ HTML Table maker. This class is used to render an html table from a chart data. """ import uuid from lxml.html import builder, tostring from pygal.util import template class HTML(object): """Lower case adapter of lxml builder""" def __getattribute__(self, attr): """Get the uppercase builder attribute""" return getattr(builder, attr.upper()) class Table(object): """Table generator class""" _dual = None def __init__(self, chart): """Init the table""" self.chart = chart def render(self, total=False, transpose=False, style=False): """Render the HTMTL table of the chart. `total` can be specified to include data sums `transpose` make labels becomes columns `style` include scoped style for the table """ self.chart.setup() ln = self.chart._len html = HTML() attrs = {} if style: attrs['id'] = 'table-%s' % uuid.uuid4() table = [] _ = lambda x: x if x is not None else '' if self.chart.x_labels: labels = [None] + list(self.chart.x_labels) if len(labels) < ln: labels += [None] * (ln + 1 - len(labels)) if len(labels) > ln + 1: labels = labels[:ln + 1] table.append(labels) if total: if len(table): table[0].append('Total') else: table.append([None] * (ln + 1) + ['Total']) acc = [0] * (ln + 1) for i, serie in enumerate(self.chart.all_series): row = [serie.title] if total: sum_ = 0 for j, value in enumerate(serie.values): if total: v = value or 0 acc[j] += v sum_ += v row.append(self.chart._format(serie, j)) if total: acc[-1] += sum_ row.append(self.chart._serie_format(serie, sum_)) table.append(row) width = ln + 1 if total: width += 1 table.append(['Total']) for val in acc: table[-1].append(self.chart._serie_format(serie, val)) # Align values len_ = max([len(r) for r in table] or [0]) for i, row in enumerate(table[:]): len_ = len(row) if len_ < width: table[i] = row + [None] * (width - len_) if not transpose: table = list(zip(*table)) thead = [] tbody = [] tfoot = [] if not transpose or self.chart.x_labels: # There's always series title but not always x_labels thead = [table[0]] tbody = table[1:] else: tbody = table if total: tfoot = [tbody[-1]] tbody = tbody[:-1] parts = [] if thead: parts.append( html.thead( *[html.tr(*[html.th(_(col)) for col in r]) for r in thead] ) ) if tbody: parts.append( html.tbody( *[html.tr(*[html.td(_(col)) for col in r]) for r in tbody] ) ) if tfoot: parts.append( html.tfoot( *[html.tr(*[html.th(_(col)) for col in r]) for r in tfoot] ) ) table = tostring(html.table(*parts, **attrs)) if style: if style is True: css = ''' #{{ id }} { border-collapse: collapse; border-spacing: 0; empty-cells: show; border: 1px solid #cbcbcb; } #{{ id }} td, #{{ id }} th { border-left: 1px solid #cbcbcb; border-width: 0 0 0 1px; margin: 0; padding: 0.5em 1em; } #{{ id }} td:first-child, #{{ id }} th:first-child { border-left-width: 0; } #{{ id }} thead, #{{ id }} tfoot { color: #000; text-align: left; vertical-align: bottom; } #{{ id }} thead { background: #e0e0e0; } #{{ id }} tfoot { background: #ededed; } #{{ id }} tr:nth-child(2n-1) td { background-color: #f2f2f2; } ''' else: css = style table = tostring( html.style(template(css, **attrs), scoped='scoped') ) + table table = table.decode('utf-8') self.chart.teardown() return table ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0658777 pygal-3.0.5/pygal/test/0000775000175000017500000000000000000000000015511 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/__init__.py0000664000175000017500000000333500000000000017626 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Pygal test package""" from decimal import Decimal import pygal from pygal.graph.map import BaseMap from pygal.util import cut 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/conftest.py0000664000175000017500000000335200000000000017713 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """pytest fixtures""" import sys import pytest import pygal from pygal.etree import etree from . import get_data @pytest.fixture def etreefx(request): """Fixture allowing to test with builtin etree and lxml""" if request.param == 'etree': etree.to_etree() if request.param == 'lxml': etree.to_lxml() def pytest_generate_tests(metafunc): """Generate the tests for etree and lxml""" if etree._lxml_etree: metafunc.fixturenames.append('etreefx') metafunc.parametrize('etreefx', ['lxml', 'etree'], indirect=True) if not hasattr(sys, 'pypy_version_info'): etree.to_lxml() if hasattr(sys, 'pypy_version_info'): etree.to_etree() if "Chart" in metafunc.fixturenames: metafunc.parametrize("Chart", pygal.CHARTS) if "datas" in metafunc.fixturenames: metafunc.parametrize( "datas", [[("Serie %d" % i, get_data(i)) for i in range(s)] for s in (5, 1, 0)] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_bar.py0000664000175000017500000000230400000000000017665 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Bar chart related tests""" from pygal import Bar def test_simple_bar(): """Simple bar test""" bar = Bar() rng = [-3, -32, -39] bar.add('test1', rng) bar.add('test2', map(abs, rng)) bar.x_labels = map(str, rng) bar.title = "Bar test" q = bar.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".legend")) == 2 assert len(q(".plot .series rect")) == 2 * 3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_box.py0000664000175000017500000001213500000000000017714 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Box chart related tests""" from pygal.graph.box import Box def test_quartiles(): """Test box points for the 1.5IQR computation method""" a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='1.5IQR' ) assert q1 == 7.0 / 4.0 assert q2 == 4.0 assert q3 == 23 / 4.0 assert q0 == 7.0 / 4.0 - 6.0 # q1 - 1.5 * iqr assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr b = [1.0, 4.0, 6.0, 8.0] # even test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='1.5IQR' ) assert q2 == 5.0 c = [2.0, None, 4.0, 6.0, None] # odd with None elements (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='1.5IQR' ) assert q2 == 4.0 d = [4] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( d, mode='1.5IQR' ) assert q0 == 4 assert q1 == 4 assert q2 == 4 assert q3 == 4 assert q4 == 4 def test_quartiles_min_extremes(): """Test box points for the extremes computation method""" a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='extremes' ) assert q1 == 7.0 / 4.0 assert q2 == 4.0 assert q3 == 23 / 4.0 assert q0 == -2.0 # min assert q4 == 8.0 # max b = [1.0, 4.0, 6.0, 8.0] # even test data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='extremes' ) assert q2 == 5.0 c = [2.0, None, 4.0, 6.0, None] # odd with None elements (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='extremes' ) assert q2 == 4.0 d = [4] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( d, mode='extremes' ) assert q0 == 4 assert q1 == 4 assert q2 == 4 assert q3 == 4 assert q4 == 4 def test_quartiles_tukey(): """Test box points for the tukey computation method""" a = [] # empty data (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='tukey' ) assert min_s == q0 == q1 == q2 == q3 == q4 == 0 assert outliers == [] # https://en.wikipedia.org/wiki/Quartile example 1 b = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='tukey' ) assert min_s == q0 == 6 assert q1 == 20.25 assert q2 == 40 assert q3 == 42.75 assert max_s == q4 == 49 assert outliers == [] # previous test with added outlier 75 c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='tukey' ) assert min_s == q0 == 6 assert q1 == 25.5 assert q2 == (40 + 41) / 2.0 assert q3 == 45 assert max_s == 75 assert outliers == [75] # one more outlier, 77 c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75, 77] (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( c, mode='tukey' ) assert min_s == q0 == 6 assert q1 == 30.75 assert q2 == 41 assert q3 == 47.5 assert max_s == 77 assert 75 in outliers assert 77 in outliers def test_quartiles_stdev(): """Test box points for the stdev computation method""" a = [ 35, 42, 35, 41, 36, 6, 12, 51, 33, 27, 46, 36, 44, 53, 75, 46, 16, 51, 45, 29, 25, 26, 54, 61, 27, 40, 23, 34, 51, 37 ] SD = 14.67 (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( a, mode='stdev' ) assert min_s == min(a) assert max_s == max(a) assert q2 == 36.5 assert q4 <= q2 + SD assert q0 >= q2 - SD assert all(n in outliers for n in [6, 12, 16, 53, 54, 61, 75]) b = [5] # test for posible zero division (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( b, mode='stdev' ) assert min_s == q0 == q1 == q2 == q3 == q4 == max_s == b[0] assert outliers == [] def test_simple_box(): """Simple box test""" box = Box() box.add('test1', [-1, 2, 3, 3.1, 3.2, 4, 5]) box.add('test2', [2, 3, 5, 6, 6, 4]) box.title = 'Box test' q = box.render_pyquery() assert len(q(".axis.y")) == 1 assert len(q(".legend")) == 2 assert len(q(".plot .series rect")) == 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_colors.py0000664000175000017500000005304500000000000020432 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Color utility functions tests""" from __future__ import division from pygal.colors import ( darken, desaturate, hsl_to_rgb, lighten, parse_color, rgb_to_hsl, rotate, saturate, unparse_color, ) def test_parse_color(): """Test color parse function""" assert parse_color('#123') == (17, 34, 51, 1., '#rgb') assert parse_color('#cdf') == (204, 221, 255, 1., '#rgb') assert parse_color('#a3d7') == (170, 51, 221, 119 / 255, '#rgba') assert parse_color('#584b4f') == (88, 75, 79, 1., '#rrggbb') assert parse_color('#8cbe22') == (140, 190, 34, 1., '#rrggbb') assert parse_color('#16cbf055') == (22, 203, 240, 1 / 3, '#rrggbbaa') assert parse_color('rgb(134, 67, 216)') == (134, 67, 216, 1., 'rgb') assert parse_color('rgb(0, 111, 222)') == (0, 111, 222, 1., 'rgb') assert parse_color('rgba(237, 83, 48, .8)') == (237, 83, 48, .8, 'rgba') assert parse_color('rgba(0, 1, 0, 0.1223)') == (0, 1, 0, .1223, 'rgba') def test_unparse_color(): """Test color unparse function""" assert unparse_color(17, 34, 51, 1., '#rgb') == '#123' assert unparse_color(204, 221, 255, 1., '#rgb') == '#cdf' assert unparse_color(170, 51, 221, 119 / 255, '#rgba') == '#a3d7' assert unparse_color(88, 75, 79, 1., '#rrggbb') == '#584b4f' assert unparse_color(140, 190, 34, 1., '#rrggbb') == '#8cbe22' assert unparse_color(22, 203, 240, 1 / 3, '#rrggbbaa') == '#16cbf055' assert unparse_color(134, 67, 216, 1., 'rgb') == 'rgb(134, 67, 216)' assert unparse_color(0, 111, 222, 1., 'rgb') == 'rgb(0, 111, 222)' assert unparse_color(237, 83, 48, .8, 'rgba') == 'rgba(237, 83, 48, 0.8)' assert unparse_color(0, 1, 0, .1223, 'rgba') == 'rgba(0, 1, 0, 0.1223)' def test_darken(): """Test darken color function""" assert darken('#800', 20) == '#200' assert darken('#800e', 20) == '#200e' assert darken('#800', 0) == '#800' assert darken('#ffffff', 10) == '#e6e6e6' assert darken('#000000', 10) == '#000000' assert darken('#f3148a', 25) == '#810747' assert darken('#f3148aab', 25) == '#810747ab' assert darken('#121212', 1) == '#0f0f0f' assert darken('#999999', 100) == '#000000' assert darken('#99999999', 100) == '#00000099' assert darken('#1479ac', 8) == '#105f87' assert darken('rgb(136, 0, 0)', 20) == 'rgb(34, 0, 0)' assert darken('rgba(20, 121, 172, .13)', 8) == 'rgba(16, 95, 135, 0.13)' def test_lighten(): """Test lighten color function""" assert lighten('#800', 20) == '#e00' assert lighten('#800', 0) == '#800' assert lighten('#ffffff', 10) == '#ffffff' assert lighten('#000000', 10) == '#1a1a1a' assert lighten('#f3148a', 25) == '#f98dc6' assert lighten('#121212', 1) == '#151515' assert lighten('#999999', 100) == '#ffffff' assert lighten('#1479ac', 8) == '#1893d1' def test_saturate(): """Test color saturation function""" assert saturate('#000', 20) == '#000' assert saturate('#fff', 20) == '#fff' assert saturate('#8a8', 100) == '#3f3' assert saturate('#855', 20) == '#9e3f3f' def test_desaturate(): """Test color desaturation function""" assert desaturate('#000', 20) == '#000' assert desaturate('#fff', 20) == '#fff' assert desaturate('#8a8', 100) == '#999' assert desaturate('#855', 20) == '#726b6b' def test_rotate(): """Test color rotation function""" assert rotate('#000', 45) == '#000' assert rotate('#fff', 45) == '#fff' assert rotate('#811', 45) == '#886a11' assert rotate('#8a8', 360) == '#8a8' assert rotate('#8a8', 0) == '#8a8' assert rotate('#8a8', -360) == '#8a8' def test_hsl_to_rgb_part_0(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_0(): """Test rgb to hsl color function""" assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) def test_hsl_to_rgb_part_1(): """Test hsl to rgb color function""" assert hsl_to_rgb(-360, 100, 50) == (255, 0, 0) assert hsl_to_rgb(-300, 100, 50) == (255, 255, 0) assert hsl_to_rgb(-240, 100, 50) == (0, 255, 0) assert hsl_to_rgb(-180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(-120, 100, 50) == (0, 0, 255) assert hsl_to_rgb(-60, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_1(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (-360, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (-300, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (-240, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (-180, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (-120, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (-60, 100, 50) pass def test_hsl_to_rgb_part_2(): """Test hsl to rgb color function""" assert hsl_to_rgb(360, 100, 50) == (255, 0, 0) assert hsl_to_rgb(420, 100, 50) == (255, 255, 0) assert hsl_to_rgb(480, 100, 50) == (0, 255, 0) assert hsl_to_rgb(540, 100, 50) == (0, 255, 255) assert hsl_to_rgb(600, 100, 50) == (0, 0, 255) assert hsl_to_rgb(660, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_2(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (360, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (420, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (480, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (540, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (600, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (660, 100, 50) pass def test_hsl_to_rgb_part_3(): """Test hsl to rgb color function""" assert hsl_to_rgb(6120, 100, 50) == (255, 0, 0) assert hsl_to_rgb(-9660, 100, 50) == (255, 255, 0) assert hsl_to_rgb(99840, 100, 50) == (0, 255, 0) assert hsl_to_rgb(-900, 100, 50) == (0, 255, 255) assert hsl_to_rgb(-104880, 100, 50) == (0, 0, 255) assert hsl_to_rgb(2820, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_3(): """Test rgb to hsl color function""" # assert rgb_to_hsl(255, 0, 0) == (6120, 100, 50) # assert rgb_to_hsl(255, 255, 0) == (-9660, 100, 50) # assert rgb_to_hsl(0, 255, 0) == (99840, 100, 50) # assert rgb_to_hsl(0, 255, 255) == (-900, 100, 50) # assert rgb_to_hsl(0, 0, 255) == (-104880, 100, 50) # assert rgb_to_hsl(255, 0, 255) == (2820, 100, 50) pass def test_hsl_to_rgb_part_4(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(12, 100, 50) == (255, 51, 0) assert hsl_to_rgb(24, 100, 50) == (255, 102, 0) assert hsl_to_rgb(36, 100, 50) == (255, 153, 0) assert hsl_to_rgb(48, 100, 50) == (255, 204, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(72, 100, 50) == (204, 255, 0) assert hsl_to_rgb(84, 100, 50) == (153, 255, 0) assert hsl_to_rgb(96, 100, 50) == (102, 255, 0) assert hsl_to_rgb(108, 100, 50) == (51, 255, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) def test_rgb_to_hsl_part_4(): """Test rgb to hsl color function""" assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 51, 0) == (12, 100, 50) assert rgb_to_hsl(255, 102, 0) == (24, 100, 50) assert rgb_to_hsl(255, 153, 0) == (36, 100, 50) assert rgb_to_hsl(255, 204, 0) == (48, 100, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(204, 255, 0) == (72, 100, 50) assert rgb_to_hsl(153, 255, 0) == (84, 100, 50) assert rgb_to_hsl(102, 255, 0) == (96, 100, 50) assert rgb_to_hsl(51, 255, 0) == (108, 100, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) def test_hsl_to_rgb_part_5(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(132, 100, 50) == (0, 255, 51) assert hsl_to_rgb(144, 100, 50) == (0, 255, 102) assert hsl_to_rgb(156, 100, 50) == (0, 255, 153) assert hsl_to_rgb(168, 100, 50) == (0, 255, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(192, 100, 50) == (0, 204, 255) assert hsl_to_rgb(204, 100, 50) == (0, 153, 255) assert hsl_to_rgb(216, 100, 50) == (0, 102, 255) assert hsl_to_rgb(228, 100, 50) == (0, 51, 255) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) def test_rgb_to_hsl_part_5(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(0, 255, 51) == (132, 100, 50) assert rgb_to_hsl(0, 255, 102) == (144, 100, 50) assert rgb_to_hsl(0, 255, 153) == (156, 100, 50) assert rgb_to_hsl(0, 255, 204) == (168, 100, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(0, 204, 255) == (192, 100, 50) assert rgb_to_hsl(0, 153, 255) == (204, 100, 50) assert rgb_to_hsl(0, 102, 255) == (216, 100, 50) assert rgb_to_hsl(0, 51, 255) == (228, 100, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) def test_hsl_to_rgb_part_6(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(252, 100, 50) == (51, 0, 255) assert hsl_to_rgb(264, 100, 50) == (102, 0, 255) assert hsl_to_rgb(276, 100, 50) == (153, 0, 255) assert hsl_to_rgb(288, 100, 50) == (204, 0, 255) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) assert hsl_to_rgb(312, 100, 50) == (255, 0, 204) assert hsl_to_rgb(324, 100, 50) == (255, 0, 153) assert hsl_to_rgb(336, 100, 50) == (255, 0, 102) assert hsl_to_rgb(348, 100, 50) == (255, 0, 51) assert hsl_to_rgb(360, 100, 50) == (255, 0, 0) def test_rgb_to_hsl_part_6(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(51, 0, 255) == (252, 100, 50) assert rgb_to_hsl(102, 0, 255) == (264, 100, 50) assert rgb_to_hsl(153, 0, 255) == (276, 100, 50) assert rgb_to_hsl(204, 0, 255) == (288, 100, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) assert rgb_to_hsl(255, 0, 204) == (312, 100, 50) assert rgb_to_hsl(255, 0, 153) == (324, 100, 50) assert rgb_to_hsl(255, 0, 102) == (336, 100, 50) assert rgb_to_hsl(255, 0, 51) == (348, 100, 50) # assert rgb_to_hsl(255, 0, 0) == (360, 100, 50) def test_hsl_to_rgb_part_7(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 20, 50) == (153, 102, 102) assert hsl_to_rgb(0, 60, 50) == (204, 51, 51) assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) def test_rgb_to_hsl_part_7(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 102, 102) == (0, 20, 50) assert rgb_to_hsl(204, 51, 51) == (0, 60, 50) assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) def test_hsl_to_rgb_part_8(): """Test hsl to rgb color function""" assert hsl_to_rgb(60, 20, 50) == (153, 153, 102) assert hsl_to_rgb(60, 60, 50) == (204, 204, 51) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) def test_rgb_to_hsl_part_8(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 153, 102) == (60, 20, 50) assert rgb_to_hsl(204, 204, 51) == (60, 60, 50) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) def test_hsl_to_rgb_part_9(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 20, 50) == (102, 153, 102) assert hsl_to_rgb(120, 60, 50) == (51, 204, 51) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) def test_rgb_to_hsl_part_9(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 153, 102) == (120, 20, 50) assert rgb_to_hsl(51, 204, 51) == (120, 60, 50) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) def test_hsl_to_rgb_part_10(): """Test hsl to rgb color function""" assert hsl_to_rgb(180, 20, 50) == (102, 153, 153) assert hsl_to_rgb(180, 60, 50) == (51, 204, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) def test_rgb_to_hsl_part_10(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 153, 153) == (180, 20, 50) assert rgb_to_hsl(51, 204, 204) == (180, 60, 50) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) def test_hsl_to_rgb_part_11(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 20, 50) == (102, 102, 153) assert hsl_to_rgb(240, 60, 50) == (51, 51, 204) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) def test_rgb_to_hsl_part_11(): """Test rgb to hsl color function""" assert rgb_to_hsl(102, 102, 153) == (240, 20, 50) assert rgb_to_hsl(51, 51, 204) == (240, 60, 50) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) def test_hsl_to_rgb_part_12(): """Test hsl to rgb color function""" assert hsl_to_rgb(300, 20, 50) == (153, 102, 153) assert hsl_to_rgb(300, 60, 50) == (204, 51, 204) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) def test_rgb_to_hsl_part_12(): """Test rgb to hsl color function""" assert rgb_to_hsl(153, 102, 153) == (300, 20, 50) assert rgb_to_hsl(204, 51, 204) == (300, 60, 50) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) def test_hsl_to_rgb_part_13(): """Test hsl to rgb color function""" assert hsl_to_rgb(0, 100, 0) == (0, 0, 0) assert hsl_to_rgb(0, 100, 10) == (51, 0, 0) assert hsl_to_rgb(0, 100, 20) == (102, 0, 0) assert hsl_to_rgb(0, 100, 30) == (153, 0, 0) assert hsl_to_rgb(0, 100, 40) == (204, 0, 0) assert hsl_to_rgb(0, 100, 50) == (255, 0, 0) assert hsl_to_rgb(0, 100, 60) == (255, 51, 51) assert hsl_to_rgb(0, 100, 70) == (255, 102, 102) assert hsl_to_rgb(0, 100, 80) == (255, 153, 153) assert hsl_to_rgb(0, 100, 90) == (255, 204, 204) assert hsl_to_rgb(0, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_13(): """Test rgb to hsl color function""" assert rgb_to_hsl(0, 0, 0) == (0, 0, 0) assert rgb_to_hsl(51, 0, 0) == (0, 100, 10) assert rgb_to_hsl(102, 0, 0) == (0, 100, 20) assert rgb_to_hsl(153, 0, 0) == (0, 100, 30) assert rgb_to_hsl(204, 0, 0) == (0, 100, 40) assert rgb_to_hsl(255, 0, 0) == (0, 100, 50) assert rgb_to_hsl(255, 51, 51) == (0, 100, 60) assert rgb_to_hsl(255, 102, 102) == (0, 100, 70) assert rgb_to_hsl(255, 153, 153) == (0, 100, 80) assert rgb_to_hsl(255, 204, 204) == (0, 100, 90) assert rgb_to_hsl(255, 255, 255) == (0, 0, 100) def test_hsl_to_rgb_part_14(): """Test hsl to rgb color function""" assert hsl_to_rgb(60, 100, 0) == (0, 0, 0) assert hsl_to_rgb(60, 100, 10) == (51, 51, 0) assert hsl_to_rgb(60, 100, 20) == (102, 102, 0) assert hsl_to_rgb(60, 100, 30) == (153, 153, 0) assert hsl_to_rgb(60, 100, 40) == (204, 204, 0) assert hsl_to_rgb(60, 100, 50) == (255, 255, 0) assert hsl_to_rgb(60, 100, 60) == (255, 255, 51) assert hsl_to_rgb(60, 100, 70) == (255, 255, 102) assert hsl_to_rgb(60, 100, 80) == (255, 255, 153) assert hsl_to_rgb(60, 100, 90) == (255, 255, 204) assert hsl_to_rgb(60, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_14(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (60, 100, 0) assert rgb_to_hsl(51, 51, 0) == (60, 100, 10) assert rgb_to_hsl(102, 102, 0) == (60, 100, 20) assert rgb_to_hsl(153, 153, 0) == (60, 100, 30) assert rgb_to_hsl(204, 204, 0) == (60, 100, 40) assert rgb_to_hsl(255, 255, 0) == (60, 100, 50) assert rgb_to_hsl(255, 255, 51) == (60, 100, 60) assert rgb_to_hsl(255, 255, 102) == (60, 100, 70) assert rgb_to_hsl(255, 255, 153) == (60, 100, 80) assert rgb_to_hsl(255, 255, 204) == (60, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (60, 100, 100) def test_hsl_to_rgb_part_15(): """Test hsl to rgb color function""" assert hsl_to_rgb(120, 100, 0) == (0, 0, 0) assert hsl_to_rgb(120, 100, 10) == (0, 51, 0) assert hsl_to_rgb(120, 100, 20) == (0, 102, 0) assert hsl_to_rgb(120, 100, 30) == (0, 153, 0) assert hsl_to_rgb(120, 100, 40) == (0, 204, 0) assert hsl_to_rgb(120, 100, 50) == (0, 255, 0) assert hsl_to_rgb(120, 100, 60) == (51, 255, 51) assert hsl_to_rgb(120, 100, 70) == (102, 255, 102) assert hsl_to_rgb(120, 100, 80) == (153, 255, 153) assert hsl_to_rgb(120, 100, 90) == (204, 255, 204) assert hsl_to_rgb(120, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_15(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (120, 100, 0) assert rgb_to_hsl(0, 51, 0) == (120, 100, 10) assert rgb_to_hsl(0, 102, 0) == (120, 100, 20) assert rgb_to_hsl(0, 153, 0) == (120, 100, 30) assert rgb_to_hsl(0, 204, 0) == (120, 100, 40) assert rgb_to_hsl(0, 255, 0) == (120, 100, 50) assert rgb_to_hsl(51, 255, 51) == (120, 100, 60) assert rgb_to_hsl(102, 255, 102) == (120, 100, 70) assert rgb_to_hsl(153, 255, 153) == (120, 100, 80) assert rgb_to_hsl(204, 255, 204) == (120, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (120, 100, 100) def test_hsl_to_rgb_part_16(): """Test hsl to rgb color function""" assert hsl_to_rgb(180, 100, 0) == (0, 0, 0) assert hsl_to_rgb(180, 100, 10) == (0, 51, 51) assert hsl_to_rgb(180, 100, 20) == (0, 102, 102) assert hsl_to_rgb(180, 100, 30) == (0, 153, 153) assert hsl_to_rgb(180, 100, 40) == (0, 204, 204) assert hsl_to_rgb(180, 100, 50) == (0, 255, 255) assert hsl_to_rgb(180, 100, 60) == (51, 255, 255) assert hsl_to_rgb(180, 100, 70) == (102, 255, 255) assert hsl_to_rgb(180, 100, 80) == (153, 255, 255) assert hsl_to_rgb(180, 100, 90) == (204, 255, 255) assert hsl_to_rgb(180, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_16(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (180, 100, 0) assert rgb_to_hsl(0, 51, 51) == (180, 100, 10) assert rgb_to_hsl(0, 102, 102) == (180, 100, 20) assert rgb_to_hsl(0, 153, 153) == (180, 100, 30) assert rgb_to_hsl(0, 204, 204) == (180, 100, 40) assert rgb_to_hsl(0, 255, 255) == (180, 100, 50) assert rgb_to_hsl(51, 255, 255) == (180, 100, 60) assert rgb_to_hsl(102, 255, 255) == (180, 100, 70) assert rgb_to_hsl(153, 255, 255) == (180, 100, 80) assert rgb_to_hsl(204, 255, 255) == (180, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (180, 100, 100) def test_hsl_to_rgb_part_17(): """Test hsl to rgb color function""" assert hsl_to_rgb(240, 100, 0) == (0, 0, 0) assert hsl_to_rgb(240, 100, 10) == (0, 0, 51) assert hsl_to_rgb(240, 100, 20) == (0, 0, 102) assert hsl_to_rgb(240, 100, 30) == (0, 0, 153) assert hsl_to_rgb(240, 100, 40) == (0, 0, 204) assert hsl_to_rgb(240, 100, 50) == (0, 0, 255) assert hsl_to_rgb(240, 100, 60) == (51, 51, 255) assert hsl_to_rgb(240, 100, 70) == (102, 102, 255) assert hsl_to_rgb(240, 100, 80) == (153, 153, 255) assert hsl_to_rgb(240, 100, 90) == (204, 204, 255) assert hsl_to_rgb(240, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_17(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (240, 100, 0) assert rgb_to_hsl(0, 0, 51) == (240, 100, 10) assert rgb_to_hsl(0, 0, 102) == (240, 100, 20) assert rgb_to_hsl(0, 0, 153) == (240, 100, 30) assert rgb_to_hsl(0, 0, 204) == (240, 100, 40) assert rgb_to_hsl(0, 0, 255) == (240, 100, 50) assert rgb_to_hsl(51, 51, 255) == (240, 100, 60) assert rgb_to_hsl(102, 102, 255) == (240, 100, 70) assert rgb_to_hsl(153, 153, 255) == (240, 100, 80) assert rgb_to_hsl(204, 204, 255) == (240, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (240, 100, 100) def test_hsl_to_rgb_part_18(): """Test hsl to rgb color function""" assert hsl_to_rgb(300, 100, 0) == (0, 0, 0) assert hsl_to_rgb(300, 100, 10) == (51, 0, 51) assert hsl_to_rgb(300, 100, 20) == (102, 0, 102) assert hsl_to_rgb(300, 100, 30) == (153, 0, 153) assert hsl_to_rgb(300, 100, 40) == (204, 0, 204) assert hsl_to_rgb(300, 100, 50) == (255, 0, 255) assert hsl_to_rgb(300, 100, 60) == (255, 51, 255) assert hsl_to_rgb(300, 100, 70) == (255, 102, 255) assert hsl_to_rgb(300, 100, 80) == (255, 153, 255) assert hsl_to_rgb(300, 100, 90) == (255, 204, 255) assert hsl_to_rgb(300, 100, 100) == (255, 255, 255) def test_rgb_to_hsl_part_18(): """Test rgb to hsl color function""" # assert rgb_to_hsl(0, 0, 0) == (300, 100, 0) assert rgb_to_hsl(51, 0, 51) == (300, 100, 10) assert rgb_to_hsl(102, 0, 102) == (300, 100, 20) assert rgb_to_hsl(153, 0, 153) == (300, 100, 30) assert rgb_to_hsl(204, 0, 204) == (300, 100, 40) assert rgb_to_hsl(255, 0, 255) == (300, 100, 50) assert rgb_to_hsl(255, 51, 255) == (300, 100, 60) assert rgb_to_hsl(255, 102, 255) == (300, 100, 70) assert rgb_to_hsl(255, 153, 255) == (300, 100, 80) assert rgb_to_hsl(255, 204, 255) == (300, 100, 90) # assert rgb_to_hsl(255, 255, 255) == (300, 100, 100) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_config.py0000664000175000017500000004115200000000000020372 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Various config options tested on one chart type or more""" from tempfile import NamedTemporaryFile from pygal import ( XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge, Histogram, HorizontalBar, HorizontalLine, HorizontalStackedBar, HorizontalStackedLine, Line, Pie, Pyramid, Radar, SolidGauge, TimeDeltaLine, TimeLine, Treemap, formatters, ) from pygal.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) + '‰') 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) + '‰', 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 = "þæ®þ怀&ij¿’€" q = line.render_pyquery() assert q(".text-overlay text").text() == "þæ®þ怀&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('ééé', [1, 2, 3]) chart.add('èèè', [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: '%s¥' % x}]) chart.add('_b', [4, 5, 6], formatter=lambda x: '%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(('4€', '5€', '6€', '1_a$', '2_a$', '3¥') + (('6_a$', '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' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_date.py0000664000175000017500000001077700000000000020053 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Date related charts tests""" from datetime import date, datetime, time, timedelta, timezone from pygal import DateLine, DateTimeLine, TimeDeltaLine, TimeLine from pygal._compat import timestamp 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() dates = list(map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) assert dates == ['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() dates = list(map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) assert dates == [ '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() dates = list(map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) assert dates == [ '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() dates = list(map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) assert dates == [ '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() dates = list(map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) assert dates == ['2013-01-01', '2013-02-01', '2013-03-01'] def test_utc_timestamping(): t = datetime(2017, 7, 14, 2, 40).replace(tzinfo=timezone.utc) assert timestamp(t) == 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_formatters.py0000664000175000017500000000474700000000000021324 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Test formatters""" from pygal import formatters 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) == '100µ' assert f(0.000123) == '123µ' assert f(0.00001) == '10µ' assert f(0.000001) == '1µ' assert f(0.0000001) == '100n' assert f(0.0000000001) == '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) == '∅' 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' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_graph.py0000664000175000017500000003113600000000000020227 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Generate tests for different chart types with different data""" import io import os import uuid import pytest import pygal 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( 'Série1', [{ 'value': 1, 'xlink': 'http://1/', 'label': '°ijæð©&×&<—×€¿_…' }, { 'value': 2, 'xlink': { 'href': 'http://6.example.com/' }, 'label': 'æÂ°€≠|€æÂ°€əæ' }, { 'value': 3, 'label': 'unicode <3' }] ) if not chart._dual: chart.x_labels = ['&œ', '¿?', '††††††††', 'unicode <3'] chart.render_pyquery() def test_unicode_labels(Chart): chart = Chart() chart.add( '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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_histogram.py0000664000175000017500000000210700000000000021117 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Histogram chart related tests""" from pygal import Histogram def test_histogram(): """Simple histogram test""" hist = Histogram() hist.add('1', [(2, 0, 1), (4, 1, 3), (3, 3.5, 5), (1.5, 5, 10)]) hist.add('2', [(2, 2, 8)], secondary=True) q = hist.render_pyquery() assert len(q('.rect')) == 5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_interpolate.py0000664000175000017500000000736700000000000021465 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Interpolations tests""" from pygal.test import make_data def test_cubic(Chart, datas): """Test cubic interpolation""" chart = Chart(interpolate='cubic') chart = make_data(chart, datas) assert chart.render() def test_cubic_prec(Chart, datas): """Test cubic interpolation precision""" chart = Chart(interpolate='cubic', interpolation_precision=200) chart = make_data(chart, datas) chart_low = Chart(interpolate='cubic', interpolation_precision=5) chart_low = make_data(chart, datas) assert len(chart.render()) >= len(chart_low.render()) def test_quadratic(Chart, datas): """Test quadratic interpolation""" chart = Chart(interpolate='quadratic') chart = make_data(chart, datas) assert chart.render() def test_lagrange(Chart, datas): """Test lagrange interpolation""" chart = Chart(interpolate='lagrange') chart = make_data(chart, datas) assert chart.render() def test_trigonometric(Chart, datas): """Test trigonometric interpolation""" chart = Chart(interpolate='trigonometric') chart = make_data(chart, datas) assert chart.render() def test_hermite(Chart, datas): """Test hermite interpolation""" chart = Chart(interpolate='hermite') chart = make_data(chart, datas) assert chart.render() def test_hermite_finite(Chart, datas): """Test hermite finite difference interpolation""" chart = Chart( interpolate='hermite', interpolation_parameters={'type': 'finite_difference'} ) chart = make_data(chart, datas) assert chart.render() def test_hermite_cardinal(Chart, datas): """Test hermite cardinal interpolation""" chart = Chart( interpolate='hermite', interpolation_parameters={ 'type': 'cardinal', 'c': .75 } ) chart = make_data(chart, datas) assert chart.render() def test_hermite_catmull_rom(Chart, datas): """Test hermite catmull rom interpolation""" chart = Chart( interpolate='hermite', interpolation_parameters={'type': 'catmull_rom'} ) chart = make_data(chart, datas) assert chart.render() def test_hermite_kochanek_bartels(Chart, datas): """Test hermite kochanek bartels interpolation""" chart = Chart( interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1 } ) chart = make_data(chart, datas) assert chart.render() chart = Chart( interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': -1, 'c': -8, 't': 0 } ) chart = make_data(chart, datas) assert chart.render() chart = Chart( interpolate='hermite', interpolation_parameters={ 'type': 'kochanek_bartels', 'b': 0, 'c': 10, 't': -1 } ) chart = make_data(chart, datas) assert chart.render() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_line.py0000664000175000017500000001235000000000000020052 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Line chart related tests""" from __future__ import division from math import cos, sin from pygal import Line from pygal.test.utils import texts def test_simple_line(): """Simple line test""" line = Line() rng = range(-30, 31, 5) line.add('test1', [cos(x / 10) for x in rng]) line.add('test2', [sin(x / 10) for x in rng]) line.add('test3', [cos(x / 10) - sin(x / 10) for x in rng]) line.x_labels = map(str, rng) line.title = "cos sin and cos - sin" q = line.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 3 assert len(q(".legend")) == 3 assert len(q(".x.axis .guides")) == 13 assert len(q(".y.axis .guides")) == 13 assert len(q(".dots")) == 3 * 13 assert q(".axis.x text").map(texts) == [ '-30', '-25', '-20', '-15', '-10', '-5', '0', '5', '10', '15', '20', '25', '30' ] assert q(".axis.y text").map(texts) == [ '-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6', '0.8', '1', '1.2' ] assert q(".title").text() == 'cos sin and cos - sin' assert q(".legend text").map(texts) == ['test1', 'test2', 'test3'] def test_line(): """Another simple line test""" line = Line() rng = [8, 12, 23, 73, 39, 57] line.add('Single serie', rng) line.title = "One serie" q = line.render_pyquery() assert len(q(".axis.x")) == 0 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 1 assert len(q(".x.axis .guides")) == 0 assert len(q(".y.axis .guides")) == 7 def test_one_dot(): """Line test with an unique value""" line = Line() line.add('one dot', [12]) line.x_labels = ['one'] q = line.render_pyquery() assert len(q(".axis.x")) == 1 assert len(q(".axis.y")) == 1 assert len(q(".y.axis .guides")) == 1 def test_no_dot(): """Line test with an empty serie""" line = Line() line.add('no dot', []) q = line.render_pyquery() assert q(".text-overlay text").text() == 'No data' def test_no_dot_at_all(): """Line test with no value""" q = Line().render_pyquery() assert q(".text-overlay text").text() == 'No data' def test_not_equal_x_labels(): """Test x_labels""" line = Line() line.add('test1', range(100)) line.truncate_label = -1 line.x_labels = map(str, range(11)) q = line.render_pyquery() assert len(q(".dots")) == 100 assert len(q(".axis.x")) == 1 assert q(".axis.x text").map(texts) == [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ] def test_int_x_labels(): """Test x_labels""" line = Line() line.add('test1', range(100)) line.truncate_label = -1 line.x_labels = list(range(11)) q = line.render_pyquery() assert len(q(".dots")) == 100 assert len(q(".axis.x")) == 1 assert q(".axis.x text").map(texts) == [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ] def test_only_major_dots_every(): """Test major dots""" line = Line(show_only_major_dots=True, x_labels_major_every=3) line.add('test', range(12)) line.x_labels = map(str, range(12)) q = line.render_pyquery() assert len(q(".dots")) == 4 def test_only_major_dots_no_labels(): """Test major dots with no labels""" line = Line(show_only_major_dots=True) line.add('test', range(12)) q = line.render_pyquery() assert len(q(".dots")) == 12 def test_only_major_dots_count(): """Test major dots with a major label count""" line = Line(show_only_major_dots=True) line.add('test', range(12)) line.x_labels = map(str, range(12)) line.x_labels_major_count = 2 q = line.render_pyquery() assert len(q(".dots")) == 2 def test_only_major_dots(): """Test major dots with specified major labels""" line = Line(show_only_major_dots=True, ) line.add('test', range(12)) line.x_labels = map(str, range(12)) line.x_labels_major = ['1', '5', '11'] q = line.render_pyquery() assert len(q(".dots")) == 3 def test_line_secondary(): """Test line with a secondary serie""" line = Line() rng = [8, 12, 23, 73, 39, 57] line.add('First serie', rng) line.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True) line.title = "One serie" q = line.render_pyquery() assert len(q(".axis.x")) == 0 assert len(q(".axis.y")) == 1 assert len(q(".plot .series path")) == 2 assert len(q(".x.axis .guides")) == 0 assert len(q(".y.axis .guides")) == 7 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_line_log_none_max_solved.py0000664000175000017500000000074100000000000024154 0ustar00gillesgilles00000000000000# 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_maps.py0000664000175000017500000000212300000000000020060 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Map plugins tests are imported here""" from importlib_metadata import entry_points # Load plugins tests for entry in entry_points(group="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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_pie.py0000664000175000017500000000413400000000000017701 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Donut chart related tests""" from pygal import Pie def test_donut(): """Test a donut pie chart""" chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage in February 2012 (in %)' chart.add('IE', 19.5) chart.add('Firefox', 36.6) chart.add('Chrome', 36.3) chart.add('Safari', 4.5) chart.add('Opera', 2.3) assert chart.render() def test_multiseries_donut(): """Test a donut pie chart with multiserie""" # this just demos that the multiseries pie does not respect # the inner_radius chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage by version in February 2012 (in %)' chart.add('IE', [5.7, 10.2, 2.6, 1]) chart.add('Firefox', [.6, 16.8, 7.4, 2.2, 1.2, 1, 1, 1.1, 4.3, 1]) chart.add('Chrome', [.3, .9, 17.1, 15.3, .6, .5, 1.6]) chart.add('Safari', [4.4, .1]) chart.add('Opera', [.1, 1.6, .1, .5]) assert chart.render() def test_half_pie(): """Test a half pie chart""" pie = Pie() pie.add('IE', 19.5) pie.add('Firefox', 36.6) pie.add('Chrome', 36.3) pie.add('Safari', 4.5) pie.add('Opera', 2.3) half = Pie(half_pie=True) half.add('IE', 19.5) half.add('Firefox', 36.6) half.add('Chrome', 36.3) half.add('Safari', 4.5) half.add('Opera', 2.3) assert pie.render() != half.render() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_serie_config.py0000664000175000017500000000436700000000000021570 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Test per serie configuration""" from pygal import Line s1 = [1, 3, 12, 3, 4] s2 = [7, -4, 10, None, 8, 3, 1] def test_no_serie_config(): """Test per serie no configuration""" chart = Line() chart.add('1', s1) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 1 assert len(q('.serie-1 .line')) == 1 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_global_config(): """Test global configuration""" chart = Line(stroke=False) chart.add('1', s1) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 0 assert len(q('.serie-1 .line')) == 0 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_serie_config(): """Test per serie configuration""" chart = Line() chart.add('1', s1, stroke=False) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 0 assert len(q('.serie-1 .line')) == 1 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 def test_serie_precedence_over_global_config(): """Test that per serie configuration overide global configuration""" chart = Line(stroke=False) chart.add('1', s1, stroke=True) chart.add('2', s2) q = chart.render_pyquery() assert len(q('.serie-0 .line')) == 1 assert len(q('.serie-1 .line')) == 0 assert len(q('.serie-0 .dot')) == 5 assert len(q('.serie-1 .dot')) == 6 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_sparktext.py0000664000175000017500000000514400000000000021153 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Test sparktext rendering""" from pygal import Bar, Line def test_basic_sparktext(): """Test basic sparktext""" chart = Line() chart.add('_', [1, 5, 22, 13, 53]) assert chart.render_sparktext() == '▁▁▃▂█' def test_all_sparktext(): """Test all character sparktext""" chart = Line() chart.add('_', range(8)) assert chart.render_sparktext() == '▁▂▃▄▅▆▇█' 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() == '▁▂▃▄▅▆▇█' assert chart.render_sparktext(relative_to=0) == '▇▇▇▇▇▇▇█' 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() == '▁▂▃▄▂█' 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() == '▁▂█▁' def test_no_data_sparktext(): """Test no data sparktext""" chart2 = Line() chart2.add('_', []) assert chart2.render_sparktext() == '' chart3 = Line() assert chart3.render_sparktext() == '' def test_same_max_and_relative_values_sparktext(): """Test flat sparktexts""" chart = Line() chart.add('_', [0, 0, 0, 0, 0]) assert chart.render_sparktext() == '▁▁▁▁▁' chart2 = Line() chart2.add('_', [1, 1, 1, 1, 1]) assert chart2.render_sparktext(relative_to=1) == '▁▁▁▁▁' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_stacked.py0000664000175000017500000000414400000000000020543 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Stacked chart related tests""" from pygal import StackedLine def test_stacked_line(): """Test stacked line""" stacked = StackedLine() stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)') ) def test_stacked_line_reverse(): """Test stack from top stacked line""" stacked = StackedLine(stack_from_top=True) stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('11 (+1)', '14 (+2)', '10', '12') ) def test_stacked_line_log(): """Test logarithmic stacked line""" stacked = StackedLine(logarithmic=True) stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)') ) def test_stacked_line_interpolate(): """Test interpolated stacked line""" stacked = StackedLine(interpolate='cubic') stacked.add('one_two', [1, 2]) stacked.add('ten_twelve', [10, 12]) q = stacked.render_pyquery() assert set([v.text for v in q("desc.value")]) == set( ('1', '2', '11 (+10)', '14 (+12)') ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_style.py0000664000175000017500000000316500000000000020267 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Style related tests""" from pygal import Line from pygal.style import ( DarkenStyle, DesaturateStyle, LightenStyle, LightStyle, RotateStyle, SaturateStyle, ) STYLES = LightenStyle, DarkenStyle, SaturateStyle, DesaturateStyle, RotateStyle def test_parametric_styles(): """Test that no parametric produce the same result""" chart = None for style in STYLES: line = Line(style=style('#f4e83a')) line.add('_', [1, 2, 3]) line.x_labels = 'abc' new_chart = line.render() assert chart != new_chart chart = new_chart def test_parametric_styles_with_parameters(): """Test a parametric style with parameters""" line = Line( style=RotateStyle('#de3804', step=12, max_=180, base_style=LightStyle) ) line.add('_', [1, 2, 3]) line.x_labels = 'abc' assert line.render() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_table.py0000664000175000017500000000233700000000000020216 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Box chart related tests""" from pyquery import PyQuery as pq from pygal import Pie def test_pie_table(): """Test rendering a table for a pie""" chart = Pie(inner_radius=.3, pretty_print=True) chart.title = 'Browser usage in February 2012 (in %)' chart.add('IE', 19.5) chart.add('Firefox', 36.6) chart.add('Chrome', 36.3) chart.add('Safari', 4.5) chart.add('Opera', 2.3) q = pq(chart.render_table()) assert len(q('table')) == 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_util.py0000664000175000017500000001421100000000000020076 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Utility functions tests""" from pytest import raises 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', 'foo foo foo bar', 'foo béè b¡ð/ijə˘©þß®~¯æ', '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) == '1234…' assert truncate('1234567890', 1) == '…' assert truncate('1234567890', 9) == '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'] assert mergextend(['a', ..., 'b'], ['c', 'd']) == ['a', 'c', 'd', 'b'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_view.py0000664000175000017500000000200400000000000020070 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """View related tests""" # TODO def test_all_logarithmic(Chart): """Test logarithmic view rendering""" chart = Chart(logarithmic=True) chart.add('1', [1, 30, 8, 199, -23]) chart.add('2', [87, 42, .9, 189, 81]) assert chart.render() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/test_xml_filters.py0000664000175000017500000000433000000000000021452 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Xml filter tests""" from pygal import Bar class ChangeBarsXMLFilter(object): """xml filter that insert a subplot""" def __init__(self, a, b): """Generate data""" self.data = [b[i] - a[i] for i in range(len(a))] def __call__(self, T): """Apply the filter on the tree""" subplot = Bar( legend_at_bottom=True, explicit_size=True, width=800, height=150 ) subplot.add("Difference", self.data) subplot = subplot.render_tree() subplot = subplot.findall("g")[0] T.insert(2, subplot) T.findall("g")[1].set('transform', 'translate(0,150), scale(1,0.75)') return T def test_xml_filters_round_trip(): """Ensure doing nothing does nothing""" plot = Bar() plot.add("A", [60, 75, 80, 78, 83, 90]) plot.add("B", [92, 87, 81, 73, 68, 55]) before = plot.render() plot.add_xml_filter(lambda T: T) after = plot.render() assert before == after def test_xml_filters_change_bars(): """Test the use a xml filter""" plot = Bar( legend_at_bottom=True, explicit_size=True, width=800, height=600 ) A = [60, 75, 80, 78, 83, 90] B = [92, 87, 81, 73, 68, 55] plot.add("A", A) plot.add("B", B) plot.add_xml_filter(ChangeBarsXMLFilter(A, B)) q = plot.render_tree() assert len(q.findall("g")) == 2 assert q.findall("g")[1].attrib["transform" ] == "translate(0,150), scale(1,0.75)" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/test/utils.py0000664000175000017500000000163300000000000017226 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Tests helpers""" from pyquery import PyQuery as pq def texts(i, e): """Helper for getting the text of an element""" return pq(e).text() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/util.py0000664000175000017500000002601100000000000016061 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Various utility functions""" from __future__ import division import re from decimal import Decimal from math import ceil, cos, floor, log10, pi, sin 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 = str(xlink.get('href')) if 'tooltip' in metadata: svg.node(node, 'title').text = str(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 and metadata['label']: svg.node( node, 'desc', class_='label' ).text = str(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] + '…' 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:] == r'\*/': 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal/view.py0000664000175000017500000003104100000000000016055 0ustar00gillesgilles00000000000000# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """Projection and bounding helpers""" from __future__ import division from math import cos, log10, pi, sin class Margin(object): """Class reprensenting a margin (top, right, left, bottom)""" def __init__(self, top, right, bottom, left): """Create the margin object from the top, right, left, bottom margin""" self.top = top self.right = right self.bottom = bottom self.left = left @property def x(self): """Helper for total x margin""" return self.left + self.right @property def y(self): """Helper for total y margin""" return self.top + self.bottom class Box(object): """Chart boundings""" margin = .02 def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): """ Create the chart bounds with min max horizontal and vertical values """ self._xmin = xmin self._ymin = ymin self._xmax = xmax self._ymax = ymax def set_polar_box(self, rmin=0, rmax=1, tmin=0, tmax=2 * pi): """Helper for polar charts""" self._rmin = rmin self._rmax = rmax self._tmin = tmin self._tmax = tmax self.xmin = self.ymin = rmin - rmax self.xmax = self.ymax = rmax - rmin @property def xmin(self): """X minimum getter""" return self._xmin @xmin.setter def xmin(self, value): """X minimum setter""" if value is not None: self._xmin = value @property def ymin(self): """Y minimum getter""" return self._ymin @ymin.setter def ymin(self, value): """Y minimum setter""" if value is not None: self._ymin = value @property def xmax(self): """X maximum getter""" return self._xmax @xmax.setter def xmax(self, value): """X maximum setter""" if value is not None: self._xmax = value @property def ymax(self): """Y maximum getter""" return self._ymax @ymax.setter def ymax(self, value): """Y maximum setter""" if value or self.ymin: self._ymax = value @property def width(self): """Helper for box width""" return self.xmax - self.xmin @property def height(self): """Helper for box height""" return self.ymax - self.ymin def swap(self): """Return the box (for horizontal graphs)""" self.xmin, self.ymin = self.ymin, self.xmin self.xmax, self.ymax = self.ymax, self.xmax def fix(self, with_margin=True): """Correct box when no values and take margin in account""" if not self.width: self.xmax = self.xmin + 1 if not self.height: self.ymin /= 2 self.ymax += self.ymin xmargin = self.margin * self.width self.xmin -= xmargin self.xmax += xmargin if with_margin: ymargin = self.margin * self.height self.ymin -= ymargin self.ymax += ymargin class View(object): """Projection base class""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.box.fix() def x(self, x): """Project x""" if x is None: return None return self.width * (x - self.box.xmin) / self.box.width def y(self, y): """Project y""" if y is None: return None return ( self.height - self.height * (y - self.box.ymin) / self.box.height ) def __call__(self, xy): """Project x and y""" x, y = xy return (self.x(x), self.y(y)) class ReverseView(View): """Same as view but reversed vertically""" def y(self, y): """Project reversed y""" if y is None: return None return (self.height * (y - self.box.ymin) / self.box.height) class HorizontalView(View): """Same as view but transposed""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self._force_vertical = None self.width = width self.height = height self.box = box self.box.fix() self.box.swap() def x(self, x): """Project x as y""" if x is None: return None if self._force_vertical: return super(HorizontalView, self).x(x) return super(HorizontalView, self).y(x) def y(self, y): """Project y as x""" if y is None: return None if self._force_vertical: return super(HorizontalView, self).y(y) return super(HorizontalView, self).x(y) class PolarView(View): """Polar projection for pie like graphs""" def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta return super(PolarView, self).__call__((rho * cos(theta), rho * sin(theta))) class PolarLogView(View): """Logarithmic polar projection""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" super(PolarLogView, self).__init__(width, height, box) if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): raise Exception( 'Box must be set with set_polar_box for polar charts' ) self.log10_rmax = log10(self.box._rmax) self.log10_rmin = log10(self.box._rmin) if self.log10_rmin == self.log10_rmax: self.log10_rmax = self.log10_rmin + 1 def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta # Center case if rho == 0: return super(PolarLogView, self).__call__((0, 0)) rho = (self.box._rmax - self.box._rmin) * ( log10(rho) - self.log10_rmin ) / (self.log10_rmax - self.log10_rmin) return super(PolarLogView, self).__call__((rho * cos(theta), rho * sin(theta))) class PolarThetaView(View): """Logarithmic polar projection""" def __init__(self, width, height, box, aperture=pi / 3): """Create the view with a width an height and a box bounds""" super(PolarThetaView, self).__init__(width, height, box) self.aperture = aperture if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts' ) def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta start = 3 * pi / 2 + self.aperture / 2 theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / ( self.box._tmax - self.box._tmin ) return super(PolarThetaView, self).__call__((rho * cos(theta), rho * sin(theta))) class PolarThetaLogView(View): """Logarithmic polar projection""" def __init__(self, width, height, box, aperture=pi / 3): """Create the view with a width an height and a box bounds""" super(PolarThetaLogView, self).__init__(width, height, box) self.aperture = aperture if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts' ) self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0 self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0 if self.log10_tmin == self.log10_tmax: self.log10_tmax = self.log10_tmin + 1 def __call__(self, rhotheta): """Project rho and theta""" if None in rhotheta: return None, None rho, theta = rhotheta # Center case if theta == 0: return super(PolarThetaLogView, self).__call__((0, 0)) theta = self.box._tmin + (self.box._tmax - self.box._tmin) * ( log10(theta) - self.log10_tmin ) / (self.log10_tmax - self.log10_tmin) start = 3 * pi / 2 + self.aperture / 2 theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / ( self.box._tmax - self.box._tmin ) return super(PolarThetaLogView, self).__call__((rho * cos(theta), rho * sin(theta))) class LogView(View): """Y Logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 if self.log10_ymin == self.log10_ymax: self.log10_ymax = self.log10_ymin + 1 self.box.fix(False) def y(self, y): """Project y""" if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0: return 0 return ( self.height - self.height * (log10(y) - self.log10_ymin) / (self.log10_ymax - self.log10_ymin) ) class XLogView(View): """X logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 self.box.fix(False) def x(self, x): """Project x""" if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0: return None return ( self.width * (log10(x) - self.log10_xmin) / (self.log10_xmax - self.log10_xmin) ) class XYLogView(XLogView, LogView): """X and Y logarithmic projection""" def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self.width = width self.height = height self.box = box self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 self.box.fix(False) class HorizontalLogView(XLogView): """Transposed Logarithmic projection""" # Do not want to call the parent here def __init__(self, width, height, box): """Create the view with a width an height and a box bounds""" self._force_vertical = None self.width = width self.height = height self.box = box self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0 if self.log10_xmin == self.log10_xmax: self.log10_xmax = self.log10_xmin + 1 self.box.fix(False) self.box.swap() def x(self, x): """Project x as y""" if x is None: return None if self._force_vertical: return super(HorizontalLogView, self).x(x) return super(XLogView, self).y(x) def y(self, y): """Project y as x""" if y is None: return None if self._force_vertical: return super(XLogView, self).y(y) return super(HorizontalLogView, self).x(y) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0658777 pygal-3.0.5/pygal.egg-info/0000775000175000017500000000000000000000000016224 5ustar00gillesgilles00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474487.0 pygal-3.0.5/pygal.egg-info/PKG-INFO0000644000175000017500000000666300000000000017332 0ustar00gillesgilles00000000000000Metadata-Version: 2.1 Name: pygal Version: 3.0.5 Summary: A Python svg graph plotting library Home-page: https://www.pygal.org/ Author: Florian Mounier / Kozea Author-email: community@kozea.fr License: GNU LGPL v3+ 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 :: 3 Classifier: Topic :: Multimedia :: Graphics :: Presentation Provides: pygal Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: COPYING Requires-Dist: importlib-metadata Provides-Extra: lxml Requires-Dist: lxml; extra == "lxml" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Requires-Dist: pygal_sphinx_directives; extra == "docs" Provides-Extra: png Requires-Dist: cairosvg; extra == "png" Provides-Extra: test Requires-Dist: cairosvg; extra == "test" Requires-Dist: coveralls; extra == "test" Requires-Dist: lxml; extra == "test" Requires-Dist: pyquery; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: ruff>=0.5.6; extra == "test" Provides-Extra: moulinrouge Requires-Dist: flask; extra == "moulinrouge" Requires-Dist: pygal_maps_ch; extra == "moulinrouge" Requires-Dist: pygal_maps_fr; extra == "moulinrouge" Requires-Dist: pygal_maps_world; extra == "moulinrouge" # 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 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474488.0 pygal-3.0.5/pygal.egg-info/SOURCES.txt0000664000175000017500000000341500000000000020113 0ustar00gillesgilles00000000000000COPYING README pygal_gen.py 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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474487.0 pygal-3.0.5/pygal.egg-info/dependency_links.txt0000664000175000017500000000000100000000000022272 0ustar00gillesgilles00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474487.0 pygal-3.0.5/pygal.egg-info/requires.txt0000664000175000017500000000036000000000000020623 0ustar00gillesgilles00000000000000importlib-metadata [docs] sphinx sphinx_rtd_theme pygal_sphinx_directives [lxml] lxml [moulinrouge] flask pygal_maps_ch pygal_maps_fr pygal_maps_world [png] cairosvg [test] cairosvg coveralls lxml pyquery pytest pytest-cov ruff>=0.5.6 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474487.0 pygal-3.0.5/pygal.egg-info/top_level.txt0000664000175000017500000000000600000000000020752 0ustar00gillesgilles00000000000000pygal ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722259434.0 pygal-3.0.5/pygal_gen.py0000775000175000017500000000443100000000000015742 0ustar00gillesgilles00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . import argparse import pygal parser = argparse.ArgumentParser( description='Generate pygal chart in command line', prog='pygal_gen') parser.add_argument('-t', '--type', dest='type', default='Line', choices=map(lambda x: x.__name__, pygal.CHARTS), help='Kind of chart to generate') parser.add_argument('-o', '--output', dest='filename', default='pygal_out.svg', help='Filename to write the svg to') parser.add_argument('-s', '--serie', dest='series', nargs='+', action='append', help='Add a serie in the form (title val1 val2...)') parser.add_argument('--version', action='version', version='pygal %s' % pygal.__version__) for key in pygal.config.CONFIG_ITEMS: opt_name = key.name val = key.value opts = {} if key.type == list: opts['type'] = key.subtype opts['nargs'] = '+' else: opts['type'] = key.type if opts['type'] == bool: del opts['type'] opts['action'] = 'store_true' if not val else 'store_false' if val: opt_name = 'no-' + opt_name if key.name == 'interpolate': opts['choices'] = list(pygal.interpolate.INTERPOLATIONS.keys()) parser.add_argument( '--%s' % opt_name, dest=key.name, default=val, **opts) config = parser.parse_args() chart = getattr(pygal, config.type)(**vars(config)) for serie in config.series: chart.add(serie[0], map(float, serie[1:])) chart.render_to_file(config.filename) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1723474488.0658777 pygal-3.0.5/setup.cfg0000664000175000017500000000004600000000000015237 0ustar00gillesgilles00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1723474433.0 pygal-3.0.5/setup.py0000664000175000017500000000512100000000000015127 0ustar00gillesgilles00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library # Copyright © 2012-2016 Kozea # # This library is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . import os from setuptools import find_packages, setup ROOT = os.path.dirname(__file__) tests_requirements = [ "cairosvg", "coveralls", "lxml", "pyquery", "pytest", "pytest-cov", "ruff>=0.5.6", ] moulinrouge_requirements = [ "flask", "pygal_maps_ch", "pygal_maps_fr", "pygal_maps_world", ] 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__'], long_description=open('README').read(), long_description_content_type="text/x-rst", url=about['__uri__'], author=about['__author__'], author_email=about['__email__'], license=about['__license__'], platforms="Any", python_requires=">=3.8", packages=find_packages(), provides=['pygal'], scripts=["pygal_gen.py"], keywords=[ "svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"], setup_requires=['pytest-runner'], install_requires=['importlib-metadata'], # TODO: remove this (see #545, #546) package_data={'pygal': ['css/*', 'graph/maps/*.svg']}, extras_require={ 'lxml': ['lxml'], 'docs': ['sphinx', 'sphinx_rtd_theme', 'pygal_sphinx_directives'], 'png': ['cairosvg'], 'test': tests_requirements, 'moulinrouge': moulinrouge_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 :: 3", "Topic :: Multimedia :: Graphics :: Presentation"])