|
|
@ -25,8 +25,9 @@ from __future__ import division |
|
|
|
from pygal.interpolate import interpolation |
|
|
|
from pygal.interpolate import interpolation |
|
|
|
from pygal.graph.base import BaseGraph |
|
|
|
from pygal.graph.base import BaseGraph |
|
|
|
from pygal.view import View, LogView, XYLogView |
|
|
|
from pygal.view import View, LogView, XYLogView |
|
|
|
from pygal.util import is_major, truncate, reverse_text_len |
|
|
|
from pygal.util import is_major, truncate, reverse_text_len, get_texts_box, cut, rad |
|
|
|
from math import isnan, pi, sqrt, ceil |
|
|
|
from math import isnan, pi, sqrt, ceil, cos |
|
|
|
|
|
|
|
from itertools import repeat, izip, chain, count |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Graph(BaseGraph): |
|
|
|
class Graph(BaseGraph): |
|
|
@ -60,6 +61,8 @@ class Graph(BaseGraph): |
|
|
|
self.height - self.margin.y, |
|
|
|
self.height - self.margin.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( |
|
|
@ -128,18 +131,21 @@ class Graph(BaseGraph): |
|
|
|
self.svg.node(axis, 'path', |
|
|
|
self.svg.node(axis, 'path', |
|
|
|
d='M%f %f v%f' % (0, 0, self.view.height), |
|
|
|
d='M%f %f v%f' % (0, 0, self.view.height), |
|
|
|
class_='line') |
|
|
|
class_='line') |
|
|
|
|
|
|
|
lastlabel = self._x_labels[-1][0] |
|
|
|
for label, position in self._x_labels: |
|
|
|
for label, position in self._x_labels: |
|
|
|
major = is_major(position) |
|
|
|
major = is_major(position) |
|
|
|
guides = self.svg.node(axis, class_='guides') |
|
|
|
guides = self.svg.node(axis, class_='guides') |
|
|
|
x = self.view.x(position) |
|
|
|
x = self.view.x(position) |
|
|
|
y = self.view.height + 5 |
|
|
|
y = self.view.height + 5 |
|
|
|
if draw_axes: |
|
|
|
if draw_axes: |
|
|
|
|
|
|
|
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, 0, self.view.height), |
|
|
|
d='M%f %f v%f' % (x, 0, self.view.height), |
|
|
|
class_='%s%sline' % ( |
|
|
|
class_='%s%sline' % ( |
|
|
|
'major ' if major else '', |
|
|
|
'major ' if major else '', |
|
|
|
'guide ' if position != 0 else '')) |
|
|
|
'guide ' if position != 0 and not last_guide |
|
|
|
|
|
|
|
else '')) |
|
|
|
y += .5 * self.label_font_size + 5 |
|
|
|
y += .5 * self.label_font_size + 5 |
|
|
|
text = self.svg.node( |
|
|
|
text = self.svg.node( |
|
|
|
guides, 'text', |
|
|
|
guides, 'text', |
|
|
@ -192,6 +198,37 @@ class Graph(BaseGraph): |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: |
|
|
|
|
|
|
|
# - shall we do separate axis 2y node, or use the above (and have an inner |
|
|
|
|
|
|
|
# loop condition) |
|
|
|
|
|
|
|
# - it |
|
|
|
|
|
|
|
# 10 is a magic number around here - margin size, don't know, |
|
|
|
|
|
|
|
# what stands for the additional 2 px |
|
|
|
|
|
|
|
if self._y_2nd_labels: |
|
|
|
|
|
|
|
secondary_ax = self.svg.node(self.nodes['plot'], class_="axis 2y") |
|
|
|
|
|
|
|
#self.svg.node(secondary_ax, 'path', |
|
|
|
|
|
|
|
# d='M%f %f v%f' % (self.view.width-12, 0, self.view.height), |
|
|
|
|
|
|
|
# class_='major line' |
|
|
|
|
|
|
|
#) |
|
|
|
|
|
|
|
for label, position in self._y_2nd_labels: |
|
|
|
|
|
|
|
major = is_major(position) |
|
|
|
|
|
|
|
# it is needed, to have the same structure |
|
|
|
|
|
|
|
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, |
|
|
|
|
|
|
|
# XXX: plus or minus? |
|
|
|
|
|
|
|
y = y + .35 * self.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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legend(self): |
|
|
|
def _legend(self): |
|
|
|
"""Make the legend box""" |
|
|
|
"""Make the legend box""" |
|
|
|
if not self.show_legend: |
|
|
|
if not self.show_legend: |
|
|
@ -209,7 +246,15 @@ class Graph(BaseGraph): |
|
|
|
truncation = reverse_text_len( |
|
|
|
truncation = reverse_text_len( |
|
|
|
available_space, self.legend_font_size) |
|
|
|
available_space, self.legend_font_size) |
|
|
|
else: |
|
|
|
else: |
|
|
|
x = self.margin.left + self.view.width + 10 |
|
|
|
# draw primary y axis on left |
|
|
|
|
|
|
|
x = 0 |
|
|
|
|
|
|
|
h, w = get_texts_box( |
|
|
|
|
|
|
|
cut(self._y_labels), self.label_font_size) |
|
|
|
|
|
|
|
#x -= 10 + max(w * cos(rad(self.y_label_rotation)), h) |
|
|
|
|
|
|
|
x -= 10 + w |
|
|
|
|
|
|
|
h, w = get_texts_box(self._legends + self._secondary_legends, self.label_font_size) |
|
|
|
|
|
|
|
x -= w |
|
|
|
|
|
|
|
|
|
|
|
y = self.margin.top + 10 |
|
|
|
y = self.margin.top + 10 |
|
|
|
cols = 1 |
|
|
|
cols = 1 |
|
|
|
if not truncation: |
|
|
|
if not truncation: |
|
|
@ -219,15 +264,44 @@ class Graph(BaseGraph): |
|
|
|
self.nodes['graph'], class_='legends', |
|
|
|
self.nodes['graph'], class_='legends', |
|
|
|
transform='translate(%d, %d)' % (x, y)) |
|
|
|
transform='translate(%d, %d)' % (x, y)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
h = max(self.legend_box_size, self.legend_font_size) |
|
|
|
h = max(self.legend_box_size, self.legend_font_size) |
|
|
|
x_step = self.view.width / cols |
|
|
|
x_step = self.view.width / cols |
|
|
|
for i, title in enumerate(self._legends): |
|
|
|
if self.legend_at_bottom: |
|
|
|
|
|
|
|
# if legends at the bottom, we dont split the windows |
|
|
|
|
|
|
|
counter = count() |
|
|
|
|
|
|
|
# gen structure - (i, (j, (l, tf))) |
|
|
|
|
|
|
|
gen = enumerate(enumerate(chain( |
|
|
|
|
|
|
|
izip(self._legends, repeat(False)), |
|
|
|
|
|
|
|
izip(self._secondary_legends, repeat(True))))) |
|
|
|
|
|
|
|
secondary_legends = legends |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
gen = enumerate(chain( |
|
|
|
|
|
|
|
enumerate(izip(self._legends, repeat(False))), |
|
|
|
|
|
|
|
enumerate(izip(self._secondary_legends, repeat(True))))) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# draw secondary axis on right |
|
|
|
|
|
|
|
x = self.margin.left + self.view.width + 10 |
|
|
|
|
|
|
|
if self._y_2nd_labels: |
|
|
|
|
|
|
|
h, w = get_texts_box( |
|
|
|
|
|
|
|
cut(self._y_labels), self.label_font_size) |
|
|
|
|
|
|
|
x += 10 + max(w * cos(rad(self.y_label_rotation)), h) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
y = self.margin.top + 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
secondary_legends = self.svg.node( |
|
|
|
|
|
|
|
self.nodes['graph'], class_='legends', |
|
|
|
|
|
|
|
transform='translate(%d, %d)' % (x, y)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (global_serie_number, (i, (title, is_secondary))) in gen: |
|
|
|
|
|
|
|
|
|
|
|
col = i % cols |
|
|
|
col = i % cols |
|
|
|
row = i // cols |
|
|
|
row = i // cols |
|
|
|
|
|
|
|
|
|
|
|
legend = self.svg.node( |
|
|
|
legend = self.svg.node( |
|
|
|
legends, class_='legend reactive activate-serie', |
|
|
|
secondary_legends if is_secondary else legends, |
|
|
|
id="activate-serie-%d" % i) |
|
|
|
class_='legend reactive activate-serie', |
|
|
|
|
|
|
|
id="activate-serie-%d" % global_serie_number) |
|
|
|
self.svg.node( |
|
|
|
self.svg.node( |
|
|
|
legend, 'rect', |
|
|
|
legend, 'rect', |
|
|
|
x=col * x_step, |
|
|
|
x=col * x_step, |
|
|
@ -237,7 +311,7 @@ class Graph(BaseGraph): |
|
|
|
) / 2, |
|
|
|
) / 2, |
|
|
|
width=self.legend_box_size, |
|
|
|
width=self.legend_box_size, |
|
|
|
height=self.legend_box_size, |
|
|
|
height=self.legend_box_size, |
|
|
|
class_="color-%d reactive" % (i % 16) |
|
|
|
class_="color-%d reactive" % (global_serie_number % 16) |
|
|
|
) |
|
|
|
) |
|
|
|
truncated = truncate(title, truncation) |
|
|
|
truncated = truncate(title, truncation) |
|
|
|
# Serious magical numbers here |
|
|
|
# Serious magical numbers here |
|
|
@ -327,7 +401,7 @@ class Graph(BaseGraph): |
|
|
|
return self._format(values[i][1]) |
|
|
|
return self._format(values[i][1]) |
|
|
|
|
|
|
|
|
|
|
|
def _points(self, x_pos): |
|
|
|
def _points(self, x_pos): |
|
|
|
for serie in self.series: |
|
|
|
for serie in self.series + self.secondary_series: |
|
|
|
serie.points = [ |
|
|
|
serie.points = [ |
|
|
|
(x_pos[i], v) |
|
|
|
(x_pos[i], v) |
|
|
|
for i, v in enumerate(serie.values)] |
|
|
|
for i, v in enumerate(serie.values)] |
|
|
|