|
|
@ -28,12 +28,12 @@ from pygal.graph.public import PublicApi |
|
|
|
from pygal.interpolate import INTERPOLATIONS |
|
|
|
from pygal.interpolate import INTERPOLATIONS |
|
|
|
from pygal.util import ( |
|
|
|
from pygal.util import ( |
|
|
|
cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box, |
|
|
|
cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box, |
|
|
|
get_texts_box, majorize, rad, reverse_text_len, split_title, truncate) |
|
|
|
get_texts_box, majorize, rad, reverse_text_len, split_title, truncate |
|
|
|
|
|
|
|
) |
|
|
|
from pygal.view import LogView, ReverseView, View, XYLogView |
|
|
|
from pygal.view import LogView, ReverseView, View, XYLogView |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Graph(PublicApi): |
|
|
|
class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
"""Graph super class containing generic common functions""" |
|
|
|
"""Graph super class containing generic common functions""" |
|
|
|
|
|
|
|
|
|
|
|
_dual = False |
|
|
|
_dual = False |
|
|
@ -64,65 +64,93 @@ class Graph(PublicApi): |
|
|
|
view_class = ReverseView if self.inverse_y_axis else View |
|
|
|
view_class = ReverseView if self.inverse_y_axis else View |
|
|
|
|
|
|
|
|
|
|
|
self.view = view_class( |
|
|
|
self.view = view_class( |
|
|
|
self.width - self.margin_box.x, |
|
|
|
self.width - self.margin_box.x, self.height - self.margin_box.y, |
|
|
|
self.height - self.margin_box.y, |
|
|
|
self._box |
|
|
|
self._box) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _make_graph(self): |
|
|
|
def _make_graph(self): |
|
|
|
"""Init common graph svg structure""" |
|
|
|
"""Init common graph svg structure""" |
|
|
|
self.nodes['graph'] = self.svg.node( |
|
|
|
self.nodes['graph'] = self.svg.node( |
|
|
|
class_='graph %s-graph %s' % ( |
|
|
|
class_='graph %s-graph %s' % ( |
|
|
|
self.__class__.__name__.lower(), |
|
|
|
self.__class__.__name__.lower(), |
|
|
|
'horizontal' if self.horizontal else 'vertical')) |
|
|
|
'horizontal' if self.horizontal else 'vertical' |
|
|
|
self.svg.node(self.nodes['graph'], 'rect', |
|
|
|
) |
|
|
|
class_='background', |
|
|
|
) |
|
|
|
x=0, y=0, |
|
|
|
self.svg.node( |
|
|
|
width=self.width, |
|
|
|
self.nodes['graph'], |
|
|
|
height=self.height) |
|
|
|
'rect', |
|
|
|
|
|
|
|
class_='background', |
|
|
|
|
|
|
|
x=0, |
|
|
|
|
|
|
|
y=0, |
|
|
|
|
|
|
|
width=self.width, |
|
|
|
|
|
|
|
height=self.height |
|
|
|
|
|
|
|
) |
|
|
|
self.nodes['plot'] = self.svg.node( |
|
|
|
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'], |
|
|
|
self.nodes['graph'], |
|
|
|
class_="titles") |
|
|
|
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['overlay'] = self.svg.node( |
|
|
|
self.nodes['graph'], class_="plot overlay", |
|
|
|
self.nodes['graph'], |
|
|
|
transform="translate(%d, %d)" % ( |
|
|
|
class_="plot overlay", |
|
|
|
self.margin_box.left, self.margin_box.top)) |
|
|
|
transform="translate(%d, %d)" % |
|
|
|
|
|
|
|
(self.margin_box.left, self.margin_box.top) |
|
|
|
|
|
|
|
) |
|
|
|
self.nodes['text_overlay'] = self.svg.node( |
|
|
|
self.nodes['text_overlay'] = self.svg.node( |
|
|
|
self.nodes['graph'], class_="plot text-overlay", |
|
|
|
self.nodes['graph'], |
|
|
|
transform="translate(%d, %d)" % ( |
|
|
|
class_="plot text-overlay", |
|
|
|
self.margin_box.left, self.margin_box.top)) |
|
|
|
transform="translate(%d, %d)" % |
|
|
|
|
|
|
|
(self.margin_box.left, self.margin_box.top) |
|
|
|
|
|
|
|
) |
|
|
|
self.nodes['tooltip_overlay'] = self.svg.node( |
|
|
|
self.nodes['tooltip_overlay'] = self.svg.node( |
|
|
|
self.nodes['graph'], class_="plot tooltip-overlay", |
|
|
|
self.nodes['graph'], |
|
|
|
transform="translate(%d, %d)" % ( |
|
|
|
class_="plot tooltip-overlay", |
|
|
|
self.margin_box.left, self.margin_box.top)) |
|
|
|
transform="translate(%d, %d)" % |
|
|
|
|
|
|
|
(self.margin_box.left, self.margin_box.top) |
|
|
|
|
|
|
|
) |
|
|
|
self.nodes['tooltip'] = self.svg.node( |
|
|
|
self.nodes['tooltip'] = self.svg.node( |
|
|
|
self.nodes['tooltip_overlay'], |
|
|
|
self.nodes['tooltip_overlay'], |
|
|
|
transform='translate(0 0)', |
|
|
|
transform='translate(0 0)', |
|
|
|
style="opacity: 0", |
|
|
|
style="opacity: 0", |
|
|
|
**{'class': 'tooltip'}) |
|
|
|
**{ |
|
|
|
|
|
|
|
'class': 'tooltip' |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self.svg.node(self.nodes['tooltip'], 'rect', |
|
|
|
self.svg.node( |
|
|
|
rx=self.tooltip_border_radius, |
|
|
|
self.nodes['tooltip'], |
|
|
|
ry=self.tooltip_border_radius, |
|
|
|
'rect', |
|
|
|
width=0, height=0, |
|
|
|
rx=self.tooltip_border_radius, |
|
|
|
**{'class': 'tooltip-box'}) |
|
|
|
ry=self.tooltip_border_radius, |
|
|
|
|
|
|
|
width=0, |
|
|
|
|
|
|
|
height=0, |
|
|
|
|
|
|
|
**{ |
|
|
|
|
|
|
|
'class': 'tooltip-box' |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
) |
|
|
|
self.svg.node(self.nodes['tooltip'], 'g', class_='text') |
|
|
|
self.svg.node(self.nodes['tooltip'], 'g', class_='text') |
|
|
|
|
|
|
|
|
|
|
|
def _x_axis(self): |
|
|
|
def _x_axis(self): |
|
|
|
"""Make the x axis: labels and guides""" |
|
|
|
"""Make the x axis: labels and guides""" |
|
|
|
if not self._x_labels or not self.show_x_labels: |
|
|
|
if not self._x_labels or not self.show_x_labels: |
|
|
|
return |
|
|
|
return |
|
|
|
axis = self.svg.node(self.nodes['plot'], class_="axis x%s" % ( |
|
|
|
axis = self.svg.node( |
|
|
|
' always_show' if self.show_x_guides else '' |
|
|
|
self.nodes['plot'], |
|
|
|
)) |
|
|
|
class_="axis x%s" % (' always_show' if self.show_x_guides else '') |
|
|
|
|
|
|
|
) |
|
|
|
truncation = self.truncate_label |
|
|
|
truncation = self.truncate_label |
|
|
|
if not truncation: |
|
|
|
if not truncation: |
|
|
|
if self.x_label_rotation or len(self._x_labels) <= 1: |
|
|
|
if self.x_label_rotation or len(self._x_labels) <= 1: |
|
|
@ -130,18 +158,21 @@ class Graph(PublicApi): |
|
|
|
else: |
|
|
|
else: |
|
|
|
first_label_position = self.view.x(self._x_labels[0][1]) or 0 |
|
|
|
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 |
|
|
|
last_label_position = self.view.x(self._x_labels[-1][1]) or 0 |
|
|
|
available_space = ( |
|
|
|
available_space = (last_label_position - first_label_position |
|
|
|
last_label_position - first_label_position) / ( |
|
|
|
) / (len(self._x_labels) - 1) |
|
|
|
len(self._x_labels) - 1) |
|
|
|
|
|
|
|
truncation = reverse_text_len( |
|
|
|
truncation = reverse_text_len( |
|
|
|
available_space, self.style.label_font_size) |
|
|
|
available_space, self.style.label_font_size |
|
|
|
|
|
|
|
) |
|
|
|
truncation = max(truncation, 1) |
|
|
|
truncation = max(truncation, 1) |
|
|
|
|
|
|
|
|
|
|
|
lastlabel = self._x_labels[-1][0] |
|
|
|
lastlabel = self._x_labels[-1][0] |
|
|
|
if 0 not in [label[1] for label in self._x_labels]: |
|
|
|
if 0 not in [label[1] for label in self._x_labels]: |
|
|
|
self.svg.node(axis, 'path', |
|
|
|
self.svg.node( |
|
|
|
d='M%f %f v%f' % (0, 0, self.view.height), |
|
|
|
axis, |
|
|
|
class_='line') |
|
|
|
'path', |
|
|
|
|
|
|
|
d='M%f %f v%f' % (0, 0, self.view.height), |
|
|
|
|
|
|
|
class_='line' |
|
|
|
|
|
|
|
) |
|
|
|
lastlabel = None |
|
|
|
lastlabel = None |
|
|
|
|
|
|
|
|
|
|
|
for label, position in self._x_labels: |
|
|
|
for label, position in self._x_labels: |
|
|
@ -158,18 +189,18 @@ class Graph(PublicApi): |
|
|
|
y = self.view.height + 5 |
|
|
|
y = self.view.height + 5 |
|
|
|
last_guide = (self._y_2nd_labels and label == lastlabel) |
|
|
|
last_guide = (self._y_2nd_labels and label == lastlabel) |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
guides, 'path', |
|
|
|
guides, |
|
|
|
|
|
|
|
'path', |
|
|
|
d='M%f %f v%f' % (x or 0, 0, self.view.height), |
|
|
|
d='M%f %f v%f' % (x or 0, 0, self.view.height), |
|
|
|
class_='%s%s%sline' % ( |
|
|
|
class_='%s%s%sline' % ( |
|
|
|
'axis ' if label == "0" else '', |
|
|
|
'axis ' if label == "0" else '', 'major ' |
|
|
|
'major ' if major else '', |
|
|
|
if major else '', 'guide ' |
|
|
|
'guide ' if position != 0 and not last_guide else '')) |
|
|
|
if position != 0 and not last_guide else '' |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
y += .5 * self.style.label_font_size + 5 |
|
|
|
y += .5 * self.style.label_font_size + 5 |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
guides, 'text', |
|
|
|
guides, 'text', x=x, y=y, class_='major' if major else '' |
|
|
|
x=x, |
|
|
|
|
|
|
|
y=y, |
|
|
|
|
|
|
|
class_='major' if major else '' |
|
|
|
|
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
text.text = truncate(label, truncation) |
|
|
|
text.text = truncate(label, truncation) |
|
|
@ -177,29 +208,35 @@ class Graph(PublicApi): |
|
|
|
self.svg.node(guides, 'title').text = label |
|
|
|
self.svg.node(guides, 'title').text = label |
|
|
|
elif self._dual: |
|
|
|
elif self._dual: |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
guides, 'title', |
|
|
|
guides, |
|
|
|
|
|
|
|
'title', |
|
|
|
).text = self._x_format(position) |
|
|
|
).text = self._x_format(position) |
|
|
|
|
|
|
|
|
|
|
|
if self.x_label_rotation: |
|
|
|
if self.x_label_rotation: |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
self.x_label_rotation, x, y) |
|
|
|
self.x_label_rotation, x, y |
|
|
|
|
|
|
|
) |
|
|
|
if self.x_label_rotation >= 180: |
|
|
|
if self.x_label_rotation >= 180: |
|
|
|
text.attrib['class'] = ' '.join( |
|
|
|
text.attrib['class'] = ' '.join(( |
|
|
|
(text.attrib['class'] and text.attrib['class'].split( |
|
|
|
text.attrib['class'] |
|
|
|
' ') or []) + ['backwards']) |
|
|
|
and text.attrib['class'].split(' ') or [] |
|
|
|
|
|
|
|
) + ['backwards']) |
|
|
|
|
|
|
|
|
|
|
|
if self._y_2nd_labels and 0 not in [ |
|
|
|
if self._y_2nd_labels and 0 not in [label[1] |
|
|
|
label[1] for label in self._x_labels]: |
|
|
|
for label in self._x_labels]: |
|
|
|
self.svg.node(axis, 'path', |
|
|
|
self.svg.node( |
|
|
|
d='M%f %f v%f' % ( |
|
|
|
axis, |
|
|
|
self.view.width, 0, self.view.height), |
|
|
|
'path', |
|
|
|
class_='line') |
|
|
|
d='M%f %f v%f' % (self.view.width, 0, self.view.height), |
|
|
|
|
|
|
|
class_='line' |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if self._x_2nd_labels: |
|
|
|
if self._x_2nd_labels: |
|
|
|
secondary_ax = self.svg.node( |
|
|
|
secondary_ax = self.svg.node( |
|
|
|
self.nodes['plot'], class_="axis x x2%s" % ( |
|
|
|
self.nodes['plot'], |
|
|
|
' always_show' if self.show_x_guides else '' |
|
|
|
class_="axis x x2%s" % |
|
|
|
)) |
|
|
|
(' always_show' if self.show_x_guides else '') |
|
|
|
|
|
|
|
) |
|
|
|
for label, position in self._x_2nd_labels: |
|
|
|
for label, position in self._x_2nd_labels: |
|
|
|
major = label in self._x_labels_major |
|
|
|
major = label in self._x_labels_major |
|
|
|
if not (self.show_minor_x_labels or major): |
|
|
|
if not (self.show_minor_x_labels or major): |
|
|
@ -209,37 +246,38 @@ class Graph(PublicApi): |
|
|
|
x = self.view.x(position) |
|
|
|
x = self.view.x(position) |
|
|
|
y = -5 |
|
|
|
y = -5 |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
guides, 'text', |
|
|
|
guides, 'text', x=x, y=y, class_='major' if major else '' |
|
|
|
x=x, |
|
|
|
|
|
|
|
y=y, |
|
|
|
|
|
|
|
class_='major' if major else '' |
|
|
|
|
|
|
|
) |
|
|
|
) |
|
|
|
text.text = label |
|
|
|
text.text = label |
|
|
|
if self.x_label_rotation: |
|
|
|
if self.x_label_rotation: |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
-self.x_label_rotation, x, y) |
|
|
|
-self.x_label_rotation, x, y |
|
|
|
|
|
|
|
) |
|
|
|
if self.x_label_rotation >= 180: |
|
|
|
if self.x_label_rotation >= 180: |
|
|
|
text.attrib['class'] = ' '.join(( |
|
|
|
text.attrib['class'] = ' '.join(( |
|
|
|
text.attrib['class'] and |
|
|
|
text.attrib['class'] |
|
|
|
text.attrib['class'].split( |
|
|
|
and text.attrib['class'].split(' ') or [] |
|
|
|
' ') or []) + ['backwards']) |
|
|
|
) + ['backwards']) |
|
|
|
|
|
|
|
|
|
|
|
def _y_axis(self): |
|
|
|
def _y_axis(self): |
|
|
|
"""Make the y axis: labels and guides""" |
|
|
|
"""Make the y axis: labels and guides""" |
|
|
|
if not self._y_labels or not self.show_y_labels: |
|
|
|
if not self._y_labels or not self.show_y_labels: |
|
|
|
return |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
axis = self.svg.node(self.nodes['plot'], class_="axis y%s" % ( |
|
|
|
axis = self.svg.node( |
|
|
|
' always_show' if self.show_y_guides else '' |
|
|
|
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 |
|
|
|
if (0 not in [label[1] for label in self._y_labels] |
|
|
|
self.show_y_guides): |
|
|
|
and self.show_y_guides): |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
axis, 'path', |
|
|
|
axis, |
|
|
|
|
|
|
|
'path', |
|
|
|
d='M%f %f h%f' % ( |
|
|
|
d='M%f %f h%f' % ( |
|
|
|
0, 0 if self.inverse_y_axis else self.view.height, |
|
|
|
0, 0 if self.inverse_y_axis else self.view.height, |
|
|
|
self.view.width), |
|
|
|
self.view.width |
|
|
|
|
|
|
|
), |
|
|
|
class_='line' |
|
|
|
class_='line' |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
@ -251,23 +289,28 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
if not (self.show_minor_y_labels or major): |
|
|
|
if not (self.show_minor_y_labels or major): |
|
|
|
continue |
|
|
|
continue |
|
|
|
guides = self.svg.node(axis, class_='%sguides' % ( |
|
|
|
guides = self.svg.node( |
|
|
|
'logarithmic ' if self.logarithmic else '' |
|
|
|
axis, |
|
|
|
)) |
|
|
|
class_='%sguides' % |
|
|
|
|
|
|
|
('logarithmic ' if self.logarithmic else '') |
|
|
|
|
|
|
|
) |
|
|
|
x = -5 |
|
|
|
x = -5 |
|
|
|
y = self.view.y(position) |
|
|
|
y = self.view.y(position) |
|
|
|
if not y: |
|
|
|
if not y: |
|
|
|
continue |
|
|
|
continue |
|
|
|
if self.show_y_guides: |
|
|
|
if self.show_y_guides: |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
guides, 'path', |
|
|
|
guides, |
|
|
|
|
|
|
|
'path', |
|
|
|
d='M%f %f h%f' % (0, y, self.view.width), |
|
|
|
d='M%f %f h%f' % (0, y, self.view.width), |
|
|
|
class_='%s%s%sline' % ( |
|
|
|
class_='%s%s%sline' % ( |
|
|
|
'axis ' if label == "0" else '', |
|
|
|
'axis ' if label == "0" else '', 'major ' |
|
|
|
'major ' if major else '', |
|
|
|
if major else '', 'guide ' if position != 0 else '' |
|
|
|
'guide ' if position != 0 else '')) |
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
guides, 'text', |
|
|
|
guides, |
|
|
|
|
|
|
|
'text', |
|
|
|
x=x, |
|
|
|
x=x, |
|
|
|
y=y + .35 * self.style.label_font_size, |
|
|
|
y=y + .35 * self.style.label_font_size, |
|
|
|
class_='major' if major else '' |
|
|
|
class_='major' if major else '' |
|
|
@ -277,18 +320,20 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
if self.y_label_rotation: |
|
|
|
if self.y_label_rotation: |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
self.y_label_rotation, x, y) |
|
|
|
self.y_label_rotation, x, y |
|
|
|
|
|
|
|
) |
|
|
|
if 90 < self.y_label_rotation < 270: |
|
|
|
if 90 < self.y_label_rotation < 270: |
|
|
|
text.attrib['class'] = ' '.join( |
|
|
|
text.attrib['class'] = ' '.join(( |
|
|
|
(text.attrib['class'] and text.attrib['class'].split( |
|
|
|
text.attrib['class'] |
|
|
|
' ') or []) + ['backwards']) |
|
|
|
and text.attrib['class'].split(' ') or [] |
|
|
|
|
|
|
|
) + ['backwards']) |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
guides, 'title', |
|
|
|
guides, |
|
|
|
|
|
|
|
'title', |
|
|
|
).text = self._y_format(position) |
|
|
|
).text = self._y_format(position) |
|
|
|
|
|
|
|
|
|
|
|
if self._y_2nd_labels: |
|
|
|
if self._y_2nd_labels: |
|
|
|
secondary_ax = self.svg.node( |
|
|
|
secondary_ax = self.svg.node(self.nodes['plot'], class_="axis y2") |
|
|
|
self.nodes['plot'], class_="axis y2") |
|
|
|
|
|
|
|
for label, position in self._y_2nd_labels: |
|
|
|
for label, position in self._y_2nd_labels: |
|
|
|
major = position in self._y_labels_major |
|
|
|
major = position in self._y_labels_major |
|
|
|
if not (self.show_minor_y_labels or major): |
|
|
|
if not (self.show_minor_y_labels or major): |
|
|
@ -298,7 +343,8 @@ class Graph(PublicApi): |
|
|
|
x = self.view.width + 5 |
|
|
|
x = self.view.width + 5 |
|
|
|
y = self.view.y(position) |
|
|
|
y = self.view.y(position) |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
guides, 'text', |
|
|
|
guides, |
|
|
|
|
|
|
|
'text', |
|
|
|
x=x, |
|
|
|
x=x, |
|
|
|
y=y + .35 * self.style.label_font_size, |
|
|
|
y=y + .35 * self.style.label_font_size, |
|
|
|
class_='major' if major else '' |
|
|
|
class_='major' if major else '' |
|
|
@ -306,12 +352,13 @@ class Graph(PublicApi): |
|
|
|
text.text = label |
|
|
|
text.text = label |
|
|
|
if self.y_label_rotation: |
|
|
|
if self.y_label_rotation: |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
self.y_label_rotation, x, y) |
|
|
|
self.y_label_rotation, x, y |
|
|
|
|
|
|
|
) |
|
|
|
if 90 < self.y_label_rotation < 270: |
|
|
|
if 90 < self.y_label_rotation < 270: |
|
|
|
text.attrib['class'] = ' '.join( |
|
|
|
text.attrib['class'] = ' '.join(( |
|
|
|
(text.attrib['class'] and |
|
|
|
text.attrib['class'] |
|
|
|
text.attrib['class'].split( |
|
|
|
and text.attrib['class'].split(' ') or [] |
|
|
|
' ') or []) + ['backwards']) |
|
|
|
) + ['backwards']) |
|
|
|
|
|
|
|
|
|
|
|
def _legend(self): |
|
|
|
def _legend(self): |
|
|
|
"""Make the legend box""" |
|
|
|
"""Make the legend box""" |
|
|
@ -320,17 +367,20 @@ class Graph(PublicApi): |
|
|
|
truncation = self.truncate_legend |
|
|
|
truncation = self.truncate_legend |
|
|
|
if self.legend_at_bottom: |
|
|
|
if self.legend_at_bottom: |
|
|
|
x = self.margin_box.left + self.spacing |
|
|
|
x = self.margin_box.left + self.spacing |
|
|
|
y = (self.margin_box.top + self.view.height + |
|
|
|
y = ( |
|
|
|
self._x_title_height + |
|
|
|
self.margin_box.top + self.view.height + self._x_title_height + |
|
|
|
self._x_labels_height + self.spacing) |
|
|
|
self._x_labels_height + self.spacing |
|
|
|
cols = self.legend_at_bottom_columns or ceil( |
|
|
|
) |
|
|
|
sqrt(self._order)) or 1 |
|
|
|
cols = self.legend_at_bottom_columns or ceil(sqrt(self._order) |
|
|
|
|
|
|
|
) or 1 |
|
|
|
|
|
|
|
|
|
|
|
if not truncation: |
|
|
|
if not truncation: |
|
|
|
available_space = self.view.width / cols - ( |
|
|
|
available_space = self.view.width / cols - ( |
|
|
|
self.legend_box_size + 5) |
|
|
|
self.legend_box_size + 5 |
|
|
|
|
|
|
|
) |
|
|
|
truncation = reverse_text_len( |
|
|
|
truncation = reverse_text_len( |
|
|
|
available_space, self.style.legend_font_size) |
|
|
|
available_space, self.style.legend_font_size |
|
|
|
|
|
|
|
) |
|
|
|
else: |
|
|
|
else: |
|
|
|
x = self.spacing |
|
|
|
x = self.spacing |
|
|
|
y = self.margin_box.top + self.spacing |
|
|
|
y = self.margin_box.top + self.spacing |
|
|
@ -339,8 +389,10 @@ class Graph(PublicApi): |
|
|
|
truncation = 15 |
|
|
|
truncation = 15 |
|
|
|
|
|
|
|
|
|
|
|
legends = self.svg.node( |
|
|
|
legends = self.svg.node( |
|
|
|
self.nodes['graph'], class_='legends', |
|
|
|
self.nodes['graph'], |
|
|
|
transform='translate(%d, %d)' % (x, y)) |
|
|
|
class_='legends', |
|
|
|
|
|
|
|
transform='translate(%d, %d)' % (x, y) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
h = max(self.legend_box_size, self.style.legend_font_size) |
|
|
|
h = max(self.legend_box_size, self.style.legend_font_size) |
|
|
|
x_step = self.view.width / cols |
|
|
|
x_step = self.view.width / cols |
|
|
@ -352,22 +404,25 @@ class Graph(PublicApi): |
|
|
|
x = self.margin_box.left + self.view.width + self.spacing |
|
|
|
x = self.margin_box.left + self.view.width + self.spacing |
|
|
|
if self._y_2nd_labels: |
|
|
|
if self._y_2nd_labels: |
|
|
|
h, w = get_texts_box( |
|
|
|
h, w = get_texts_box( |
|
|
|
cut(self._y_2nd_labels), self.style.label_font_size) |
|
|
|
cut(self._y_2nd_labels), self.style.label_font_size |
|
|
|
x += self.spacing + max(w * abs(cos(rad( |
|
|
|
) |
|
|
|
self.y_label_rotation))), h) |
|
|
|
x += self.spacing + max( |
|
|
|
|
|
|
|
w * abs(cos(rad(self.y_label_rotation))), h |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
y = self.margin_box.top + self.spacing |
|
|
|
y = self.margin_box.top + self.spacing |
|
|
|
|
|
|
|
|
|
|
|
secondary_legends = self.svg.node( |
|
|
|
secondary_legends = self.svg.node( |
|
|
|
self.nodes['graph'], class_='legends', |
|
|
|
self.nodes['graph'], |
|
|
|
transform='translate(%d, %d)' % (x, y)) |
|
|
|
class_='legends', |
|
|
|
|
|
|
|
transform='translate(%d, %d)' % (x, y) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
serie_number = -1 |
|
|
|
serie_number = -1 |
|
|
|
i = 0 |
|
|
|
i = 0 |
|
|
|
|
|
|
|
|
|
|
|
for titles, is_secondary in ( |
|
|
|
for titles, is_secondary in ((self._legends, False), |
|
|
|
(self._legends, False), |
|
|
|
(self._secondary_legends, True)): |
|
|
|
(self._secondary_legends, True)): |
|
|
|
|
|
|
|
if not self.legend_at_bottom and is_secondary: |
|
|
|
if not self.legend_at_bottom and is_secondary: |
|
|
|
i = 0 |
|
|
|
i = 0 |
|
|
|
|
|
|
|
|
|
|
@ -381,9 +436,11 @@ class Graph(PublicApi): |
|
|
|
legend = self.svg.node( |
|
|
|
legend = self.svg.node( |
|
|
|
secondary_legends if is_secondary else legends, |
|
|
|
secondary_legends if is_secondary else legends, |
|
|
|
class_='legend reactive activate-serie', |
|
|
|
class_='legend reactive activate-serie', |
|
|
|
id="activate-serie-%d" % serie_number) |
|
|
|
id="activate-serie-%d" % serie_number |
|
|
|
|
|
|
|
) |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
legend, 'rect', |
|
|
|
legend, |
|
|
|
|
|
|
|
'rect', |
|
|
|
x=col * x_step, |
|
|
|
x=col * x_step, |
|
|
|
y=1.5 * row * h + ( |
|
|
|
y=1.5 * row * h + ( |
|
|
|
self.style.legend_font_size - self.legend_box_size |
|
|
|
self.style.legend_font_size - self.legend_box_size |
|
|
@ -403,7 +460,8 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
truncated = truncate(title, truncation) |
|
|
|
truncated = truncate(title, truncation) |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
node, 'text', |
|
|
|
node, |
|
|
|
|
|
|
|
'text', |
|
|
|
x=col * x_step + self.legend_box_size + 5, |
|
|
|
x=col * x_step + self.legend_box_size + 5, |
|
|
|
y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size |
|
|
|
y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size |
|
|
|
).text = truncated |
|
|
|
).text = truncated |
|
|
@ -418,19 +476,22 @@ class Graph(PublicApi): |
|
|
|
if self._title: |
|
|
|
if self._title: |
|
|
|
for i, title_line in enumerate(self._title, 1): |
|
|
|
for i, title_line in enumerate(self._title, 1): |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
self.nodes['title'], 'text', class_='title plot_title', |
|
|
|
self.nodes['title'], |
|
|
|
|
|
|
|
'text', |
|
|
|
|
|
|
|
class_='title plot_title', |
|
|
|
x=self.width / 2, |
|
|
|
x=self.width / 2, |
|
|
|
y=i * (self.style.title_font_size + self.spacing) |
|
|
|
y=i * (self.style.title_font_size + self.spacing) |
|
|
|
).text = title_line |
|
|
|
).text = title_line |
|
|
|
|
|
|
|
|
|
|
|
def _make_x_title(self): |
|
|
|
def _make_x_title(self): |
|
|
|
"""Make the X-Axis title""" |
|
|
|
"""Make the X-Axis title""" |
|
|
|
y = (self.height - self.margin_box.bottom + |
|
|
|
y = (self.height - self.margin_box.bottom + self._x_labels_height) |
|
|
|
self._x_labels_height) |
|
|
|
|
|
|
|
if self._x_title: |
|
|
|
if self._x_title: |
|
|
|
for i, title_line in enumerate(self._x_title, 1): |
|
|
|
for i, title_line in enumerate(self._x_title, 1): |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
self.nodes['title'], 'text', class_='title', |
|
|
|
self.nodes['title'], |
|
|
|
|
|
|
|
'text', |
|
|
|
|
|
|
|
class_='title', |
|
|
|
x=self.margin_box.left + self.view.width / 2, |
|
|
|
x=self.margin_box.left + self.view.width / 2, |
|
|
|
y=y + i * (self.style.title_font_size + self.spacing) |
|
|
|
y=y + i * (self.style.title_font_size + self.spacing) |
|
|
|
) |
|
|
|
) |
|
|
@ -442,12 +503,15 @@ class Graph(PublicApi): |
|
|
|
yc = self.margin_box.top + self.view.height / 2 |
|
|
|
yc = self.margin_box.top + self.view.height / 2 |
|
|
|
for i, title_line in enumerate(self._y_title, 1): |
|
|
|
for i, title_line in enumerate(self._y_title, 1): |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
self.nodes['title'], 'text', class_='title', |
|
|
|
self.nodes['title'], |
|
|
|
|
|
|
|
'text', |
|
|
|
|
|
|
|
class_='title', |
|
|
|
x=self._legend_at_left_width, |
|
|
|
x=self._legend_at_left_width, |
|
|
|
y=i * (self.style.title_font_size + self.spacing) + yc |
|
|
|
y=i * (self.style.title_font_size + self.spacing) + yc |
|
|
|
) |
|
|
|
) |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
text.attrib['transform'] = "rotate(%d %f %f)" % ( |
|
|
|
-90, self._legend_at_left_width, yc) |
|
|
|
-90, self._legend_at_left_width, yc |
|
|
|
|
|
|
|
) |
|
|
|
text.text = title_line |
|
|
|
text.text = title_line |
|
|
|
|
|
|
|
|
|
|
|
def _interpolate(self, xs, ys): |
|
|
|
def _interpolate(self, xs, ys): |
|
|
@ -461,16 +525,19 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
interpolate = INTERPOLATIONS[self.interpolate] |
|
|
|
interpolate = INTERPOLATIONS[self.interpolate] |
|
|
|
|
|
|
|
|
|
|
|
return list(interpolate( |
|
|
|
return list( |
|
|
|
x, y, self.interpolation_precision, |
|
|
|
interpolate( |
|
|
|
**self.interpolation_parameters)) |
|
|
|
x, y, self.interpolation_precision, |
|
|
|
|
|
|
|
**self.interpolation_parameters |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _rescale(self, points): |
|
|
|
def _rescale(self, points): |
|
|
|
"""Scale for secondary""" |
|
|
|
"""Scale for secondary""" |
|
|
|
return [ |
|
|
|
return [( |
|
|
|
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale |
|
|
|
x, self._scale_diff + (y - self._scale_min_2nd) * self._scale |
|
|
|
if y is not None else None) |
|
|
|
if y is not None else None |
|
|
|
for x, y in points] |
|
|
|
) for x, y in points] |
|
|
|
|
|
|
|
|
|
|
|
def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None): |
|
|
|
def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None): |
|
|
|
"""Insert in desc tags informations for the javascript tooltip""" |
|
|
|
"""Insert in desc tags informations for the javascript tooltip""" |
|
|
@ -483,16 +550,21 @@ class Graph(PublicApi): |
|
|
|
classes.append('top') |
|
|
|
classes.append('top') |
|
|
|
classes = ' '.join(classes) |
|
|
|
classes = ' '.join(classes) |
|
|
|
|
|
|
|
|
|
|
|
self.svg.node(node, 'desc', |
|
|
|
self.svg.node(node, 'desc', class_="x " + classes).text = to_str(x) |
|
|
|
class_="x " + classes).text = to_str(x) |
|
|
|
self.svg.node(node, 'desc', class_="y " + classes).text = to_str(y) |
|
|
|
self.svg.node(node, 'desc', |
|
|
|
|
|
|
|
class_="y " + classes).text = to_str(y) |
|
|
|
|
|
|
|
if xlabel: |
|
|
|
if xlabel: |
|
|
|
self.svg.node(node, 'desc', |
|
|
|
self.svg.node(node, 'desc', class_="x_label").text = to_str(xlabel) |
|
|
|
class_="x_label").text = to_str(xlabel) |
|
|
|
|
|
|
|
|
|
|
|
def _static_value( |
|
|
|
def _static_value(self, serie_node, value, x, y, metadata, |
|
|
|
self, |
|
|
|
align_text='left', classes=None): |
|
|
|
serie_node, |
|
|
|
|
|
|
|
value, |
|
|
|
|
|
|
|
x, |
|
|
|
|
|
|
|
y, |
|
|
|
|
|
|
|
metadata, |
|
|
|
|
|
|
|
align_text='left', |
|
|
|
|
|
|
|
classes=None |
|
|
|
|
|
|
|
): |
|
|
|
"""Write the print value""" |
|
|
|
"""Write the print value""" |
|
|
|
label = metadata and metadata.get('label') |
|
|
|
label = metadata and metadata.get('label') |
|
|
|
classes = classes and [classes] or [] |
|
|
|
classes = classes and [classes] or [] |
|
|
@ -502,7 +574,8 @@ class Graph(PublicApi): |
|
|
|
if self.print_values: |
|
|
|
if self.print_values: |
|
|
|
y -= self.style.value_font_size / 2 |
|
|
|
y -= self.style.value_font_size / 2 |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
serie_node['text_overlay'], 'text', |
|
|
|
serie_node['text_overlay'], |
|
|
|
|
|
|
|
'text', |
|
|
|
class_=' '.join(label_cls), |
|
|
|
class_=' '.join(label_cls), |
|
|
|
x=x, |
|
|
|
x=x, |
|
|
|
y=y + self.style.value_font_size / 3 |
|
|
|
y=y + self.style.value_font_size / 3 |
|
|
@ -515,11 +588,14 @@ class Graph(PublicApi): |
|
|
|
val_cls.append('showable') |
|
|
|
val_cls.append('showable') |
|
|
|
|
|
|
|
|
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
serie_node['text_overlay'], 'text', |
|
|
|
serie_node['text_overlay'], |
|
|
|
|
|
|
|
'text', |
|
|
|
class_=' '.join(val_cls), |
|
|
|
class_=' '.join(val_cls), |
|
|
|
x=x, |
|
|
|
x=x, |
|
|
|
y=y + self.style.value_font_size / 3, |
|
|
|
y=y + self.style.value_font_size / 3, |
|
|
|
attrib={'text-anchor': align_text} |
|
|
|
attrib={ |
|
|
|
|
|
|
|
'text-anchor': align_text |
|
|
|
|
|
|
|
} |
|
|
|
).text = value if self.print_zeroes or value != '0' else '' |
|
|
|
).text = value if self.print_zeroes or value != '0' else '' |
|
|
|
|
|
|
|
|
|
|
|
def _points(self, x_pos): |
|
|
|
def _points(self, x_pos): |
|
|
@ -528,9 +604,7 @@ class Graph(PublicApi): |
|
|
|
and interpolated points if interpolate option is specified |
|
|
|
and interpolated points if interpolate option is specified |
|
|
|
""" |
|
|
|
""" |
|
|
|
for serie in self.all_series: |
|
|
|
for serie in self.all_series: |
|
|
|
serie.points = [ |
|
|
|
serie.points = [(x_pos[i], v) for i, v in enumerate(serie.values)] |
|
|
|
(x_pos[i], v) |
|
|
|
|
|
|
|
for i, v in enumerate(serie.values)] |
|
|
|
|
|
|
|
if serie.points and self.interpolate: |
|
|
|
if serie.points and self.interpolate: |
|
|
|
serie.interpolated = self._interpolate(x_pos, serie.values) |
|
|
|
serie.interpolated = self._interpolate(x_pos, serie.values) |
|
|
|
else: |
|
|
|
else: |
|
|
@ -600,33 +674,18 @@ class Graph(PublicApi): |
|
|
|
value = serie.values[i] |
|
|
|
value = serie.values[i] |
|
|
|
metadata = serie.metadata.get(i) |
|
|
|
metadata = serie.metadata.get(i) |
|
|
|
|
|
|
|
|
|
|
|
kwargs = { |
|
|
|
kwargs = {'chart': self, 'serie': serie, 'index': i} |
|
|
|
'chart': self, |
|
|
|
formatter = ((metadata and metadata.get('formatter')) |
|
|
|
'serie': serie, |
|
|
|
or serie.formatter or self.formatter |
|
|
|
'index': i |
|
|
|
or self._value_format) |
|
|
|
} |
|
|
|
|
|
|
|
formatter = ( |
|
|
|
|
|
|
|
(metadata and metadata.get('formatter')) or |
|
|
|
|
|
|
|
serie.formatter or |
|
|
|
|
|
|
|
self.formatter or |
|
|
|
|
|
|
|
self._value_format |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
kwargs = filter_kwargs(formatter, kwargs) |
|
|
|
kwargs = filter_kwargs(formatter, kwargs) |
|
|
|
return formatter(value, **kwargs) |
|
|
|
return formatter(value, **kwargs) |
|
|
|
|
|
|
|
|
|
|
|
def _serie_format(self, serie, value): |
|
|
|
def _serie_format(self, serie, value): |
|
|
|
"""Format an independent value for the serie""" |
|
|
|
"""Format an independent value for the serie""" |
|
|
|
|
|
|
|
|
|
|
|
kwargs = { |
|
|
|
kwargs = {'chart': self, 'serie': serie, 'index': None} |
|
|
|
'chart': self, |
|
|
|
formatter = (serie.formatter or self.formatter or self._value_format) |
|
|
|
'serie': serie, |
|
|
|
|
|
|
|
'index': None |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
formatter = ( |
|
|
|
|
|
|
|
serie.formatter or |
|
|
|
|
|
|
|
self.formatter or |
|
|
|
|
|
|
|
self._value_format |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
kwargs = filter_kwargs(formatter, kwargs) |
|
|
|
kwargs = filter_kwargs(formatter, kwargs) |
|
|
|
return formatter(value, **kwargs) |
|
|
|
return formatter(value, **kwargs) |
|
|
|
|
|
|
|
|
|
|
@ -639,18 +698,24 @@ class Graph(PublicApi): |
|
|
|
for series_group in (self.series, self.secondary_series): |
|
|
|
for series_group in (self.series, self.secondary_series): |
|
|
|
if self.show_legend and series_group: |
|
|
|
if self.show_legend and series_group: |
|
|
|
h, w = get_texts_box( |
|
|
|
h, w = get_texts_box( |
|
|
|
map(lambda x: truncate(x, self.truncate_legend or 15), |
|
|
|
map( |
|
|
|
[serie.title['title'] |
|
|
|
lambda x: truncate(x, self.truncate_legend or 15), [ |
|
|
|
if isinstance(serie.title, dict) |
|
|
|
serie.title['title'] |
|
|
|
else serie.title or '' for serie in series_group]), |
|
|
|
if isinstance(serie.title, dict) else serie.title |
|
|
|
self.style.legend_font_size) |
|
|
|
or '' for serie in series_group |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
), self.style.legend_font_size |
|
|
|
|
|
|
|
) |
|
|
|
if self.legend_at_bottom: |
|
|
|
if self.legend_at_bottom: |
|
|
|
h_max = max(h, self.legend_box_size) |
|
|
|
h_max = max(h, self.legend_box_size) |
|
|
|
cols = (self._order // self.legend_at_bottom_columns |
|
|
|
cols = ( |
|
|
|
if self.legend_at_bottom_columns |
|
|
|
self._order // self.legend_at_bottom_columns |
|
|
|
else ceil(sqrt(self._order)) or 1) |
|
|
|
if self.legend_at_bottom_columns else |
|
|
|
|
|
|
|
ceil(sqrt(self._order)) or 1 |
|
|
|
|
|
|
|
) |
|
|
|
self.margin_box.bottom += self.spacing + h_max * round( |
|
|
|
self.margin_box.bottom += self.spacing + h_max * round( |
|
|
|
cols - 1) * 1.5 + h_max |
|
|
|
cols - 1 |
|
|
|
|
|
|
|
) * 1.5 + h_max |
|
|
|
else: |
|
|
|
else: |
|
|
|
if series_group is self.series: |
|
|
|
if series_group is self.series: |
|
|
|
legend_width = self.spacing + w + self.legend_box_size |
|
|
|
legend_width = self.spacing + w + self.legend_box_size |
|
|
@ -658,18 +723,22 @@ class Graph(PublicApi): |
|
|
|
self._legend_at_left_width += legend_width |
|
|
|
self._legend_at_left_width += legend_width |
|
|
|
else: |
|
|
|
else: |
|
|
|
self.margin_box.right += ( |
|
|
|
self.margin_box.right += ( |
|
|
|
self.spacing + w + self.legend_box_size) |
|
|
|
self.spacing + w + self.legend_box_size |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self._x_labels_height = 0 |
|
|
|
self._x_labels_height = 0 |
|
|
|
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels: |
|
|
|
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels: |
|
|
|
for xlabels in (self._x_labels, self._x_2nd_labels): |
|
|
|
for xlabels in (self._x_labels, self._x_2nd_labels): |
|
|
|
if xlabels: |
|
|
|
if xlabels: |
|
|
|
h, w = get_texts_box( |
|
|
|
h, w = get_texts_box( |
|
|
|
map(lambda x: truncate(x, self.truncate_label or 25), |
|
|
|
map( |
|
|
|
cut(xlabels)), |
|
|
|
lambda x: truncate(x, self.truncate_label or 25), |
|
|
|
self.style.label_font_size) |
|
|
|
cut(xlabels) |
|
|
|
|
|
|
|
), self.style.label_font_size |
|
|
|
|
|
|
|
) |
|
|
|
self._x_labels_height = self.spacing + max( |
|
|
|
self._x_labels_height = self.spacing + max( |
|
|
|
w * abs(sin(rad(self.x_label_rotation))), h) |
|
|
|
w * abs(sin(rad(self.x_label_rotation))), h |
|
|
|
|
|
|
|
) |
|
|
|
if xlabels is self._x_labels: |
|
|
|
if xlabels is self._x_labels: |
|
|
|
self.margin_box.bottom += self._x_labels_height |
|
|
|
self.margin_box.bottom += self._x_labels_height |
|
|
|
else: |
|
|
|
else: |
|
|
@ -678,26 +747,32 @@ class Graph(PublicApi): |
|
|
|
if self.x_label_rotation % 180 < 90: |
|
|
|
if self.x_label_rotation % 180 < 90: |
|
|
|
self.margin_box.right = max( |
|
|
|
self.margin_box.right = max( |
|
|
|
w * abs(cos(rad(self.x_label_rotation))), |
|
|
|
w * abs(cos(rad(self.x_label_rotation))), |
|
|
|
self.margin_box.right) |
|
|
|
self.margin_box.right |
|
|
|
|
|
|
|
) |
|
|
|
else: |
|
|
|
else: |
|
|
|
self.margin_box.left = max( |
|
|
|
self.margin_box.left = max( |
|
|
|
w * abs(cos(rad(self.x_label_rotation))), |
|
|
|
w * abs(cos(rad(self.x_label_rotation))), |
|
|
|
self.margin_box.left) |
|
|
|
self.margin_box.left |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if self.show_y_labels: |
|
|
|
if self.show_y_labels: |
|
|
|
for ylabels in (self._y_labels, self._y_2nd_labels): |
|
|
|
for ylabels in (self._y_labels, self._y_2nd_labels): |
|
|
|
if ylabels: |
|
|
|
if ylabels: |
|
|
|
h, w = get_texts_box( |
|
|
|
h, w = get_texts_box( |
|
|
|
cut(ylabels), self.style.label_font_size) |
|
|
|
cut(ylabels), self.style.label_font_size |
|
|
|
|
|
|
|
) |
|
|
|
if ylabels is self._y_labels: |
|
|
|
if ylabels is self._y_labels: |
|
|
|
self.margin_box.left += self.spacing + max( |
|
|
|
self.margin_box.left += self.spacing + max( |
|
|
|
w * abs(cos(rad(self.y_label_rotation))), h) |
|
|
|
w * abs(cos(rad(self.y_label_rotation))), h |
|
|
|
|
|
|
|
) |
|
|
|
else: |
|
|
|
else: |
|
|
|
self.margin_box.right += self.spacing + max( |
|
|
|
self.margin_box.right += self.spacing + max( |
|
|
|
w * abs(cos(rad(self.y_label_rotation))), h) |
|
|
|
w * abs(cos(rad(self.y_label_rotation))), h |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self._title = split_title( |
|
|
|
self._title = split_title( |
|
|
|
self.title, self.width, self.style.title_font_size) |
|
|
|
self.title, self.width, self.style.title_font_size |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if self.title: |
|
|
|
if self.title: |
|
|
|
h, _ = get_text_box(self._title[0], self.style.title_font_size) |
|
|
|
h, _ = get_text_box(self._title[0], self.style.title_font_size) |
|
|
@ -705,7 +780,8 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
self._x_title = split_title( |
|
|
|
self._x_title = split_title( |
|
|
|
self.x_title, self.width - self.margin_box.x, |
|
|
|
self.x_title, self.width - self.margin_box.x, |
|
|
|
self.style.title_font_size) |
|
|
|
self.style.title_font_size |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self._x_title_height = 0 |
|
|
|
self._x_title_height = 0 |
|
|
|
if self._x_title: |
|
|
|
if self._x_title: |
|
|
@ -716,7 +792,8 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
self._y_title = split_title( |
|
|
|
self._y_title = split_title( |
|
|
|
self.y_title, self.height - self.margin_box.y, |
|
|
|
self.y_title, self.height - self.margin_box.y, |
|
|
|
self.style.title_font_size) |
|
|
|
self.style.title_font_size |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self._y_title_height = 0 |
|
|
|
self._y_title_height = 0 |
|
|
|
if self._y_title: |
|
|
|
if self._y_title: |
|
|
@ -741,15 +818,16 @@ class Graph(PublicApi): |
|
|
|
ci['point_estimate'] = value |
|
|
|
ci['point_estimate'] = value |
|
|
|
|
|
|
|
|
|
|
|
low, high = getattr( |
|
|
|
low, high = getattr( |
|
|
|
stats, |
|
|
|
stats, 'confidence_interval_%s' % ci.get('type', 'manual') |
|
|
|
'confidence_interval_%s' % ci.get('type', 'manual') |
|
|
|
|
|
|
|
)(**ci) |
|
|
|
)(**ci) |
|
|
|
|
|
|
|
|
|
|
|
self.svg.confidence_interval( |
|
|
|
self.svg.confidence_interval( |
|
|
|
node, x, |
|
|
|
node, |
|
|
|
|
|
|
|
x, |
|
|
|
# Respect some charts y modifications (pyramid, stackbar) |
|
|
|
# Respect some charts y modifications (pyramid, stackbar) |
|
|
|
y + (self.view.y(low) - self.view.y(value)), |
|
|
|
y + (self.view.y(low) - self.view.y(value)), |
|
|
|
y + (self.view.y(high) - self.view.y(value))) |
|
|
|
y + (self.view.y(high) - self.view.y(value)) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _legends(self): |
|
|
|
def _legends(self): |
|
|
@ -764,54 +842,59 @@ class Graph(PublicApi): |
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _values(self): |
|
|
|
def _values(self): |
|
|
|
"""Getter for series values (flattened)""" |
|
|
|
"""Getter for series values (flattened)""" |
|
|
|
return [val |
|
|
|
return [ |
|
|
|
for serie in self.series |
|
|
|
val for serie in self.series for val in serie.values |
|
|
|
for val in serie.values |
|
|
|
if val is not None |
|
|
|
if val is not None] |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _secondary_values(self): |
|
|
|
def _secondary_values(self): |
|
|
|
"""Getter for secondary series values (flattened)""" |
|
|
|
"""Getter for secondary series values (flattened)""" |
|
|
|
return [val |
|
|
|
return [ |
|
|
|
for serie in self.secondary_series |
|
|
|
val for serie in self.secondary_series for val in serie.values |
|
|
|
for val in serie.values |
|
|
|
if val is not None |
|
|
|
if val is not None] |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _len(self): |
|
|
|
def _len(self): |
|
|
|
"""Getter for the maximum series size""" |
|
|
|
"""Getter for the maximum series size""" |
|
|
|
return max([ |
|
|
|
return max([len(serie.values) for serie in self.all_series] or [0]) |
|
|
|
len(serie.values) |
|
|
|
|
|
|
|
for serie in self.all_series] or [0]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _secondary_min(self): |
|
|
|
def _secondary_min(self): |
|
|
|
"""Getter for the minimum series value""" |
|
|
|
"""Getter for the minimum series value""" |
|
|
|
return (self.secondary_range[0] if ( |
|
|
|
return ( |
|
|
|
self.secondary_range and self.secondary_range[0] is not None) |
|
|
|
self.secondary_range[0] |
|
|
|
else (min(self._secondary_values) |
|
|
|
if (self.secondary_range |
|
|
|
if self._secondary_values else None)) |
|
|
|
and self.secondary_range[0] is not None) else |
|
|
|
|
|
|
|
(min(self._secondary_values) if self._secondary_values else None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _min(self): |
|
|
|
def _min(self): |
|
|
|
"""Getter for the minimum series value""" |
|
|
|
"""Getter for the minimum series value""" |
|
|
|
return (self.range[0] if (self.range and self.range[0] is not None) |
|
|
|
return ( |
|
|
|
else (min(self._values) |
|
|
|
self.range[0] if (self.range and self.range[0] is not None) else |
|
|
|
if self._values else None)) |
|
|
|
(min(self._values) if self._values else None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _max(self): |
|
|
|
def _max(self): |
|
|
|
"""Getter for the maximum series value""" |
|
|
|
"""Getter for the maximum series value""" |
|
|
|
return (self.range[1] if (self.range and self.range[1] is not None) |
|
|
|
return ( |
|
|
|
else (max(self._values) if self._values else None)) |
|
|
|
self.range[1] if (self.range and self.range[1] is not None) else |
|
|
|
|
|
|
|
(max(self._values) if self._values else None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _secondary_max(self): |
|
|
|
def _secondary_max(self): |
|
|
|
"""Getter for the maximum series value""" |
|
|
|
"""Getter for the maximum series value""" |
|
|
|
return (self.secondary_range[1] if ( |
|
|
|
return ( |
|
|
|
self.secondary_range and self.secondary_range[1] is not None) |
|
|
|
self.secondary_range[1] |
|
|
|
else (max(self._secondary_values) |
|
|
|
if (self.secondary_range |
|
|
|
if self._secondary_values else None)) |
|
|
|
and self.secondary_range[1] is not None) else |
|
|
|
|
|
|
|
(max(self._secondary_values) if self._secondary_values else None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _order(self): |
|
|
|
def _order(self): |
|
|
@ -825,13 +908,18 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
def _compute_x_labels(self): |
|
|
|
def _compute_x_labels(self): |
|
|
|
self._x_labels = self.x_labels and list( |
|
|
|
self._x_labels = self.x_labels and list( |
|
|
|
zip(map(self._x_label_format_if_value, self.x_labels), |
|
|
|
zip( |
|
|
|
self._x_pos)) |
|
|
|
map(self._x_label_format_if_value, self.x_labels), self._x_pos |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _compute_x_labels_major(self): |
|
|
|
def _compute_x_labels_major(self): |
|
|
|
if self.x_labels_major_every: |
|
|
|
if self.x_labels_major_every: |
|
|
|
self._x_labels_major = [self._x_labels[i][0] for i in range( |
|
|
|
self._x_labels_major = [ |
|
|
|
0, len(self._x_labels), self.x_labels_major_every)] |
|
|
|
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: |
|
|
|
elif self.x_labels_major_count: |
|
|
|
label_count = len(self._x_labels) |
|
|
|
label_count = len(self._x_labels) |
|
|
@ -840,17 +928,20 @@ class Graph(PublicApi): |
|
|
|
self._x_labels_major = [label[0] for label in self._x_labels] |
|
|
|
self._x_labels_major = [label[0] for label in self._x_labels] |
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
|
else: |
|
|
|
self._x_labels_major = [self._x_labels[ |
|
|
|
self._x_labels_major = [ |
|
|
|
int(i * (label_count - 1) / (major_count - 1))][0] |
|
|
|
self._x_labels[int( |
|
|
|
for i in range(major_count)] |
|
|
|
i * (label_count - 1) / (major_count - 1) |
|
|
|
|
|
|
|
)][0] for i in range(major_count) |
|
|
|
|
|
|
|
] |
|
|
|
else: |
|
|
|
else: |
|
|
|
self._x_labels_major = self.x_labels_major and list( |
|
|
|
self._x_labels_major = self.x_labels_major and list( |
|
|
|
map(self._x_label_format_if_value, self.x_labels_major)) or [] |
|
|
|
map(self._x_label_format_if_value, self.x_labels_major) |
|
|
|
|
|
|
|
) or [] |
|
|
|
|
|
|
|
|
|
|
|
def _compute_y_labels(self): |
|
|
|
def _compute_y_labels(self): |
|
|
|
y_pos = compute_scale( |
|
|
|
y_pos = compute_scale( |
|
|
|
self._box.ymin, self._box.ymax, self.logarithmic, |
|
|
|
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, |
|
|
|
self.order_min, self.min_scale, self.max_scale |
|
|
|
self.min_scale, self.max_scale |
|
|
|
) |
|
|
|
) |
|
|
|
if self.y_labels: |
|
|
|
if self.y_labels: |
|
|
|
self._y_labels = [] |
|
|
|
self._y_labels = [] |
|
|
@ -872,8 +963,11 @@ class Graph(PublicApi): |
|
|
|
|
|
|
|
|
|
|
|
def _compute_y_labels_major(self): |
|
|
|
def _compute_y_labels_major(self): |
|
|
|
if self.y_labels_major_every: |
|
|
|
if self.y_labels_major_every: |
|
|
|
self._y_labels_major = [self._y_labels[i][1] for i in range( |
|
|
|
self._y_labels_major = [ |
|
|
|
0, len(self._y_labels), self.y_labels_major_every)] |
|
|
|
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: |
|
|
|
elif self.y_labels_major_count: |
|
|
|
label_count = len(self._y_labels) |
|
|
|
label_count = len(self._y_labels) |
|
|
@ -881,9 +975,11 @@ class Graph(PublicApi): |
|
|
|
if (major_count >= label_count): |
|
|
|
if (major_count >= label_count): |
|
|
|
self._y_labels_major = [label[1] for label in self._y_labels] |
|
|
|
self._y_labels_major = [label[1] for label in self._y_labels] |
|
|
|
else: |
|
|
|
else: |
|
|
|
self._y_labels_major = [self._y_labels[ |
|
|
|
self._y_labels_major = [ |
|
|
|
int(i * (label_count - 1) / (major_count - 1))][1] |
|
|
|
self._y_labels[int( |
|
|
|
for i in range(major_count)] |
|
|
|
i * (label_count - 1) / (major_count - 1) |
|
|
|
|
|
|
|
)][1] for i in range(major_count) |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
elif self.y_labels_major: |
|
|
|
elif self.y_labels_major: |
|
|
|
self._y_labels_major = list(map(self._adapt, self.y_labels_major)) |
|
|
|
self._y_labels_major = list(map(self._adapt, self.y_labels_major)) |
|
|
@ -902,19 +998,22 @@ class Graph(PublicApi): |
|
|
|
for line in range(x_lines): |
|
|
|
for line in range(x_lines): |
|
|
|
_current_x += (self.width - self.margin_box.x) / squares[0] |
|
|
|
_current_x += (self.width - self.margin_box.x) / squares[0] |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
self.nodes['plot'], 'path', |
|
|
|
self.nodes['plot'], |
|
|
|
|
|
|
|
'path', |
|
|
|
class_='bg-lines', |
|
|
|
class_='bg-lines', |
|
|
|
d='M%s %s L%s %s' % ( |
|
|
|
d='M%s %s L%s %s' % |
|
|
|
_current_x, 0, _current_x, |
|
|
|
(_current_x, 0, _current_x, self.height - self.margin_box.y) |
|
|
|
self.height - self.margin_box.y)) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
for line in range(y_lines): |
|
|
|
for line in range(y_lines): |
|
|
|
_current_y += (self.height - self.margin_box.y) / squares[1] |
|
|
|
_current_y += (self.height - self.margin_box.y) / squares[1] |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
self.nodes['plot'], 'path', |
|
|
|
self.nodes['plot'], |
|
|
|
|
|
|
|
'path', |
|
|
|
class_='bg-lines', |
|
|
|
class_='bg-lines', |
|
|
|
d='M%s %s L%s %s' % ( |
|
|
|
d='M%s %s L%s %s' % |
|
|
|
0, _current_y, self.width - self.margin_box.x, _current_y)) |
|
|
|
(0, _current_y, self.width - self.margin_box.x, _current_y) |
|
|
|
|
|
|
|
) |
|
|
|
return ((self.width - self.margin_box.x) / squares[0], |
|
|
|
return ((self.width - self.margin_box.x) / squares[0], |
|
|
|
(self.height - self.margin_box.y) / squares[1]) |
|
|
|
(self.height - self.margin_box.y) / squares[1]) |
|
|
|
|
|
|
|
|
|
|
@ -938,8 +1037,8 @@ class Graph(PublicApi): |
|
|
|
"""Check if there is any data""" |
|
|
|
"""Check if there is any data""" |
|
|
|
return any([ |
|
|
|
return any([ |
|
|
|
len([ |
|
|
|
len([ |
|
|
|
v for a in (s[0] if is_list_like(s) else [s]) |
|
|
|
v |
|
|
|
for v in (a if is_list_like(a) else [a]) |
|
|
|
for a in (s[0] if is_list_like(s) else [s]) |
|
|
|
if v is not None]) |
|
|
|
for v in (a if is_list_like(a) else [a]) if v is not None |
|
|
|
for s in self.raw_series |
|
|
|
]) for s in self.raw_series |
|
|
|
]) |
|
|
|
]) |
|
|
|