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.
 
 
 

353 lines
10 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/>.
"""
Various utils
"""
from __future__ import division
import re
from decimal import Decimal
from math import floor, pi, log, log10, ceil
from itertools import cycle
from pygal.adapters import not_zero, positive
ORDERS = u"yzafpnµm kMGTPEZY"
def float_format(number):
"""Format a float to a precision of 3, without zeroes or dots"""
return ("%.3f" % number).rstrip('0').rstrip('.')
def humanize(number):
"""Format a number to engineer scale"""
order = number and int(floor(log(abs(number)) / log(1000)))
human_readable = ORDERS.split(" ")[int(order > 0)]
if order == 0 or order > len(human_readable):
return float_format(number / (1000 ** int(order)))
return (
float_format(number / (1000 ** int(order))) +
human_readable[int(order) - int(order > 0)])
def is_major(number):
"""Returns True if number is a round order: 1, 100, 0.001"""
return not number or 10 ** floor(log10(abs(number))) == abs(number)
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 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)
def coord_format(xy):
"""Format x y coords to svg"""
return '%f %f' % xy
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=False, order_min=None,
min_scale=4, max_scale=20, force_steps=None):
"""Compute an optimal scale between min and max"""
if min_ == 0 and max_ == 0:
return [0]
if max_ - min_ == 0:
return [min_]
if force_steps:
# TODO: handle logarithmic scale
step = float(max_ - min_) / (force_steps - 1)
curr = min_
ret = []
for i in range(force_steps):
ret.append(curr)
curr += step
return ret
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)
for key, value in metadata.items():
if key == 'xlink' and isinstance(value, dict):
value = value.get('href', value)
if value:
if isinstance(value, unicode):
svg.node(node, 'desc', class_=key).text = value
else:
svg.node(node, 'desc', class_=key).text = str(value)
return node
def cycle_fill(short_list, max_len):
"""Fill a list to max_len using a cycle of it"""
short_list = list(short_list)
list_cycle = cycle(short_list)
while len(short_list) < max_len:
short_list.append(list_cycle.next())
return short_list
def truncate(string, index):
"""Truncate a string at index and add ..."""
if len(string) > index and index > 0:
string = string[:index - 1] + u''
return string
# Stolen from brownie http://packages.python.org/Brownie/
class cached_property(object):
"""Optimize a static property"""
def __init__(self, getter, doc=None):
self.getter = getter
self.__module__ = getter.__module__
self.__name__ = getter.__name__
self.__doc__ = doc or getter.__doc__
def __get__(self, obj, type_=None):
if obj is None:
return self
value = obj.__dict__[self.__name__] = self.getter(obj)
return value
css_comments = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL)
def minify_css(css):
# Inspired by slimmer by Peter Bengtsson
remove_next_comment = 1
for css_comment in css_comments.findall(css):
if css_comment[-3:] == '\*/':
remove_next_comment = 0
continue
if remove_next_comment:
css = css.replace(css_comment, '')
else:
remove_next_comment = 1
# >= 2 whitespace becomes one whitespace
css = re.sub(r'\s\s+', ' ', css)
# no whitespace before end of line
css = re.sub(r'\s+\n', '', css)
# Remove space before and after certain chars
for char in ('{', '}', ':', ';', ','):
css = re.sub(char + r'\s', char, css)
css = re.sub(r'\s' + char, char, css)
css = re.sub(r'}\s(#|\w)', r'}\1', css)
# no need for the ; before end of attributes
css = re.sub(r';}', r'}', css)
css = re.sub(r'}//-->', r'}\n//-->', css)
return css.strip()
def compose(f, g):
"""Chain functions"""
fun = lambda *args, **kwargs: f(g(*args, **kwargs))
fun.__name__ = "%s o %s" % (f.__name__, g.__name__)
return fun
def safe_enumerate(iterable):
for i, v in enumerate(iterable):
if v is not None:
yield i, v
from pygal.serie import Serie
def prepare_values(raw, config, cls):
"""Prepare the values to start with sane values"""
if not raw:
return
adapters = list(cls._adapters) or [lambda x:x]
if config.logarithmic:
for fun in not_zero, positive:
if fun in adapters:
adapters.remove(fun)
adapters = adapters + [positive, not_zero]
adapter = reduce(compose, adapters) if not config.strict else ident
series = []
width = max([len(values) for _, values in raw] +
[len(config.x_labels or [])])
for title, raw_values in raw:
metadata = {}
values = []
if isinstance(raw_values, dict):
value_list = [None] * width
for k, v in raw_values.items():
if k in config.x_labels:
value_list[config.x_labels.index(k)] = v
raw_values = value_list
else:
raw_values = list(raw_values)
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):
value = raw_value.pop('value')
metadata[index] = raw_value
else:
value = raw_value
if cls.__name__ == 'XY':
if not hasattr(value, '__iter__'):
value = (value, config.zero)
value = map(adapter, value)
else:
value = adapter(value)
values.append(value)
series.append(Serie(title, values, metadata))
return series