mirror of https://github.com/Kozea/pygal.git
Florian Mounier
13 years ago
7 changed files with 276 additions and 0 deletions
@ -0,0 +1,26 @@
|
||||
from pygal.graph.base import BaseGraph |
||||
|
||||
|
||||
class Bar(BaseGraph): |
||||
"""Bar graph""" |
||||
|
||||
def _compute(self): |
||||
vals = [val for serie in self.series for val in serie.values] |
||||
self._box.ymin, self._box.ymax = min(min(vals), 0), max(max(vals), 0) |
||||
x_step = len(self.series[0].values) |
||||
x_pos = [x / float(x_step) for x in range(x_step + 1) |
||||
] if x_step > 1 else [0, 1] # Center if only one value |
||||
y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale |
||||
) if not self.y_labels else map(int, self.y_labels) |
||||
|
||||
self._x_ranges = zip(x_pos, x_pos[1:]) |
||||
self._x_labels = self.x_labels and zip(self.x_labels, [ |
||||
sum(x_range) / 2. for x_range in self._x_ranges]) |
||||
self._y_labels = zip(map(str, y_pos), y_pos) |
||||
|
||||
def _plot(self): |
||||
for serie in self.series: |
||||
serie_node = self.svg.serie(serie.index) |
||||
self.svg.bar(serie_node, serie, [ |
||||
tuple((self._x_ranges[i][j], v) for j in range(2)) |
||||
for i, v in enumerate(serie.values)]) |
@ -0,0 +1,143 @@
|
||||
from pygal.serie import Serie |
||||
from pygal.view import Margin, Box |
||||
from pygal.util import round_to_scale, cut, rad |
||||
from pygal.svg import Svg |
||||
from pygal.config import Config |
||||
from math import log10, sin, cos, pi |
||||
|
||||
|
||||
class BaseGraph(object): |
||||
"""Graphs commons""" |
||||
|
||||
def __init__(self, config=None): |
||||
self.config = config or Config() |
||||
self.svg = Svg(self) |
||||
self.series = [] |
||||
self.margin = Margin(*([20] * 4)) |
||||
self._x_labels = self._y_labels = None |
||||
self._box = Box() |
||||
|
||||
def __getattr__(self, attr): |
||||
if attr in dir(self.config): |
||||
return object.__getattribute__(self.config, attr) |
||||
return object.__getattribute__(self, attr) |
||||
|
||||
def _pos(self, min_, max_, scale): |
||||
order = round(log10(max(abs(min_), abs(max_)))) - 1 |
||||
while (max_ - min_) / float(10 ** order) < 4: |
||||
order -= 1 |
||||
step = float(10 ** order) |
||||
while (max_ - min_) / step > 20: |
||||
step *= 2. |
||||
positions = set() |
||||
if self.x_start_at_zero: |
||||
position = 0 |
||||
else: |
||||
position = round_to_scale(min_, step) |
||||
while position < (max_ + step): |
||||
rounded = round_to_scale(position, scale) |
||||
if min_ <= rounded <= max_: |
||||
positions.add(rounded) |
||||
position += step |
||||
if not positions: |
||||
return [min_] |
||||
return positions |
||||
|
||||
def _text_len(self, lenght, fs): |
||||
return lenght * 0.6 * fs |
||||
|
||||
def _get_text_box(self, text, fs): |
||||
return (fs, self._text_len(len(text), fs)) |
||||
|
||||
def _get_texts_box(self, texts, fs): |
||||
max_len = max(map(len, texts)) |
||||
return (fs, self._text_len(max_len, fs)) |
||||
|
||||
def _compute(self): |
||||
"""Initial computations to draw the graph""" |
||||
|
||||
def _compute_margin(self): |
||||
if self.show_legend: |
||||
h, w = self._get_texts_box( |
||||
cut(self.series, 'title'), self.legend_font_size) |
||||
self.margin.right += 10 + w + self.legend_box_size |
||||
|
||||
if self.title: |
||||
h, w = self._get_text_box(self.title, self.title_font_size) |
||||
self.margin.top += 10 + h |
||||
|
||||
if self._x_labels: |
||||
h, w = self._get_texts_box( |
||||
cut(self._x_labels), self.label_font_size) |
||||
self.margin.bottom += 10 + max( |
||||
w * sin(rad(self.x_label_rotation)), h) |
||||
if self.x_label_rotation: |
||||
self.margin.right = max( |
||||
.5 * w * cos(rad(self.x_label_rotation)), |
||||
self.margin.right) |
||||
if self._y_labels: |
||||
h, w = self._get_texts_box( |
||||
cut(self._y_labels), self.label_font_size) |
||||
self.margin.left += 10 + max( |
||||
w * cos(rad(self.y_label_rotation)), h) |
||||
|
||||
@property |
||||
def _legends(self): |
||||
return [serie.title for serie in self.series] |
||||
|
||||
def _decorate(self): |
||||
self.svg.set_view() |
||||
self.svg.make_graph() |
||||
self.svg.x_axis() |
||||
self.svg.y_axis() |
||||
self.svg.legend() |
||||
self.svg.title() |
||||
|
||||
def _draw(self): |
||||
self._compute() |
||||
self._compute_margin() |
||||
self._decorate() |
||||
self._plot() |
||||
|
||||
def add(self, title, values): |
||||
self.series.append(Serie(title, values, len(self.series))) |
||||
|
||||
def render(self): |
||||
if len(self.series) == 0 or sum( |
||||
map(len, map(lambda s: s.values, self.series))) == 0: |
||||
return "No data" |
||||
try: |
||||
self.validate() |
||||
self._draw() |
||||
return self.svg.render() |
||||
except Exception: |
||||
from traceback import format_exc |
||||
error_svg = ( |
||||
'<?xml version="1.0" standalone="no"?>' |
||||
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ' |
||||
'"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' |
||||
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1">') |
||||
trace = (format_exc() |
||||
.replace('&', '&') |
||||
.replace('<', '<') |
||||
.replace('>', '>')) |
||||
for i, line in enumerate(trace.split('\n')): |
||||
error_svg += '<text y="%d">%s</text>' % ( |
||||
(i + 1) * 25, line) |
||||
error_svg += '</svg>' |
||||
return error_svg |
||||
|
||||
def validate(self): |
||||
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 _in_browser(self): |
||||
from lxml.html import open_in_browser |
||||
self._draw() |
||||
open_in_browser(self.svg.root, encoding='utf-8') |
||||
|
||||
def render_response(self): |
||||
from flask import Response |
||||
return Response(self.render(), mimetype='image/svg+xml') |
@ -0,0 +1,24 @@
|
||||
from pygal.graph.base import BaseGraph |
||||
|
||||
|
||||
class Line(BaseGraph): |
||||
"""Line graph""" |
||||
|
||||
def _compute(self): |
||||
vals = [val for serie in self.series for val in serie.values] |
||||
self._box.ymin, self._box.ymax = min(vals), max(vals) |
||||
x_step = len(self.series[0].values) |
||||
self._x_pos = [x / float(x_step - 1) for x in range(x_step) |
||||
] if x_step != 1 else [.5] # Center if only one value |
||||
self._y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale |
||||
) if not self.y_labels else map(int, self.y_labels) |
||||
|
||||
self._x_labels = self.x_labels and zip(self.x_labels, self._x_pos) |
||||
self._y_labels = zip(map(str, self._y_pos), self._y_pos) |
||||
|
||||
def _plot(self): |
||||
for serie in self.series: |
||||
self.svg.line( |
||||
self.svg.serie(serie.index), [ |
||||
(self._x_pos[i], v) |
||||
for i, v in enumerate(serie.values)]) |
@ -0,0 +1,22 @@
|
||||
from pygal.serie import Serie |
||||
from pygal.graph.base import BaseGraph |
||||
from math import pi |
||||
|
||||
|
||||
class Pie(BaseGraph): |
||||
"""Pie graph""" |
||||
|
||||
def add(self, title, value): |
||||
self.series.append(Serie(title, [value], len(self.series))) |
||||
|
||||
def _plot(self): |
||||
total = float(sum(serie.values[0] for serie in self.series)) |
||||
current_angle = 0 |
||||
for serie in self.series: |
||||
val = serie.values[0] |
||||
angle = 2 * pi * val / total |
||||
self.svg.slice( |
||||
self.svg.serie(serie.index), |
||||
current_angle, |
||||
angle, val / total) |
||||
current_angle += angle |
@ -0,0 +1,37 @@
|
||||
from pygal.graph.base import BaseGraph |
||||
|
||||
|
||||
class StackedBar(BaseGraph): |
||||
"""Stacked Bar graph""" |
||||
|
||||
def _compute(self): |
||||
transposed = zip(*[serie.values for serie in self.series]) |
||||
positive_vals = [sum([val if val > 0 else 0 for val in vals]) |
||||
for vals in transposed] |
||||
negative_vals = [sum([val if val < 0 else 0 for val in vals]) |
||||
for vals in transposed] |
||||
|
||||
self._box.ymin, self._box.ymax = ( |
||||
min(min(negative_vals), 0), max(max(positive_vals), 0)) |
||||
self._length = len(self.series[0].values) |
||||
|
||||
x_pos = [x / float(self._length) |
||||
for x in range(self._length + 1) |
||||
] if self._length > 1 else [0, 1] # Center if only one value |
||||
y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale |
||||
) if not self.y_labels else map(int, self.y_labels) |
||||
self._x_ranges = zip(x_pos, x_pos[1:]) |
||||
|
||||
self._x_labels = self.x_labels and zip(self.x_labels, [ |
||||
sum(x_range) / 2 for x_range in self._x_ranges]) |
||||
self._y_labels = zip(map(str, y_pos), y_pos) |
||||
|
||||
def _plot(self): |
||||
stack_vals = [[0, 0] for i in range(self._length)] |
||||
for serie in self.series: |
||||
serie_node = self.svg.serie(serie.index) |
||||
stack_vals = self.svg.bar( |
||||
serie_node, serie, [ |
||||
tuple((self._x_ranges[i][j], v) for j in range(2)) |
||||
for i, v in enumerate(serie.values)], |
||||
stack_vals) |
@ -0,0 +1,24 @@
|
||||
from pygal.graph.base import BaseGraph |
||||
|
||||
|
||||
class XY(BaseGraph): |
||||
"""XY Line graph""" |
||||
|
||||
def _compute(self): |
||||
for serie in self.series: |
||||
serie.values = sorted(serie.values, key=lambda x: x[0]) |
||||
xvals = [val[0] for serie in self.series for val in serie.values] |
||||
yvals = [val[1] for serie in self.series for val in serie.values] |
||||
self._box.xmin, self._box.xmax = min(xvals), max(xvals) |
||||
self._box.ymin, self._box.ymax = min(yvals), max(yvals) |
||||
|
||||
x_pos = self._pos(self._box.xmin, self._box.xmax, self.x_scale) |
||||
y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale) |
||||
|
||||
self._x_labels = zip(map(str, x_pos), x_pos) |
||||
self._y_labels = zip(map(str, y_pos), y_pos) |
||||
|
||||
def _plot(self): |
||||
for serie in self.series: |
||||
self.svg.line( |
||||
self.svg.serie(serie.index), serie.values, True) |
Loading…
Reference in new issue