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",