Browse Source

Merge remote-tracking branch 'wiktorn/2ndary_axis'

pull/26/merge
Florian Mounier 12 years ago
parent
commit
70a6bcbf9d
  1. 8
      pygal/css/graph.css
  2. 17
      pygal/ghost.py
  3. 45
      pygal/graph/base.py
  4. 74
      pygal/graph/graph.py
  5. 51
      pygal/graph/line.py

8
pygal/css/graph.css

@ -49,8 +49,12 @@ text.no_data {
.axis.y text {
text-anchor: end;
}
.axis.y2 text {
text-anchor: start;
}
.axis.y .logarithmic text:not(.major) {
.axis.y .logarithmic text:not(.major) ,
.axis.y2 .logarithmic text:not(.major) {
font-size: 50%;
}
@ -66,11 +70,13 @@ text.no_data {
}
.horizontal .axis.y .guide.line,
.horizontal .axis.y2 .guide.line,
.vertical .axis.x .guide.line {
opacity: 0;
}
.axis.y .guides:hover .guide.line,
.axis.y2 .guides:hover .guide.line,
.line-graph .axis.x .guides:hover .guide.line,
.gauge-graph .axis.x .guides:hover .guide.line,
.stackedline-graph .axis.x .guides:hover .guide.line,

17
pygal/ghost.py

@ -55,20 +55,25 @@ class Ghost(object):
config(**kwargs)
self.config = config
self.raw_series = []
self.raw_series2 = []
def add(self, title, values):
def add(self, title, values, secondary=False):
"""Add a serie to this graph"""
if not hasattr(values, '__iter__') and not isinstance(values, dict):
values = [values]
self.raw_series.append((title, values))
if secondary:
self.raw_series2.append((title, values))
else:
self.raw_series.append((title, values))
def make_series(self):
return prepare_values(self.raw_series, self.config, self.cls)
def make_series(self, series):
return prepare_values(series, self.config, self.cls)
def make_instance(self):
self.config(**self.__dict__)
series = self.make_series()
self._last__inst = self.cls(self.config, series)
series = self.make_series(self.raw_series)
secondary_series = self.make_series(self.raw_series2)
self._last__inst = self.cls(self.config, series, secondary_series)
return self._last__inst
# Rendering

45
pygal/graph/base.py

@ -35,14 +35,16 @@ class BaseGraph(object):
_adapters = []
def __init__(self, config, series):
def __init__(self, config, series, secondary_series):
"""Init the graph"""
self.config = config
self.series = series or []
self.secondary_series = secondary_series or []
self.horizontal = getattr(self, 'horizontal', False)
self.svg = Svg(self)
self._x_labels = None
self._y_labels = None
self._y_2nd_labels = None
self.nodes = {}
self.margin = Margin(*([20] * 4))
self._box = Box()
@ -100,6 +102,18 @@ class BaseGraph(object):
h_max = max(h, self.legend_box_size)
self.margin.bottom += 10 + h_max * round(
sqrt(self._order) - 1) * 1.5 + h_max
else:
self.margin.left += 10 + w + self.legend_box_size
if self.show_legend and self.secondary_series:
h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15),
cut(self.secondary_series, 'title')),
self.legend_font_size)
if self.legend_at_bottom:
h_max = max(h, self.legend_box_size)
self.margin.bottom += 10 + h_max * round(
sqrt(self._order) - 1) * 1.5 + h_max
else:
self.margin.right += 10 + w + self.legend_box_size
@ -130,6 +144,11 @@ class BaseGraph(object):
"""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)"""
@ -138,10 +157,24 @@ class BaseGraph(object):
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.series] or [0])
return max([len(serie.values) for serie in self.series + self.secondary_series] or [0])
@cached_property
def _secondary_min(self):
"""Getter for the minimum series value"""
return (self.range and self.range[0]) or (
min(self._secondary_values) if self._secondary_values else None)
@cached_property
def _min(self):
@ -155,10 +188,16 @@ class BaseGraph(object):
return (self.range and self.range[1]) or (
max(self._values) if self._values else None)
@cached_property
def _secondary_max(self):
"""Getter for the maximum series value"""
return (self.range and self.range[1]) or (
max(self._secondary_values) if self._secondary_values else None)
@cached_property
def _order(self):
"""Getter for the maximum series value"""
return len(self.series)
return len(self.series + self.secondary_series)
def _draw(self):
"""Draw all the things"""

74
pygal/graph/graph.py

@ -25,8 +25,9 @@ from __future__ import division
from pygal.interpolate import interpolation
from pygal.graph.base import BaseGraph
from pygal.view import View, LogView, XYLogView
from pygal.util import is_major, truncate, reverse_text_len
from math import isnan, pi, sqrt, ceil
from pygal.util import is_major, truncate, reverse_text_len, get_texts_box, cut, rad
from math import isnan, pi, sqrt, ceil, cos
from itertools import repeat, izip, chain, count
class Graph(BaseGraph):
@ -128,18 +129,21 @@ class Graph(BaseGraph):
self.svg.node(axis, 'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line')
lastlabel = self._x_labels[-1][0]
for label, position in self._x_labels:
major = is_major(position)
guides = self.svg.node(axis, class_='guides')
x = self.view.x(position)
y = self.view.height + 5
if draw_axes:
last_guide = (self._y_2nd_labels and label == lastlabel)
self.svg.node(
guides, 'path',
d='M%f %f v%f' % (x, 0, self.view.height),
class_='%s%sline' % (
'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
text = self.svg.node(
guides, 'text',
@ -192,6 +196,25 @@ class Graph(BaseGraph):
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y)
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 = is_major(position)
# 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.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):
"""Make the legend box"""
if not self.show_legend:
@ -209,7 +232,7 @@ class Graph(BaseGraph):
truncation = reverse_text_len(
available_space, self.legend_font_size)
else:
x = self.margin.left + self.view.width + 10
x = 10
y = self.margin.top + 10
cols = 1
if not truncation:
@ -219,15 +242,48 @@ class Graph(BaseGraph):
self.nodes['graph'], class_='legends',
transform='translate(%d, %d)' % (x, y))
h = max(self.legend_box_size, self.legend_font_size)
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)))
# i - global serie number - used for coloring and identification
# j - position within current legend box
# l - label
# tf - whether it is secondary label
gen = enumerate(enumerate(chain(
izip(self._legends, repeat(False)),
izip(self._secondary_legends, repeat(True)))))
secondary_legends = legends # svg node is the same
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
row = i // cols
legend = self.svg.node(
legends, class_='legend reactive activate-serie',
id="activate-serie-%d" % i)
secondary_legends if is_secondary else legends,
class_='legend reactive activate-serie',
id="activate-serie-%d" % global_serie_number)
self.svg.node(
legend, 'rect',
x=col * x_step,
@ -237,7 +293,7 @@ class Graph(BaseGraph):
) / 2,
width=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)
# Serious magical numbers here
@ -327,7 +383,7 @@ class Graph(BaseGraph):
return self._format(values[i][1])
def _points(self, x_pos):
for serie in self.series:
for serie in self.series + self.secondary_series:
serie.points = [
(x_pos[i], v)
for i, v in enumerate(serie.values)]

51
pygal/graph/line.py

@ -48,9 +48,13 @@ class Line(Graph):
values +
[(values[-1][0], zero)])
def line(self, serie_node, serie):
def line(self, serie_node, serie, rescale=False):
"""Draw the line serie"""
view_values = map(self.view, serie.points)
if rescale and self.secondary_series:
points = list ((x, self._scale_diff+(y - self._scale_min_2nd) * self._scale) for x, y in serie.points)
else:
points = serie.points
view_values = map(self.view, points)
if self.show_dots:
for i, (x, y) in enumerate(view_values):
if None in (x, y):
@ -71,7 +75,11 @@ class Line(Graph):
val = self._get_value(serie.points, i)
self.svg.node(dots, 'circle', cx=x, cy=y, r=self.dots_size,
class_='dot reactive tooltip-trigger')
self._tooltip_data(dots, val, x, y)
self._tooltip_data(dots,
"%s: %s" % (self.x_labels[i], val) if self.x_labels and
self.x_labels_num_limit
else val,
x, y)
self._static_value(
serie_node, val,
x + self.value_font_size,
@ -87,12 +95,22 @@ class Line(Graph):
class_='line reactive' + (' nofill' if not self.fill else ''))
def _compute(self):
# X Labels
x_pos = [
x / (self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value
self._points(x_pos)
x_labels = zip(self.x_labels, x_pos)
if self.x_labels_num_limit and len(x_labels)>self.x_labels_num_limit:
step = (len(x_labels)-1)/(self.x_labels_num_limit-1)
x_labels = list(x_labels[int(i*step)] for i in range(self.x_labels_num_limit))
self._x_labels = self.x_labels and x_labels
# Y Label
if self.include_x_axis:
self._box.ymin = min(self._min, 0)
self._box.ymax = max(self._max, 0)
@ -104,15 +122,28 @@ class Line(Graph):
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
) if not self.y_labels else map(float, self.y_labels)
x_labels = zip(self.x_labels, x_pos)
if self.x_labels_num_limit and len(x_labels)>self.x_labels_num_limit:
step = (len(x_labels)-1)/(self.x_labels_num_limit-1)
x_labels = list(x_labels[int(i*step)] for i in range(self.x_labels_num_limit))
self._x_labels = self.x_labels and x_labels
self._y_labels = zip(map(self._format, y_pos), y_pos)
# secondary y axis support
if self.secondary_series:
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)
scale = right_range / (steps-1)
self._y_2nd_labels = list((self._format(ymin+i*scale), pos) for i, pos in enumerate(y_pos))
min_2nd = float(self._y_2nd_labels[0][0])
self._scale = left_range / right_range
self._scale_diff = y_pos[0]
self._scale_min_2nd = min_2nd
def _plot(self):
for index, serie in enumerate(self.series):
self.line(self._serie(index), serie)
for index, serie in enumerate(self.secondary_series, len(self.series)):
self.line(self._serie(index), serie, True)

Loading…
Cancel
Save