mirror of https://github.com/Kozea/pygal.git
Python to generate nice looking SVG graph
http://pygal.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
8.3 KiB
217 lines
8.3 KiB
# -*- coding: utf-8 -*- |
|
# This file is part of pygal |
|
# |
|
# A python svg graph plotting library |
|
# Copyright © 2012 Kozea |
|
# |
|
# This library is free software: you can redistribute it and/or modify it under |
|
# the terms of the GNU Lesser General Public License as published by the Free |
|
# Software Foundation, either version 3 of the License, or (at your option) any |
|
# later version. |
|
# |
|
# This library is distributed in the hope that it will be useful, but WITHOUT |
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
# details. |
|
# |
|
# You should have received a copy of the GNU Lesser General Public License |
|
# along with pygal. If not, see <http://www.gnu.org/licenses/>. |
|
""" |
|
Svg helper |
|
|
|
""" |
|
|
|
from __future__ import division |
|
import io |
|
import os |
|
import json |
|
from datetime import date |
|
from numbers import Number |
|
from lxml import etree |
|
from math import cos, sin, pi |
|
from urlparse import urlparse |
|
from pygal.util import template, coord_format, minify_css |
|
from pygal import __version__ |
|
|
|
|
|
class Svg(object): |
|
"""Svg object""" |
|
ns = 'http://www.w3.org/2000/svg' |
|
|
|
def __init__(self, graph): |
|
self.graph = graph |
|
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', |
|
}) |
|
self.root.append( |
|
etree.Comment(u'Generated with pygal %s ©Kozea 2012 on %s' % ( |
|
__version__, 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') |
|
|
|
def add_styles(self): |
|
"""Add the css to the svg""" |
|
for css in ['base.css'] + list(self.graph.css): |
|
if urlparse(css).scheme: |
|
self.processing_instructions.append( |
|
etree.PI( |
|
u'xml-stylesheet', u'href="%s"' % css)) |
|
else: |
|
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, |
|
font_sizes=self.graph.font_sizes()) |
|
if not self.graph.pretty_print: |
|
css_text = minify_css(css_text) |
|
self.node( |
|
self.defs, 'style', type='text/css').text = css_text |
|
|
|
def add_scripts(self): |
|
"""Add the js to the svg""" |
|
common_script = self.node(self.defs, 'script', type='text/javascript') |
|
common_script.text = " = ".join( |
|
("window.config", json.dumps(self.graph.config.to_dict()))) |
|
|
|
for js in self.graph.js: |
|
if urlparse(js).scheme: |
|
self.node( |
|
self.defs, 'script', type='text/javascript', href=js) |
|
else: |
|
script = self.node(self.defs, 'script', type='text/javascript') |
|
with io.open(js, encoding='utf-8') as f: |
|
script.text = f.read() |
|
|
|
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 attrib.items(): |
|
if value is None: |
|
del attrib[key] |
|
elif not isinstance(value, basestring): |
|
attrib[key] = str(value) |
|
elif key.endswith('_'): |
|
attrib[key.rstrip('_')] = attrib[key] |
|
del attrib[key] |
|
elif key == 'href': |
|
attrib['{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')): |
|
attr1 = extras.get(key1, None) |
|
attr2 = extras.get(key2, None) |
|
extras[key1], extras[key2] = attr2, attr1 |
|
return self.node(parent, tag, attrib, **extras) |
|
|
|
def line(self, node, coords, close=False, **kwargs): |
|
"""Draw a svg line""" |
|
line_len = len(coords) |
|
if line_len < 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 |
|
origin = coord_format(coords[origin_index]) |
|
line = ' '.join([coord_format(c) |
|
for c in coords[origin_index + 1:] |
|
if None not in c]) |
|
self.node(node, 'path', |
|
d=root % (origin, line), **kwargs) |
|
|
|
def slice( |
|
self, serie_node, node, radius, small_radius, |
|
angle, start_angle, center, val): |
|
"""Draw a pie slice""" |
|
project = lambda rho, alpha: ( |
|
rho * sin(-alpha), rho * cos(-alpha)) |
|
diff = lambda x, y: (x[0] - y[0], x[1] - y[1]) |
|
fmt = lambda x: '%f %f' % x |
|
get_radius = lambda r: fmt(tuple([r] * 2)) |
|
absolute_project = lambda rho, theta: fmt( |
|
diff(center, project(rho, theta))) |
|
|
|
if angle == 2 * pi: |
|
self.node(node, 'circle', |
|
cx=center[0], |
|
cy=center[1], |
|
r=radius, |
|
class_='slice reactive tooltip-trigger') |
|
else: |
|
to = [absolute_project(radius, start_angle), |
|
absolute_project(radius, start_angle + angle), |
|
absolute_project(small_radius, start_angle + angle), |
|
absolute_project(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], |
|
get_radius(radius), int(angle > pi), to[1], |
|
to[2], |
|
get_radius(small_radius), int(angle > pi), to[3]), |
|
class_='slice reactive tooltip-trigger') |
|
x, y = diff(center, project( |
|
(radius + small_radius) / 2, start_angle + angle / 2)) |
|
|
|
self.graph._tooltip_data(node, val, x, y, classes="centered") |
|
self.graph._static_value(serie_node, val, x, y) |
|
|
|
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): |
|
no_data = self.node(self.root, 'text', |
|
x=self.graph.width / 2, |
|
y=self.graph.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""" |
|
svg = etree.tostring( |
|
self.root, pretty_print=pretty_print, |
|
xml_declaration=False, |
|
encoding='utf-8') |
|
if not self.graph.disable_xml_declaration: |
|
svg = b'\n'.join( |
|
[etree.tostring( |
|
pi, encoding='utf-8', pretty_print=pretty_print) |
|
for pi in self.processing_instructions] |
|
) + b'\n' + svg |
|
if self.graph.disable_xml_declaration or is_unicode: |
|
svg = svg.decode('utf-8') |
|
return svg
|
|
|