12cf2e8247
Add IsEvadedOffense to EFPenalty Fix remote log reading in not Windows
946 lines
36 KiB
Python
946 lines
36 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/>.
|
|
"""Chart properties and drawing"""
|
|
|
|
from __future__ import division
|
|
|
|
from math import ceil, cos, sin, sqrt
|
|
|
|
from pygal import stats
|
|
from pygal._compat import is_list_like, is_str, to_str
|
|
from pygal.graph.public import PublicApi
|
|
from pygal.interpolate import INTERPOLATIONS
|
|
from pygal.util import (
|
|
cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box,
|
|
get_texts_box, majorize, rad, reverse_text_len, split_title, truncate)
|
|
from pygal.view import LogView, ReverseView, View, XYLogView
|
|
|
|
|
|
class Graph(PublicApi):
|
|
|
|
"""Graph super class containing generic common functions"""
|
|
|
|
_dual = False
|
|
|
|
def _decorate(self):
|
|
"""Draw all decorations"""
|
|
self._set_view()
|
|
self._make_graph()
|
|
self._axes()
|
|
self._legend()
|
|
self._make_title()
|
|
self._make_x_title()
|
|
self._make_y_title()
|
|
|
|
def _axes(self):
|
|
"""Draw axes"""
|
|
self._y_axis()
|
|
self._x_axis()
|
|
|
|
def _set_view(self):
|
|
"""Assign a view to current graph"""
|
|
if self.logarithmic:
|
|
if self._dual:
|
|
view_class = XYLogView
|
|
else:
|
|
view_class = LogView
|
|
else:
|
|
view_class = ReverseView if self.inverse_y_axis else View
|
|
|
|
self.view = view_class(
|
|
self.width - self.margin_box.x,
|
|
self.height - self.margin_box.y,
|
|
self._box)
|
|
|
|
def _make_graph(self):
|
|
"""Init common graph svg structure"""
|
|
self.nodes['graph'] = self.svg.node(
|
|
class_='graph %s-graph %s' % (
|
|
self.__class__.__name__.lower(),
|
|
'horizontal' if self.horizontal else 'vertical'))
|
|
self.svg.node(self.nodes['graph'], 'rect',
|
|
class_='background',
|
|
x=0, y=0,
|
|
width=self.width,
|
|
height=self.height)
|
|
self.nodes['plot'] = self.svg.node(
|
|
self.nodes['graph'], class_="plot",
|
|
transform="translate(%d, %d)" % (
|
|
self.margin_box.left, self.margin_box.top))
|
|
self.svg.node(self.nodes['plot'], 'rect',
|
|
class_='background',
|
|
x=0, y=0,
|
|
width=self.view.width,
|
|
height=self.view.height)
|
|
self.nodes['title'] = self.svg.node(
|
|
self.nodes['graph'],
|
|
class_="titles")
|
|
self.nodes['overlay'] = self.svg.node(
|
|
self.nodes['graph'], class_="plot overlay",
|
|
transform="translate(%d, %d)" % (
|
|
self.margin_box.left, self.margin_box.top))
|
|
self.nodes['text_overlay'] = self.svg.node(
|
|
self.nodes['graph'], class_="plot text-overlay",
|
|
transform="translate(%d, %d)" % (
|
|
self.margin_box.left, self.margin_box.top))
|
|
self.nodes['tooltip_overlay'] = self.svg.node(
|
|
self.nodes['graph'], class_="plot tooltip-overlay",
|
|
transform="translate(%d, %d)" % (
|
|
self.margin_box.left, self.margin_box.top))
|
|
self.nodes['tooltip'] = self.svg.node(
|
|
self.nodes['tooltip_overlay'],
|
|
transform='translate(0 0)',
|
|
style="opacity: 0",
|
|
**{'class': 'tooltip'})
|
|
|
|
self.svg.node(self.nodes['tooltip'], 'rect',
|
|
rx=self.tooltip_border_radius,
|
|
ry=self.tooltip_border_radius,
|
|
width=0, height=0,
|
|
**{'class': 'tooltip-box'})
|
|
self.svg.node(self.nodes['tooltip'], 'g', class_='text')
|
|
|
|
def _x_axis(self):
|
|
"""Make the x axis: labels and guides"""
|
|
if not self._x_labels or not self.show_x_labels:
|
|
return
|
|
axis = self.svg.node(self.nodes['plot'], class_="axis x%s" % (
|
|
' always_show' if self.show_x_guides else ''
|
|
))
|
|
truncation = self.truncate_label
|
|
if not truncation:
|
|
if self.x_label_rotation or len(self._x_labels) <= 1:
|
|
truncation = 25
|
|
else:
|
|
first_label_position = self.view.x(self._x_labels[0][1]) or 0
|
|
last_label_position = self.view.x(self._x_labels[-1][1]) or 0
|
|
available_space = (
|
|
last_label_position - first_label_position) / (
|
|
len(self._x_labels) - 1)
|
|
truncation = reverse_text_len(
|
|
available_space, self.style.label_font_size)
|
|
truncation = max(truncation, 1)
|
|
|
|
lastlabel = self._x_labels[-1][0]
|
|
if 0 not in [label[1] for label in self._x_labels]:
|
|
self.svg.node(axis, 'path',
|
|
d='M%f %f v%f' % (0, 0, self.view.height),
|
|
class_='line')
|
|
lastlabel = None
|
|
|
|
for label, position in self._x_labels:
|
|
if self.horizontal:
|
|
major = position in self._x_labels_major
|
|
else:
|
|
major = label in self._x_labels_major
|
|
if not (self.show_minor_x_labels or major):
|
|
continue
|
|
guides = self.svg.node(axis, class_='guides')
|
|
x = self.view.x(position)
|
|
if x is None:
|
|
continue
|
|
y = self.view.height + 5
|
|
last_guide = (self._y_2nd_labels and label == lastlabel)
|
|
self.svg.node(
|
|
guides, 'path',
|
|
d='M%f %f v%f' % (x or 0, 0, self.view.height),
|
|
class_='%s%s%sline' % (
|
|
'axis ' if label == "0" else '',
|
|
'major ' if major else '',
|
|
'guide ' if position != 0 and not last_guide else ''))
|
|
y += .5 * self.style.label_font_size + 5
|
|
text = self.svg.node(
|
|
guides, 'text',
|
|
x=x,
|
|
y=y,
|
|
class_='major' if major else ''
|
|
)
|
|
|
|
text.text = truncate(label, truncation)
|
|
if text.text != label:
|
|
self.svg.node(guides, 'title').text = label
|
|
elif self._dual:
|
|
self.svg.node(
|
|
guides, 'title',
|
|
).text = self._x_format(position)
|
|
|
|
if self.x_label_rotation:
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % (
|
|
self.x_label_rotation, x, y)
|
|
if self.x_label_rotation >= 180:
|
|
text.attrib['class'] = ' '.join(
|
|
(text.attrib['class'] and text.attrib['class'].split(
|
|
' ') or []) + ['backwards'])
|
|
|
|
if self._y_2nd_labels and 0 not in [
|
|
label[1] for label in self._x_labels]:
|
|
self.svg.node(axis, 'path',
|
|
d='M%f %f v%f' % (
|
|
self.view.width, 0, self.view.height),
|
|
class_='line')
|
|
|
|
if self._x_2nd_labels:
|
|
secondary_ax = self.svg.node(
|
|
self.nodes['plot'], class_="axis x x2%s" % (
|
|
' always_show' if self.show_x_guides else ''
|
|
))
|
|
for label, position in self._x_2nd_labels:
|
|
major = label in self._x_labels_major
|
|
if not (self.show_minor_x_labels or major):
|
|
continue
|
|
# it is needed, to have the same structure as primary axis
|
|
guides = self.svg.node(secondary_ax, class_='guides')
|
|
x = self.view.x(position)
|
|
y = -5
|
|
text = self.svg.node(
|
|
guides, 'text',
|
|
x=x,
|
|
y=y,
|
|
class_='major' if major else ''
|
|
)
|
|
text.text = label
|
|
if self.x_label_rotation:
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % (
|
|
-self.x_label_rotation, x, y)
|
|
if self.x_label_rotation >= 180:
|
|
text.attrib['class'] = ' '.join((
|
|
text.attrib['class'] and
|
|
text.attrib['class'].split(
|
|
' ') or []) + ['backwards'])
|
|
|
|
def _y_axis(self):
|
|
"""Make the y axis: labels and guides"""
|
|
if not self._y_labels or not self.show_y_labels:
|
|
return
|
|
|
|
axis = self.svg.node(self.nodes['plot'], class_="axis y%s" % (
|
|
' always_show' if self.show_y_guides else ''
|
|
))
|
|
|
|
if (0 not in [label[1] for label in self._y_labels] and
|
|
self.show_y_guides):
|
|
self.svg.node(
|
|
axis, 'path',
|
|
d='M%f %f h%f' % (
|
|
0, 0 if self.inverse_y_axis else self.view.height,
|
|
self.view.width),
|
|
class_='line'
|
|
)
|
|
|
|
for label, position in self._y_labels:
|
|
if self.horizontal:
|
|
major = label in self._y_labels_major
|
|
else:
|
|
major = position 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 ''
|
|
))
|
|
x = -5
|
|
y = self.view.y(position)
|
|
if not y:
|
|
continue
|
|
if self.show_y_guides:
|
|
self.svg.node(
|
|
guides, 'path',
|
|
d='M%f %f h%f' % (0, y, self.view.width),
|
|
class_='%s%s%sline' % (
|
|
'axis ' if label == "0" else '',
|
|
'major ' if major else '',
|
|
'guide ' if position != 0 else ''))
|
|
text = self.svg.node(
|
|
guides, 'text',
|
|
x=x,
|
|
y=y + .35 * self.style.label_font_size,
|
|
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)
|
|
if 90 < self.y_label_rotation < 270:
|
|
text.attrib['class'] = ' '.join(
|
|
(text.attrib['class'] and text.attrib['class'].split(
|
|
' ') or []) + ['backwards'])
|
|
self.svg.node(
|
|
guides, 'title',
|
|
).text = self._y_format(position)
|
|
|
|
if self._y_2nd_labels:
|
|
secondary_ax = self.svg.node(
|
|
self.nodes['plot'], class_="axis y2")
|
|
for label, position in self._y_2nd_labels:
|
|
major = position in self._y_labels_major
|
|
if not (self.show_minor_y_labels or major):
|
|
continue
|
|
# it is needed, to have the same structure as primary axis
|
|
guides = self.svg.node(secondary_ax, class_='guides')
|
|
x = self.view.width + 5
|
|
y = self.view.y(position)
|
|
text = self.svg.node(
|
|
guides, 'text',
|
|
x=x,
|
|
y=y + .35 * self.style.label_font_size,
|
|
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)
|
|
if 90 < self.y_label_rotation < 270:
|
|
text.attrib['class'] = ' '.join(
|
|
(text.attrib['class'] and
|
|
text.attrib['class'].split(
|
|
' ') or []) + ['backwards'])
|
|
|
|
def _legend(self):
|
|
"""Make the legend box"""
|
|
if not self.show_legend:
|
|
return
|
|
truncation = self.truncate_legend
|
|
if self.legend_at_bottom:
|
|
x = self.margin_box.left + self.spacing
|
|
y = (self.margin_box.top + self.view.height +
|
|
self._x_title_height +
|
|
self._x_labels_height + self.spacing)
|
|
cols = self.legend_at_bottom_columns or ceil(
|
|
sqrt(self._order)) or 1
|
|
|
|
if not truncation:
|
|
available_space = self.view.width / cols - (
|
|
self.legend_box_size + 5)
|
|
truncation = reverse_text_len(
|
|
available_space, self.style.legend_font_size)
|
|
else:
|
|
x = self.spacing
|
|
y = self.margin_box.top + self.spacing
|
|
cols = 1
|
|
if not truncation:
|
|
truncation = 15
|
|
|
|
legends = self.svg.node(
|
|
self.nodes['graph'], class_='legends',
|
|
transform='translate(%d, %d)' % (x, y))
|
|
|
|
h = max(self.legend_box_size, self.style.legend_font_size)
|
|
x_step = self.view.width / cols
|
|
if self.legend_at_bottom:
|
|
secondary_legends = legends # svg node is the same
|
|
else:
|
|
|
|
# draw secondary axis on right
|
|
x = self.margin_box.left + self.view.width + self.spacing
|
|
if self._y_2nd_labels:
|
|
h, w = get_texts_box(
|
|
cut(self._y_2nd_labels), self.style.label_font_size)
|
|
x += self.spacing + max(w * abs(cos(rad(
|
|
self.y_label_rotation))), h)
|
|
|
|
y = self.margin_box.top + self.spacing
|
|
|
|
secondary_legends = self.svg.node(
|
|
self.nodes['graph'], class_='legends',
|
|
transform='translate(%d, %d)' % (x, y))
|
|
|
|
serie_number = -1
|
|
i = 0
|
|
|
|
for titles, is_secondary in (
|
|
(self._legends, False),
|
|
(self._secondary_legends, True)):
|
|
if not self.legend_at_bottom and is_secondary:
|
|
i = 0
|
|
|
|
for title in titles:
|
|
serie_number += 1
|
|
if title is None:
|
|
continue
|
|
col = i % cols
|
|
row = i // cols
|
|
|
|
legend = self.svg.node(
|
|
secondary_legends if is_secondary else legends,
|
|
class_='legend reactive activate-serie',
|
|
id="activate-serie-%d" % serie_number)
|
|
self.svg.node(
|
|
legend, 'rect',
|
|
x=col * x_step,
|
|
y=1.5 * row * h + (
|
|
self.style.legend_font_size - self.legend_box_size
|
|
if self.style.legend_font_size > self.legend_box_size
|
|
else 0
|
|
) / 2,
|
|
width=self.legend_box_size,
|
|
height=self.legend_box_size,
|
|
class_="color-%d reactive" % serie_number
|
|
)
|
|
|
|
if isinstance(title, dict):
|
|
node = decorate(self.svg, legend, title)
|
|
title = title['title']
|
|
else:
|
|
node = legend
|
|
|
|
truncated = truncate(title, truncation)
|
|
self.svg.node(
|
|
node, 'text',
|
|
x=col * x_step + self.legend_box_size + 5,
|
|
y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size
|
|
).text = truncated
|
|
|
|
if truncated != title:
|
|
self.svg.node(legend, 'title').text = title
|
|
|
|
i += 1
|
|
|
|
def _make_title(self):
|
|
"""Make the title"""
|
|
if self._title:
|
|
for i, title_line in enumerate(self._title, 1):
|
|
self.svg.node(
|
|
self.nodes['title'], 'text', class_='title plot_title',
|
|
x=self.width / 2,
|
|
y=i * (self.style.title_font_size + self.spacing)
|
|
).text = title_line
|
|
|
|
def _make_x_title(self):
|
|
"""Make the X-Axis title"""
|
|
y = (self.height - self.margin_box.bottom +
|
|
self._x_labels_height)
|
|
if self._x_title:
|
|
for i, title_line in enumerate(self._x_title, 1):
|
|
text = self.svg.node(
|
|
self.nodes['title'], 'text', class_='title',
|
|
x=self.margin_box.left + self.view.width / 2,
|
|
y=y + i * (self.style.title_font_size + self.spacing)
|
|
)
|
|
text.text = title_line
|
|
|
|
def _make_y_title(self):
|
|
"""Make the Y-Axis title"""
|
|
if self._y_title:
|
|
yc = self.margin_box.top + self.view.height / 2
|
|
for i, title_line in enumerate(self._y_title, 1):
|
|
text = self.svg.node(
|
|
self.nodes['title'], 'text', class_='title',
|
|
x=self._legend_at_left_width,
|
|
y=i * (self.style.title_font_size + self.spacing) + yc
|
|
)
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % (
|
|
-90, self._legend_at_left_width, yc)
|
|
text.text = title_line
|
|
|
|
def _interpolate(self, xs, ys):
|
|
"""Make the interpolation"""
|
|
x = []
|
|
y = []
|
|
for i in range(len(ys)):
|
|
if ys[i] is not None:
|
|
x.append(xs[i])
|
|
y.append(ys[i])
|
|
|
|
interpolate = INTERPOLATIONS[self.interpolate]
|
|
|
|
return list(interpolate(
|
|
x, y, self.interpolation_precision,
|
|
**self.interpolation_parameters))
|
|
|
|
def _rescale(self, points):
|
|
"""Scale for secondary"""
|
|
return [
|
|
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale
|
|
if y is not None else None)
|
|
for x, y in points]
|
|
|
|
def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None):
|
|
"""Insert in desc tags informations for the javascript tooltip"""
|
|
self.svg.node(node, 'desc', class_="value").text = value
|
|
if classes is None:
|
|
classes = []
|
|
if x > self.view.width / 2:
|
|
classes.append('left')
|
|
if y > self.view.height / 2:
|
|
classes.append('top')
|
|
classes = ' '.join(classes)
|
|
|
|
self.svg.node(node, 'desc',
|
|
class_="x " + classes).text = to_str(x)
|
|
self.svg.node(node, 'desc',
|
|
class_="y " + classes).text = to_str(y)
|
|
if xlabel:
|
|
self.svg.node(node, 'desc',
|
|
class_="x_label").text = to_str(xlabel)
|
|
|
|
def _static_value(self, serie_node, value, x, y, metadata,
|
|
align_text='left', classes=None):
|
|
"""Write the print value"""
|
|
label = metadata and metadata.get('label')
|
|
classes = classes and [classes] or []
|
|
|
|
if self.print_labels and label:
|
|
label_cls = classes + ['label']
|
|
if self.print_values:
|
|
y -= self.style.value_font_size / 2
|
|
self.svg.node(
|
|
serie_node['text_overlay'], 'text',
|
|
class_=' '.join(label_cls),
|
|
x=x,
|
|
y=y + self.style.value_font_size / 3
|
|
).text = label
|
|
y += self.style.value_font_size
|
|
|
|
if self.print_values or self.dynamic_print_values:
|
|
val_cls = classes + ['value']
|
|
if self.dynamic_print_values:
|
|
val_cls.append('showable')
|
|
|
|
self.svg.node(
|
|
serie_node['text_overlay'], 'text',
|
|
class_=' '.join(val_cls),
|
|
x=x,
|
|
y=y + self.style.value_font_size / 3,
|
|
attrib={'text-anchor': align_text}
|
|
).text = value if self.print_zeroes or value != '0' else ''
|
|
|
|
def _points(self, x_pos):
|
|
"""
|
|
Convert given data values into drawable points (x, y)
|
|
and interpolated points if interpolate option is specified
|
|
"""
|
|
for serie in self.all_series:
|
|
serie.points = [
|
|
(x_pos[i], v)
|
|
for i, v in enumerate(serie.values)]
|
|
if serie.points and self.interpolate:
|
|
serie.interpolated = self._interpolate(x_pos, serie.values)
|
|
else:
|
|
serie.interpolated = []
|
|
|
|
def _compute_secondary(self):
|
|
"""Compute secondary axis min max and label positions"""
|
|
# secondary y axis support
|
|
if self.secondary_series and self._y_labels:
|
|
y_pos = list(zip(*self._y_labels))[1]
|
|
if self.include_x_axis:
|
|
ymin = min(self._secondary_min, 0)
|
|
ymax = max(self._secondary_max, 0)
|
|
else:
|
|
ymin = self._secondary_min
|
|
ymax = self._secondary_max
|
|
steps = len(y_pos)
|
|
left_range = abs(y_pos[-1] - y_pos[0])
|
|
right_range = abs(ymax - ymin) or 1
|
|
scale = right_range / ((steps - 1) or 1)
|
|
self._y_2nd_labels = [(self._y_format(ymin + i * scale), pos)
|
|
for i, pos in enumerate(y_pos)]
|
|
|
|
self._scale = left_range / right_range
|
|
self._scale_diff = y_pos[0]
|
|
self._scale_min_2nd = ymin
|
|
|
|
def _post_compute(self):
|
|
"""Hook called after compute and before margin computations and plot"""
|
|
pass
|
|
|
|
def _get_x_label(self, i):
|
|
"""Convenience function to get the x_label of a value index"""
|
|
if not self.x_labels or not self._x_labels or len(self._x_labels) <= i:
|
|
return
|
|
return self._x_labels[i][0]
|
|
|
|
@property
|
|
def all_series(self):
|
|
"""Getter for all series (nomal and secondary)"""
|
|
return self.series + self.secondary_series
|
|
|
|
@property
|
|
def _x_format(self):
|
|
"""Return the abscissa value formatter (always unary)"""
|
|
return self.x_value_formatter
|
|
|
|
@property
|
|
def _default_formatter(self):
|
|
return to_str
|
|
|
|
@property
|
|
def _y_format(self):
|
|
"""Return the ordinate value formatter (always unary)"""
|
|
return self.value_formatter
|
|
|
|
def _value_format(self, value):
|
|
"""
|
|
Format value for value display.
|
|
(Varies in type between chart types)
|
|
"""
|
|
|
|
return self._y_format(value)
|
|
|
|
def _format(self, serie, i):
|
|
"""Format the nth value for the serie"""
|
|
value = serie.values[i]
|
|
metadata = serie.metadata.get(i)
|
|
|
|
kwargs = {
|
|
'chart': self,
|
|
'serie': serie,
|
|
'index': i
|
|
}
|
|
formatter = (
|
|
(metadata and metadata.get('formatter')) or
|
|
serie.formatter or
|
|
self.formatter or
|
|
self._value_format
|
|
)
|
|
kwargs = filter_kwargs(formatter, kwargs)
|
|
return formatter(value, **kwargs)
|
|
|
|
def _serie_format(self, serie, value):
|
|
"""Format an independent value for the serie"""
|
|
|
|
kwargs = {
|
|
'chart': self,
|
|
'serie': serie,
|
|
'index': None
|
|
}
|
|
formatter = (
|
|
serie.formatter or
|
|
self.formatter or
|
|
self._value_format
|
|
)
|
|
kwargs = filter_kwargs(formatter, kwargs)
|
|
return formatter(value, **kwargs)
|
|
|
|
def _compute(self):
|
|
"""Initial computations to draw the graph"""
|
|
|
|
def _compute_margin(self):
|
|
"""Compute graph margins from set texts"""
|
|
self._legend_at_left_width = 0
|
|
for series_group in (self.series, self.secondary_series):
|
|
if self.show_legend and series_group:
|
|
h, w = get_texts_box(
|
|
map(lambda x: truncate(x, self.truncate_legend or 15),
|
|
[serie.title['title']
|
|
if isinstance(serie.title, dict)
|
|
else serie.title or '' for serie in series_group]),
|
|
self.style.legend_font_size)
|
|
if self.legend_at_bottom:
|
|
h_max = max(h, self.legend_box_size)
|
|
cols = (self._order // self.legend_at_bottom_columns
|
|
if self.legend_at_bottom_columns
|
|
else ceil(sqrt(self._order)) or 1)
|
|
self.margin_box.bottom += self.spacing + h_max * round(
|
|
cols - 1) * 1.5 + h_max
|
|
else:
|
|
if series_group is self.series:
|
|
legend_width = self.spacing + w + self.legend_box_size
|
|
self.margin_box.left += legend_width
|
|
self._legend_at_left_width += legend_width
|
|
else:
|
|
self.margin_box.right += (
|
|
self.spacing + w + self.legend_box_size)
|
|
|
|
self._x_labels_height = 0
|
|
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels:
|
|
for xlabels in (self._x_labels, self._x_2nd_labels):
|
|
if xlabels:
|
|
h, w = get_texts_box(
|
|
map(lambda x: truncate(x, self.truncate_label or 25),
|
|
cut(xlabels)),
|
|
self.style.label_font_size)
|
|
self._x_labels_height = self.spacing + max(
|
|
w * abs(sin(rad(self.x_label_rotation))), h)
|
|
if xlabels is self._x_labels:
|
|
self.margin_box.bottom += self._x_labels_height
|
|
else:
|
|
self.margin_box.top += self._x_labels_height
|
|
if self.x_label_rotation:
|
|
if self.x_label_rotation % 180 < 90:
|
|
self.margin_box.right = max(
|
|
w * abs(cos(rad(self.x_label_rotation))),
|
|
self.margin_box.right)
|
|
else:
|
|
self.margin_box.left = max(
|
|
w * abs(cos(rad(self.x_label_rotation))),
|
|
self.margin_box.left)
|
|
|
|
if self.show_y_labels:
|
|
for ylabels in (self._y_labels, self._y_2nd_labels):
|
|
if ylabels:
|
|
h, w = get_texts_box(
|
|
cut(ylabels), self.style.label_font_size)
|
|
if ylabels is self._y_labels:
|
|
self.margin_box.left += self.spacing + max(
|
|
w * abs(cos(rad(self.y_label_rotation))), h)
|
|
else:
|
|
self.margin_box.right += self.spacing + max(
|
|
w * abs(cos(rad(self.y_label_rotation))), h)
|
|
|
|
self._title = split_title(
|
|
self.title, self.width, self.style.title_font_size)
|
|
|
|
if self.title:
|
|
h, _ = get_text_box(self._title[0], self.style.title_font_size)
|
|
self.margin_box.top += len(self._title) * (self.spacing + h)
|
|
|
|
self._x_title = split_title(
|
|
self.x_title, self.width - self.margin_box.x,
|
|
self.style.title_font_size)
|
|
|
|
self._x_title_height = 0
|
|
if self._x_title:
|
|
h, _ = get_text_box(self._x_title[0], self.style.title_font_size)
|
|
height = len(self._x_title) * (self.spacing + h)
|
|
self.margin_box.bottom += height
|
|
self._x_title_height = height + self.spacing
|
|
|
|
self._y_title = split_title(
|
|
self.y_title, self.height - self.margin_box.y,
|
|
self.style.title_font_size)
|
|
|
|
self._y_title_height = 0
|
|
if self._y_title:
|
|
h, _ = get_text_box(self._y_title[0], self.style.title_font_size)
|
|
height = len(self._y_title) * (self.spacing + h)
|
|
self.margin_box.left += height
|
|
self._y_title_height = height + self.spacing
|
|
|
|
# Inner margin
|
|
if self.print_values_position == 'top':
|
|
gh = self.height - self.margin_box.y
|
|
alpha = 1.1 * (self.style.value_font_size / gh) * self._box.height
|
|
if self._max and self._max > 0:
|
|
self._box.ymax += alpha
|
|
if self._min and self._min < 0:
|
|
self._box.ymin -= alpha
|
|
|
|
def _confidence_interval(self, node, x, y, value, metadata):
|
|
if not metadata or 'ci' not in metadata:
|
|
return
|
|
ci = metadata['ci']
|
|
ci['point_estimate'] = value
|
|
|
|
low, high = getattr(
|
|
stats,
|
|
'confidence_interval_%s' % ci.get('type', 'manual')
|
|
)(**ci)
|
|
|
|
self.svg.confidence_interval(
|
|
node, x,
|
|
# Respect some charts y modifications (pyramid, stackbar)
|
|
y + (self.view.y(low) - self.view.y(value)),
|
|
y + (self.view.y(high) - self.view.y(value)))
|
|
|
|
@cached_property
|
|
def _legends(self):
|
|
"""Getter for series title"""
|
|
return [serie.title for serie in self.series]
|
|
|
|
@cached_property
|
|
def _secondary_legends(self):
|
|
"""Getter for series title on secondary y axis"""
|
|
return [serie.title for serie in self.secondary_series]
|
|
|
|
@cached_property
|
|
def _values(self):
|
|
"""Getter for series values (flattened)"""
|
|
return [val
|
|
for serie in self.series
|
|
for val in serie.values
|
|
if val is not None]
|
|
|
|
@cached_property
|
|
def _secondary_values(self):
|
|
"""Getter for secondary series values (flattened)"""
|
|
return [val
|
|
for serie in self.secondary_series
|
|
for val in serie.values
|
|
if val is not None]
|
|
|
|
@cached_property
|
|
def _len(self):
|
|
"""Getter for the maximum series size"""
|
|
return max([
|
|
len(serie.values)
|
|
for serie in self.all_series] or [0])
|
|
|
|
@cached_property
|
|
def _secondary_min(self):
|
|
"""Getter for the minimum series value"""
|
|
return (self.secondary_range[0] if (
|
|
self.secondary_range and self.secondary_range[0] is not None)
|
|
else (min(self._secondary_values)
|
|
if self._secondary_values else None))
|
|
|
|
@cached_property
|
|
def _min(self):
|
|
"""Getter for the minimum series value"""
|
|
return (self.range[0] if (self.range and self.range[0] is not None)
|
|
else (min(self._values)
|
|
if self._values else None))
|
|
|
|
@cached_property
|
|
def _max(self):
|
|
"""Getter for the maximum series value"""
|
|
return (self.range[1] if (self.range and self.range[1] is not None)
|
|
else (max(self._values) if self._values else None))
|
|
|
|
@cached_property
|
|
def _secondary_max(self):
|
|
"""Getter for the maximum series value"""
|
|
return (self.secondary_range[1] if (
|
|
self.secondary_range and self.secondary_range[1] is not None)
|
|
else (max(self._secondary_values)
|
|
if self._secondary_values else None))
|
|
|
|
@cached_property
|
|
def _order(self):
|
|
"""Getter for the number of series"""
|
|
return len(self.all_series)
|
|
|
|
def _x_label_format_if_value(self, label):
|
|
if not is_str(label):
|
|
return self._x_format(label)
|
|
return label
|
|
|
|
def _compute_x_labels(self):
|
|
self._x_labels = self.x_labels and list(
|
|
zip(map(self._x_label_format_if_value, self.x_labels),
|
|
self._x_pos))
|
|
|
|
def _compute_x_labels_major(self):
|
|
if self.x_labels_major_every:
|
|
self._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):
|
|
self._x_labels_major = [label[0] for label in self._x_labels]
|
|
|
|
else:
|
|
self._x_labels_major = [self._x_labels[
|
|
int(i * (label_count - 1) / (major_count - 1))][0]
|
|
for i in range(major_count)]
|
|
else:
|
|
self._x_labels_major = self.x_labels_major and list(
|
|
map(self._x_label_format_if_value, self.x_labels_major)) or []
|
|
|
|
def _compute_y_labels(self):
|
|
y_pos = compute_scale(
|
|
self._box.ymin, self._box.ymax, self.logarithmic,
|
|
self.order_min, self.min_scale, self.max_scale
|
|
)
|
|
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 % len(y_pos)])
|
|
title = y_label
|
|
else:
|
|
pos = self._adapt(y_label)
|
|
title = self._y_format(pos)
|
|
self._y_labels.append((title, pos))
|
|
self._box.ymin = min(self._box.ymin, min(cut(self._y_labels, 1)))
|
|
self._box.ymax = max(self._box.ymax, max(cut(self._y_labels, 1)))
|
|
else:
|
|
self._y_labels = list(zip(map(self._y_format, y_pos), y_pos))
|
|
|
|
def _compute_y_labels_major(self):
|
|
if self.y_labels_major_every:
|
|
self._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):
|
|
self._y_labels_major = [label[1] for label in self._y_labels]
|
|
else:
|
|
self._y_labels_major = [self._y_labels[
|
|
int(i * (label_count - 1) / (major_count - 1))][1]
|
|
for i in range(major_count)]
|
|
|
|
elif self.y_labels_major:
|
|
self._y_labels_major = list(map(self._adapt, self.y_labels_major))
|
|
elif self._y_labels:
|
|
self._y_labels_major = majorize(cut(self._y_labels, 1))
|
|
else:
|
|
self._y_labels_major = []
|
|
|
|
def add_squares(self, squares):
|
|
x_lines = squares[0] - 1
|
|
y_lines = squares[1] - 1
|
|
|
|
_current_x = 0
|
|
_current_y = 0
|
|
|
|
for line in range(x_lines):
|
|
_current_x += (self.width - self.margin_box.x) / squares[0]
|
|
self.svg.node(
|
|
self.nodes['plot'], 'path',
|
|
class_='bg-lines',
|
|
d='M%s %s L%s %s' % (
|
|
_current_x, 0, _current_x,
|
|
self.height - self.margin_box.y))
|
|
|
|
for line in range(y_lines):
|
|
_current_y += (self.height - self.margin_box.y) / squares[1]
|
|
self.svg.node(
|
|
self.nodes['plot'], 'path',
|
|
class_='bg-lines',
|
|
d='M%s %s L%s %s' % (
|
|
0, _current_y, self.width - self.margin_box.x, _current_y))
|
|
return ((self.width - self.margin_box.x) / squares[0],
|
|
(self.height - self.margin_box.y) / squares[1])
|
|
|
|
def _draw(self):
|
|
"""Draw all the things"""
|
|
self._compute()
|
|
self._compute_x_labels()
|
|
self._compute_x_labels_major()
|
|
self._compute_y_labels()
|
|
self._compute_y_labels_major()
|
|
self._compute_secondary()
|
|
self._post_compute()
|
|
self._compute_margin()
|
|
self._decorate()
|
|
if self.series and self._has_data() and self._values:
|
|
self._plot()
|
|
else:
|
|
self.svg.draw_no_data()
|
|
|
|
def _has_data(self):
|
|
"""Check if there is any data"""
|
|
return any([
|
|
len([
|
|
v for a in (s[0] if is_list_like(s) else [s])
|
|
for v in (a if is_list_like(a) else [a])
|
|
if v is not None])
|
|
for s in self.raw_series
|
|
])
|