210 lines
7.2 KiB
Python
210 lines
7.2 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# This file is part of pygal
|
||
|
#
|
||
|
# A python svg graph plotting library
|
||
|
# Copyright © 2012-2016 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 math import cos, pi
|
||
|
|
||
|
from pygal._compat import is_str
|
||
|
from pygal.adapters import none_to_zero, positive
|
||
|
from pygal.graph.line import Line
|
||
|
from pygal.util import cached_property, compute_scale, cut, deg, truncate
|
||
|
from pygal.view import PolarLogView, PolarView
|
||
|
|
||
|
|
||
|
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
|
||
|
|
||
|
@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 or not self.show_x_labels:
|
||
|
return
|
||
|
|
||
|
axis = self.svg.node(self.nodes['plot'], class_="axis x web%s" % (
|
||
|
' always_show' if self.show_x_guides else ''
|
||
|
))
|
||
|
format_ = lambda x: '%f %f' % x
|
||
|
center = self.view((0, 0))
|
||
|
r = self._rmax
|
||
|
|
||
|
# Can't simply determine truncation
|
||
|
truncation = self.truncate_label or 25
|
||
|
|
||
|
for label, theta in self._x_labels:
|
||
|
major = label in self._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_='%s%sline' % (
|
||
|
'axis ' if label == "0" else '',
|
||
|
'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 = truncate(label, truncation)
|
||
|
if text.text != label:
|
||
|
self.svg.node(guides, 'title').text = label
|
||
|
else:
|
||
|
self.svg.node(
|
||
|
guides, 'title',
|
||
|
).text = self._x_format(theta)
|
||
|
|
||
|
angle = - theta + pi / 2
|
||
|
if cos(angle) < 0:
|
||
|
angle -= pi
|
||
|
text.attrib['transform'] = 'rotate(%f %s)' % (
|
||
|
self.x_label_rotation or deg(angle), format_(pos_text))
|
||
|
|
||
|
def _y_axis(self, draw_axes=True):
|
||
|
"""Override y axis to make it polar"""
|
||
|
if not self._y_labels or not self.show_y_labels:
|
||
|
return
|
||
|
|
||
|
axis = self.svg.node(self.nodes['plot'], class_="axis y web")
|
||
|
|
||
|
for label, r in reversed(self._y_labels):
|
||
|
major = r in self._y_labels_major
|
||
|
if not (self.show_minor_y_labels or major):
|
||
|
continue
|
||
|
guides = self.svg.node(axis, class_='%sguides' % (
|
||
|
'logarithmic ' if self.logarithmic else ''
|
||
|
))
|
||
|
if self.show_y_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]))
|
||
|
x -= 5
|
||
|
text = self.svg.node(
|
||
|
guides, 'text',
|
||
|
x=x,
|
||
|
y=y,
|
||
|
class_='major' if major else ''
|
||
|
)
|
||
|
text.text = label
|
||
|
|
||
|
if self.y_label_rotation:
|
||
|
text.attrib['transform'] = "rotate(%d %f %f)" % (
|
||
|
self.y_label_rotation, x, y)
|
||
|
|
||
|
self.svg.node(
|
||
|
guides, 'title',
|
||
|
).text = self._y_format(r)
|
||
|
|
||
|
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 = self._adapt(y_label.get('value'))
|
||
|
title = y_label.get('label', self._y_format(pos))
|
||
|
elif is_str(y_label):
|
||
|
pos = self._adapt(y_pos[i])
|
||
|
title = y_label
|
||
|
else:
|
||
|
pos = self._adapt(y_label)
|
||
|
title = self._y_format(pos)
|
||
|
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._y_format, y_pos), y_pos))
|