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.
 
 
 

216 lines
7.9 KiB

# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2015 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/>.
"""
Radar chart: As known as kiviat chart or spider chart is a polar line chart
useful for multivariate observation.
"""
from __future__ import division
from pygal.graph.line import Line
from pygal.adapters import positive, none_to_zero
from pygal.view import PolarView, PolarLogView
from pygal.util import deg, cached_property, compute_scale, majorize, cut
from pygal._compat import is_str
from math import cos, pi
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
def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
return self._format(values[i][0])
@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:
return
axis = self.svg.node(self.nodes['plot'], class_="axis x web")
format_ = lambda x: '%f %f' % x
center = self.view((0, 0))
r = self._rmax
if self.x_labels_major:
x_labels_major = self.x_labels_major
elif self.x_labels_major_every:
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):
x_labels_major = [label[0] for label in self._x_labels]
else:
x_labels_major = [self._x_labels[
int(i * label_count / major_count)][0]
for i in range(major_count)]
else:
x_labels_major = []
for label, theta in self._x_labels:
major = label in 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_='%sline' % ('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 = label
angle = - theta + pi / 2
if cos(angle) < 0:
angle -= pi
text.attrib['transform'] = 'rotate(%f %s)' % (
deg(angle), format_(pos_text))
def _y_axis(self, draw_axes=True):
"""Override y axis to make it polar"""
if not self._y_labels:
return
axis = self.svg.node(self.nodes['plot'], class_="axis y web")
if self.y_labels_major:
y_labels_major = self.y_labels_major
elif self.y_labels_major_every:
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):
y_labels_major = [label[1] for label in self._y_labels]
else:
y_labels_major = [self._y_labels[
int(i * (label_count - 1) / (major_count - 1))][1]
for i in range(major_count)]
else:
y_labels_major = majorize(
cut(self._y_labels, 1)
)
for label, r in reversed(self._y_labels):
major = r in y_labels_major
if not (self.show_minor_y_labels or major):
continue
guides = self.svg.node(axis, class_='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]))
self.svg.node(
guides, 'text',
x=x - 5,
y=y,
class_='major' if major else ''
).text = label
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 = float(y_label.get('value'))
title = y_label.get('label', self._format(pos))
elif is_str(y_label):
pos = y_pos[i]
title = y_label
else:
pos = float(y_label)
title = self._format(y_label)
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._format, y_pos), y_pos))