diff --git a/pygal/bar.py b/pygal/bar.py new file mode 100644 index 0000000..6f706c6 --- /dev/null +++ b/pygal/bar.py @@ -0,0 +1,81 @@ +from pygal import Serie, Margin, Label +from pygal.svg import Svg +from pygal.base import BaseGraph + + +class Bar(BaseGraph): + """Bar graph""" + + def __init__(self, width, height, precision=5, + format='g', style=None): + self.width = width + self.height = height + self.svg = Svg(width, height, style=style) + self.label_font_size = 12 + self.format = format + self.precision = precision + self.series = [] + self.x_labels = self.y_labels = self.title = None + + def add(self, title, values): + self.series.append( + Serie(title, values)) + + def _label(self, label): + return Label(('{:.%d%s}' % ( + self.precision, self.format)).format(label), label) + + def _y_labels(self, ymin, ymax): + step = (ymax - ymin) / 20. + if not step: + return [self._label(ymin)] + label = ymin + labels = [] + while label <= ymax: + labels.append(self._label(label)) + label += step + return labels + + def validate(self): + assert len(self.series) + if self.x_labels: + assert len(self.series[0].values) == len(self.x_labels) + for serie in self.series: + assert len(self.series[0].values) == len(serie.values) + + def draw(self): + self.validate() + x_step = len(self.series[0].values) + 1 + x_pos = [x / float(x_step) for x in range(x_step) + ] if x_step > 1 else [0, 1] # Center if only one value + x_ranges = zip(x_pos, x_pos[1:]) + + vals = [val for serie in self.series for val in serie.values] + margin = Margin(*(4 * [10])) + ymin, ymax = min(vals), max(vals) + if self.x_labels: + x_labels = [Label(label, sum(x_ranges[i]) / 2) + for i, label in enumerate(self.x_labels)] + y_labels = self.y_labels or self._y_labels(ymin, ymax) + series_labels = [serie.title for serie in self.series] + margin.left += 10 + max( + map(len, [l.label for l in y_labels])) * 0.6 * self.label_font_size + if self.x_labels: + margin.bottom += 10 + self.label_font_size + margin.right += 20 + max( + map(len, series_labels)) * 0.6 * self.label_font_size + margin.top += 10 + self.label_font_size + + # Actual drawing + self.svg.set_view(margin, ymin, ymax) + self.svg.graph(margin) + if self.x_labels: + self.svg.x_axis(x_labels) + self.svg.y_axis(y_labels) + self.svg.legend(margin, series_labels) + self.svg.title(margin, self.title) + for serie_index, serie in enumerate(self.series): + serie_node = self.svg.serie(serie_index) + self.svg.bar(serie_node, [ + ((x_ranges[i][0], v), (x_ranges[i][1], v)) + for i, v in enumerate(serie.values)]) diff --git a/pygal/svg.py b/pygal/svg.py index d7d364b..2b1d4d7 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -147,6 +147,20 @@ class Svg(object): self.node(serie, 'path', d='M%s L%s' % (origin, svg_values), class_='line') + def bar(self, serie, values, origin=None): + view_values = map(lambda x: (self.view(x[0]), self.view(x[1])), values) + + for i, ((x, y), (X, Y)) in enumerate(view_values): + width = X - x + padding = .1 * width + width = width - 2 * padding + self.node(serie, 'rect', + width=width, + height=self.view.y(0) - y, + x=x + padding, + y=y, + class_='rect') + def render(self): return etree.tostring( self.root, pretty_print=True, diff --git a/pygal/test/test_bar.py b/pygal/test/test_bar.py new file mode 100644 index 0000000..f9a30f7 --- /dev/null +++ b/pygal/test/test_bar.py @@ -0,0 +1,11 @@ +from pygal.bar import Bar +from math import cos, sin + + +def test_simple_bar(): + bar = Bar(800, 600, precision=2, format='f') + rng = [12, 3, 30, 4, 0] + bar.add('test1', rng) + bar.x_labels = map(str, rng) + bar.title = "Bar test" + bar._in_browser() diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index 939dd14..b999321 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -17,4 +17,4 @@ def test_one_dot(): line = Line(800, 600) line.add('one dot', [12]) line.x_labels = ['one'] - line._in_browser() + line.render()