diff --git a/CHANGELOG b/CHANGELOG index 13d6fd4..6e6a46f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ V 1.5.0 UNRELEASED Add per serie configuration Add half pie (thanks philt2001) Add render_table (WIP) + Make lxml an optionnal dependency V 1.4.6 Add support for \n separated multiline titles (thanks sirlark) diff --git a/perf.py b/perf.py index bce22d6..9dbf163 100644 --- a/perf.py +++ b/perf.py @@ -18,7 +18,7 @@ # along with pygal. If not, see . -from pygal import CHARTS_NAMES, CHARTS_BY_NAME +from pygal import CHARTS, CHARTS_BY_NAME from pygal.test import adapt from random import sample @@ -82,7 +82,7 @@ if '--mem' in sys.argv: sys.exit(0) -charts = CHARTS_NAMES if '--all' in sys.argv else 'Line', +charts = CHARTS if '--all' in sys.argv else 'Line', for chart in charts: prt('%s\n' % chart) diff --git a/pygal/_compat.py b/pygal/_compat.py index 37c4720..f251f43 100644 --- a/pygal/_compat.py +++ b/pygal/_compat.py @@ -16,9 +16,20 @@ # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . +import os import sys from collections import Iterable +try: + if os.getenv('NO_LXML', None): + raise ImportError('Explicit lxml bypass') + from lxml import etree + etree.lxml = True +except ImportError: + from xml.etree import ElementTree as etree + etree.lxml = False + + if sys.version_info[0] == 3: base = (str, bytes) coerce = str @@ -41,6 +52,12 @@ def to_str(string): return string +def to_unicode(string): + if not isinstance(string, coerce): + return string.decode('utf-8') + return string + + def u(s): if sys.version_info[0] == 2: return s.decode('utf-8') diff --git a/pygal/ghost.py b/pygal/ghost.py index 0a6b0b2..4350748 100644 --- a/pygal/ghost.py +++ b/pygal/ghost.py @@ -123,7 +123,7 @@ class Ghost(object): def render_pyquery(self): """Render the graph, and return a pyquery wrapped tree""" from pyquery import PyQuery as pq - return pq(self.render_tree()) + return pq(self.render(), parser='html') def render_in_browser(self): """Render the graph, open it in your browser with black magic""" diff --git a/pygal/graph/base.py b/pygal/graph/base.py index b8fedb8..a2a0020 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -293,7 +293,7 @@ class BaseGraph(object): is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): - """Render the graph, and return lxml tree""" + """Render the graph, and return (l)xml etree""" svg = self.svg.root for f in self.xml_filters: svg = f(svg) diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py index 951c86d..91fd1d4 100644 --- a/pygal/graph/frenchmap.py +++ b/pygal/graph/frenchmap.py @@ -26,9 +26,8 @@ from collections import defaultdict from pygal.ghost import ChartCollection from pygal.util import cut, cached_property, decorate from pygal.graph.graph import Graph -from pygal._compat import u +from pygal._compat import u, etree from numbers import Number -from lxml import etree import os @@ -190,6 +189,7 @@ class FrenchMapDepartments(Graph): x_labels = list(DEPARTMENTS.keys()) area_names = DEPARTMENTS area_prefix = 'z' + kind = 'departement' svg_map = DPT_MAP @@ -222,9 +222,10 @@ class FrenchMapDepartments(Graph): ratio = 1 else: ratio = .3 + .7 * (value - min_) / (max_ - min_) - areae = map.xpath( - "//*[contains(concat(' ', normalize-space(@class), ' ')," - " ' %s%s ')]" % (self.area_prefix, area_code)) + areae = map.findall( + ".//*[@class='%s%s %s map-element']" % ( + self.area_prefix, area_code, + self.kind)) if not areae: continue @@ -236,14 +237,16 @@ class FrenchMapDepartments(Graph): metadata = serie.metadata.get(j) if metadata: - parent = area.getparent() node = decorate(self.svg, area, metadata) if node != area: area.remove(node) - index = parent.index(area) - parent.remove(area) - node.append(area) - parent.insert(index, 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) last_node = len(area) > 0 and area[-1] if last_node is not None and last_node.tag == 'title': @@ -265,6 +268,7 @@ class FrenchMapRegions(FrenchMapDepartments): area_names = REGIONS area_prefix = 'a' svg_map = REG_MAP + kind = 'region' class FrenchMap(ChartCollection): diff --git a/pygal/graph/supranationalworldmap.py b/pygal/graph/supranationalworldmap.py index 4cc9a16..a21be10 100644 --- a/pygal/graph/supranationalworldmap.py +++ b/pygal/graph/supranationalworldmap.py @@ -25,7 +25,7 @@ from __future__ import division from pygal.graph.worldmap import Worldmap from pygal.i18n import SUPRANATIONAL from pygal.util import cut, decorate -from lxml import etree +from pygal._compat import etree import os with open(os.path.join( @@ -68,14 +68,13 @@ class SupranationalWorldmap(Worldmap): metadata = serie.metadata.get(j) if metadata: - parent = country.getparent() node = decorate(self.svg, country, metadata) if node != country: country.remove(node) - index = parent.index(country) - parent.remove(country) + index = list(map).index(country) + map.remove(country) node.append(country) - parent.insert(index, node) + map.insert(index, node) last_node = len(country) > 0 and country[-1] if last_node is not None and last_node.tag == 'title': diff --git a/pygal/graph/worldmap.py b/pygal/graph/worldmap.py index 8e144d6..cd9fd73 100644 --- a/pygal/graph/worldmap.py +++ b/pygal/graph/worldmap.py @@ -25,7 +25,7 @@ from __future__ import division from pygal.util import cut, cached_property, decorate from pygal.graph.graph import Graph from pygal.i18n import COUNTRIES -from lxml import etree +from pygal._compat import etree import os with open(os.path.join( @@ -86,14 +86,13 @@ class Worldmap(Graph): metadata = serie.metadata.get(j) if metadata: - parent = country.getparent() node = decorate(self.svg, country, metadata) if node != country: country.remove(node) - index = parent.index(country) - parent.remove(country) + index = list(map).index(country) + map.remove(country) node.append(country) - parent.insert(index, node) + map.insert(index, node) last_node = len(country) > 0 and country[-1] if last_node is not None and last_node.tag == 'title': diff --git a/pygal/svg.py b/pygal/svg.py index f09bbff..7c5f938 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -22,13 +22,12 @@ Svg helper """ from __future__ import division -from pygal._compat import to_str, u +from pygal._compat import to_str, u, etree import io import os import json from datetime import date, datetime from numbers import Number -from lxml import etree from math import cos, sin, pi from pygal.util import template, coord_format, minify_css from pygal import __version__ @@ -37,6 +36,7 @@ from pygal import __version__ class Svg(object): """Svg object""" ns = 'http://www.w3.org/2000/svg' + xlink_ns = 'http://www.w3.org/1999/xlink' def __init__(self, graph): self.graph = graph @@ -45,19 +45,30 @@ class Svg(object): else: self.id = '' self.processing_instructions = [ - etree.PI(u('xml'), u("version='1.0' encoding='utf-8'"))] - self.root = etree.Element( - "{%s}svg" % self.ns, - nsmap={ - None: self.ns, - 'xlink': 'http://www.w3.org/1999/xlink', - }) + etree.ProcessingInstruction( + u('xml'), u("version='1.0' encoding='utf-8'"))] + if etree.lxml: + attrs = { + 'nsmap': { + None: self.ns, + 'xlink': self.xlink_ns + } + } + else: + attrs = { + 'xmlns': self.ns + } + etree.register_namespace('xlink', self.xlink_ns) + + self.root = etree.Element('svg', **attrs) self.root.attrib['id'] = self.id.lstrip('#').rstrip() self.root.attrib['class'] = 'pygal-chart' self.root.append( etree.Comment(u( - 'Generated with pygal %s ©Kozea 2011-2014 on %s' % ( - __version__, date.today().isoformat())))) + 'Generated with pygal %s (%s) ©Kozea 2011-2014 on %s' % ( + __version__, + 'lxml' if etree.lxml else 'etree', + date.today().isoformat())))) self.root.append(etree.Comment(u('http://pygal.org'))) self.root.append(etree.Comment(u('http://github.com/Kozea/pygal'))) self.defs = self.node(tag='defs') @@ -227,14 +238,17 @@ class Svg(object): """Last thing to do before rendering""" for f in self.graph.xml_filters: self.root = f(self.root) + args = { + 'encoding': 'utf-8' + } + if etree.lxml: + args['pretty_print'] = pretty_print svg = etree.tostring( - self.root, pretty_print=pretty_print, - xml_declaration=False, - encoding='utf-8') + self.root, **args) if not self.graph.disable_xml_declaration: svg = b'\n'.join( [etree.tostring( - pi, encoding='utf-8', pretty_print=pretty_print) + pi, **args) for pi in self.processing_instructions] ) + b'\n' + svg if self.graph.disable_xml_declaration or is_unicode: diff --git a/pygal/test/__init__.py b/pygal/test/__init__.py index da94e4a..d3471ae 100644 --- a/pygal/test/__init__.py +++ b/pygal/test/__init__.py @@ -22,6 +22,7 @@ from pygal.util import cut from datetime import datetime from pygal.i18n import COUNTRIES from pygal.graph.frenchmap import DEPARTMENTS, REGIONS +from pygal._compat import etree def get_data(i): diff --git a/pygal/test/test_xml_filters.py b/pygal/test/test_xml_filters.py index fe1207a..179749a 100644 --- a/pygal/test/test_xml_filters.py +++ b/pygal/test/test_xml_filters.py @@ -18,19 +18,22 @@ # along with pygal. If not, see . from pygal import Bar + class ChangeBarsXMLFilter(object): def __init__(self, a, b): self.data = [b[i] - a[i] for i in range(len(a))] def __call__(self, T): - subplot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=150) + subplot = Bar(legend_at_bottom=True, explicit_size=True, + width=800, height=150) subplot.add("Difference", self.data) subplot = subplot.render_tree() - subplot = subplot.xpath("g")[0] + subplot = subplot.findall("g")[0] T.insert(2, subplot) - T.xpath("g")[1].set('transform', 'translate(0,150), scale(1,0.75)') + T.findall("g")[1].set('transform', 'translate(0,150), scale(1,0.75)') return T + def test_xml_filters_round_trip(): plot = Bar() plot.add("A", [60, 75, 80, 78, 83, 90]) @@ -40,13 +43,16 @@ def test_xml_filters_round_trip(): after = plot.render() assert before == after + def test_xml_filters_change_bars(): - plot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=600) + 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)) + plot.add_xml_filter(ChangeBarsXMLFilter(A, B)) q = plot.render_tree() - assert len(q.xpath("g")) == 2 - assert q.xpath("g")[1].attrib["transform"] == "translate(0,150), scale(1,0.75)" + assert len(q.findall("g")) == 2 + assert q.findall("g")[1].attrib[ + "transform"] == "translate(0,150), scale(1,0.75)" diff --git a/pygal/util.py b/pygal/util.py index 4607cde..9159386 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -21,7 +21,7 @@ Various utils """ from __future__ import division -from pygal._compat import to_str, u, is_list_like +from pygal._compat import u, is_list_like, to_unicode import re from decimal import Decimal from math import floor, pi, log, log10, ceil @@ -241,7 +241,7 @@ def decorate(svg, node, metadata): if key == 'xlink' and isinstance(value, dict): value = value.get('href', value) if value: - svg.node(node, 'desc', class_=key).text = to_str(value) + svg.node(node, 'desc', class_=key).text = to_unicode(value) return node @@ -328,7 +328,7 @@ def prepare_values(raw, config, cls): from pygal.graph.worldmap import Worldmap from pygal.graph.frenchmap import FrenchMapDepartments if config.x_labels is None and hasattr(cls, 'x_labels'): - config.x_labels = cls.x_labels + config.x_labels = list(map(to_unicode, cls.x_labels)) if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)): config.zero = 1 diff --git a/setup.py b/setup.py index 83ffa2d..a46df48 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ class PyTest(TestCommand): self.test_suite = True def run_tests(self): - #import here, cause outside the eggs aren't loaded + # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.test_args) sys.exit(errno) @@ -65,7 +65,11 @@ setup( tests_require=["pytest", "pyquery", "flask", "cairosvg"], cmdclass={'test': PyTest}, package_data={'pygal': ['css/*', 'graph/*.svg']}, - install_requires=['lxml'], + extra_requires={ + 'lxml': ['lxml'], + 'png': ['cairosvg'] + + }, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console",