Browse Source

Fix many things, add various font_sizes, remove alignment baseline as ff is stupid

pull/8/head
Florian Mounier 13 years ago
parent
commit
8036a08d57
  1. 25
      demo/moulinrouge/__init__.py
  2. 11
      out.py
  3. 49
      pygal/base.py
  4. 27
      pygal/config.py
  5. 55
      pygal/css/graph.css
  6. 2
      pygal/stackedbar.py
  7. 10
      pygal/style.py
  8. 114
      pygal/svg.py
  9. 14
      pygal/util.py

25
demo/moulinrouge/__init__.py

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import Flask, Response, render_template, url_for from flask import Flask, Response, render_template, url_for
from log_colorizer import make_colored_stream_handler from log_colorizer import make_colored_stream_handler
from moulinrouge.data import labels, series
from logging import getLogger, INFO, DEBUG from logging import getLogger, INFO, DEBUG
import pygal import pygal
from pygal.config import Config from pygal.config import Config
@ -62,8 +63,7 @@ def create_app():
values = [random_value((-max, min)[random.randrange(0, 2)], values = [random_value((-max, min)[random.randrange(0, 2)],
max) for i in range(data)] max) for i in range(data)]
g.add(random_label(), values) g.add(random_label(), values)
return g.render_response()
return Response(g.render(), mimetype='image/svg+xml')
@app.route("/all") @app.route("/all")
def all(): def all():
@ -76,12 +76,19 @@ def create_app():
width=width, width=width,
height=height) height=height)
# @app.route("/rotation[<int:angle>].svg") @app.route("/rotation[<int:angle>].svg")
# def rotation_svg(angle): def rotation_svg(angle):
# return generate_vbar( config = Config()
# show_graph_title=True, config.width = 375
# graph_title="Rotation %d" % angle, config.height = 245
# x_label_rotation=angle) config.x_labels = labels
config.x_label_rotation = angle
g = pygal.Line(config)
for serie, values in series.items():
g.add(serie, values)
g.add(serie, values)
return g.render_response()
@app.route("/rotation") @app.route("/rotation")
def rotation(): def rotation():
@ -98,7 +105,7 @@ def create_app():
g = pygal.Line(600, 400) g = pygal.Line(600, 400)
g.x_labels = ['a', 'b', 'c', 'd'] g.x_labels = ['a', 'b', 'c', 'd']
g.add('serie', [11, 50, 133, 2]) g.add('serie', [11, 50, 133, 2])
return Response(g.render(), mimetype='image/svg+xml') return g.render_response()
@app.route("/bigline") @app.route("/bigline")
def big_line(): def big_line():

11
out.py

@ -3,9 +3,9 @@ from pygal import Line, Bar, XY, Pie, StackedBar, Config
from math import cos, sin from math import cos, sin
bar = Bar() bar = Bar()
rng = [6, 19] rng = [-6, -19, 0, -1, 2]
bar.add('test1', rng) bar.add('test1', rng)
# bar.add('test2', map(abs, rng)) bar.add('test2', map(abs, rng))
bar.x_labels = map(str, rng) bar.x_labels = map(str, rng)
bar.title = "Bar test" bar.title = "Bar test"
with open('out-bar.svg', 'w') as f: with open('out-bar.svg', 'w') as f:
@ -13,11 +13,12 @@ with open('out-bar.svg', 'w') as f:
stackedbar = StackedBar() stackedbar = StackedBar()
rng = [3, -32, 39, 12] rng = [3, -32, 39, 12]
stackedbar.add('test1', rng) stackedbar.add('@@@@@@@', rng)
rng2 = [24, -8, 18, 12] rng2 = [24, -8, 18, 12]
stackedbar.add('test2', rng2) stackedbar.add('++++++', rng2)
rng3 = [6, 1, -10, 0] rng3 = [6, 1, -10, 0]
stackedbar.add('test3', rng3) stackedbar.add('--->', rng3)
stackedbar.x_label_rotation = 35
stackedbar.x_labels = map(lambda x: '%s / %s / %s' % x, stackedbar.x_labels = map(lambda x: '%s / %s / %s' % x,
zip(map(str, rng), zip(map(str, rng),
map(str, rng2), map(str, rng2),

49
pygal/base.py

@ -1,9 +1,9 @@
from pygal.serie import Serie from pygal.serie import Serie
from pygal.view import Margin from pygal.view import Margin
from pygal.util import round_to_scale from pygal.util import round_to_scale, cut, rad
from pygal.svg import Svg from pygal.svg import Svg
from pygal.config import Config from pygal.config import Config
import math from math import log10, sin, cos, pi
class BaseGraph(object): class BaseGraph(object):
@ -13,7 +13,7 @@ class BaseGraph(object):
self.config = config or Config() self.config = config or Config()
self.svg = Svg(self) self.svg = Svg(self)
self.series = [] self.series = []
self.margin = Margin(*([10] * 4)) self.margin = Margin(*([20] * 4))
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in dir(self.config): if attr in dir(self.config):
@ -21,7 +21,7 @@ class BaseGraph(object):
return object.__getattribute__(self, attr) return object.__getattribute__(self, attr)
def _pos(self, min_, max_, scale): def _pos(self, min_, max_, scale):
order = round(math.log10(max(abs(min_), abs(max_)))) - 1 order = round(log10(max(abs(min_), abs(max_)))) - 1
while (max_ - min_) / float(10 ** order) < 4: while (max_ - min_) / float(10 ** order) < 4:
order -= 1 order -= 1
step = float(10 ** order) step = float(10 ** order)
@ -41,17 +41,38 @@ class BaseGraph(object):
return [min_] return [min_]
return positions 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_margin(self, x_labels=None, y_labels=None): def _compute_margin(self, x_labels=None, y_labels=None):
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 x_labels:
h, w = self._get_texts_box(cut(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 y_labels: if y_labels:
h, w = self._get_texts_box(cut(y_labels), self.label_font_size)
self.margin.left += 10 + max( self.margin.left += 10 + max(
map(len, [l for l, _ in y_labels]) w * cos(rad(self.y_label_rotation)), h)
) * 0.6 * self.label_font_size
if x_labels:
self.margin.bottom += 10 + self.label_font_size
self.margin.right += 20 + max(
map(len, [serie.title for serie in self.series])
) * 0.6 * self.label_font_size
self.margin.top += 10 + self.label_font_size
def add(self, title, values): def add(self, title, values):
self.series.append(Serie(title, values, len(self.series))) self.series.append(Serie(title, values, len(self.series)))
@ -91,3 +112,7 @@ class BaseGraph(object):
from lxml.html import open_in_browser from lxml.html import open_in_browser
self._draw() self._draw()
open_in_browser(self.svg.root, encoding='utf-8') 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')

27
pygal/config.py

@ -1,6 +1,10 @@
from pygal.style import DefaultStyle from pygal.style import DefaultStyle
class FontSizes(object):
"""Container for font sizes"""
class Config(object): class Config(object):
"""Class holding config values""" """Class holding config values"""
@ -13,7 +17,18 @@ class Config(object):
base_css = None base_css = None
# Style holding values injected in css # Style holding values injected in css
style = DefaultStyle style = DefaultStyle
label_font_size = 12 # Various font sizes
label_font_size = 10
values_font_size = 18
title_font_size = 16
legend_font_size = 14
# Specify labels rotation angles in degrees
x_label_rotation = 0
y_label_rotation = 0
# Set to false to remove legend
show_legend = True
# Size of legend boxes
legend_box_size = 12
# X labels, must have same len than data. # X labels, must have same len than data.
# Leave it to None to disable x labels display. # Leave it to None to disable x labels display.
x_labels = None x_labels = None
@ -30,3 +45,13 @@ class Config(object):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Can be instanciated with config kwargs""" """Can be instanciated with config kwargs"""
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@property
def font_sizes(self):
fs = FontSizes()
for name in dir(self):
if name.endswith('_font_size'):
setattr(fs,
name.replace('_font_size', ''),
'%dpx' % getattr(self, name))
return fs

55
pygal/css/graph.css

@ -1,12 +1,11 @@
svg { svg {
background-color: {{ style.background }}; background-color: {{ style.background }};
box-shadow: 0 0 5px {{ style.foreground }};
} }
svg * { svg * {
-webkit-transition: 250ms; -webkit-transition: {{ style.transition }};
-moz-transition: 250ms; -moz-transition: {{ style.transition }};
transition: 250ms; transition: {{ style.transition }};
} }
.graph > .background { .graph > .background {
@ -23,33 +22,44 @@ svg * {
.title { .title {
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
font-size: {{ font_sizes.title }};
text-anchor: middle; text-anchor: middle;
alignment-baseline: baseline;
} }
.legend text { .legends .legend text {
font-size: 12px; font-family: monospace;
font-family: sans; font-size: {{ font_sizes.legend }};
alignment-baseline: hanging;
} }
.legend rect { .legends .legend rect {
stroke: {{ style.foreground_dark }}; fill-opacity: {{ style.opacity }};
}
.legends .legend:hover text {
stroke: {{ style.foreground_light }};
}
.legends .legend:hover rect {
fill-opacity: 1
} }
.axis text { .axis text {
font-size: 12px; font-size: {{ font_sizes.label }};
font-family: sans; font-family: sans;
} }
.axis.x text { .axis.x text {
font-family: monospace;
text-anchor: middle; text-anchor: middle;
alignment-baseline: baseline; }
.axis.x text[transform] {
text-anchor: start;
} }
.axis.y text { .axis.y text {
font-family: monospace;
text-anchor: end; text-anchor: end;
alignment-baseline: middle;
} }
.axis .line { .axis .line {
@ -83,23 +93,16 @@ svg * {
stroke-width: 5px; stroke-width: 5px;
} }
.series .dots .dot text, .series .bars .bar text, .series .slices .slice text { .series text {
opacity: 0; opacity: 0;
font-size: 12px; font-size: {{ font_sizes.values }};
text-anchor: middle; text-anchor: middle;
alignment-baseline: baseline;
stroke: {{ style.foreground_light }}; stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
alignment-baseline: baseline; text-shadow: 0 0 16px {{ style.background }};
text-shadow: 0 0 5px {{ style.background }};
z-index: 9999;
}
.series .bars .bar text, .series .slices .slice text {
alignment-baseline: middle;
} }
.series .dots .dot:hover text, .series .bars .bar:hover text, .series .slices .slice:hover text { .series .dot:hover text, .series .bar:hover text, .series .slice:hover text {
opacity: 1; opacity: 1;
} }
@ -114,7 +117,7 @@ svg * {
} }
.series .rect, .series .slice { .series .rect, .series .slice {
fill-opacity: .8; fill-opacity: {{ style.opacity }};
} }
.series .rect:hover, .series .slice:hover { .series .rect:hover, .series .slice:hover {

2
pygal/stackedbar.py

@ -34,7 +34,7 @@ class StackedBar(BaseGraph):
stack_vals = [[0, 0] for i in range(length)] stack_vals = [[0, 0] for i in range(length)]
for serie in self.series: for serie in self.series:
serie_node = self.svg.serie(serie.index) serie_node = self.svg.serie(serie.index)
stack_vals = self.svg.stackbar( stack_vals = self.svg.bar(
serie_node, serie, [ serie_node, serie, [
tuple((x_ranges[i][j], v) for j in range(2)) tuple((x_ranges[i][j], v) for j in range(2))
for i, v in enumerate(serie.values)], for i, v in enumerate(serie.values)],

10
pygal/style.py

@ -5,6 +5,8 @@ class Style(object):
foreground='#999', foreground='#999',
foreground_light='#eee', foreground_light='#eee',
foreground_dark='#555', foreground_dark='#555',
opacity='.8',
transition='250ms',
colors=( colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe',
'#899ca1', '#f8f8f2', '#808384', '#bf4646', '#516083', '#899ca1', '#f8f8f2', '#808384', '#bf4646', '#516083',
@ -15,6 +17,8 @@ class Style(object):
self.foreground = foreground self.foreground = foreground
self.foreground_light = foreground_light self.foreground_light = foreground_light
self.foreground_dark = foreground_dark self.foreground_dark = foreground_dark
self.opacity = opacity
self.transition = transition
self._colors = colors self._colors = colors
@property @property
@ -37,6 +41,10 @@ LightStyle = Style(
colors=('#242424', '#9f6767', '#92ac68', colors=('#242424', '#9f6767', '#92ac68',
'#d0d293', '#9aacc3', '#bb77a4', '#d0d293', '#9aacc3', '#bb77a4',
'#77bbb5', '#777777')) '#77bbb5', '#777777'))
NeonStyle = Style(
opacity='.1',
transition='1s ease-out')
styles = {'default': DefaultStyle, styles = {'default': DefaultStyle,
'light': LightStyle} 'light': LightStyle,
'neon': NeonStyle}

114
pygal/svg.py

@ -37,7 +37,8 @@ class Svg(object):
.replace(' }}', '\x00') .replace(' }}', '\x00')
.replace('}', '}}') .replace('}', '}}')
.replace('\x00', '}') .replace('\x00', '}')
.format(style=self.graph.style)) .format(style=self.graph.style,
font_sizes=self.graph.font_sizes))
def node(self, parent=None, tag='g', attrib=None, **extras): def node(self, parent=None, tag='g', attrib=None, **extras):
if parent is None: if parent is None:
@ -90,11 +91,17 @@ class Svg(object):
for label, position in labels: for label, position in labels:
guides = self.node(axis, class_='guides') guides = self.node(axis, class_='guides')
x = self.view.x(position) x = self.view.x(position)
y = self.view.height + 5
self.node(guides, 'path', self.node(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_='%sline' % ( class_='%sline' % (
'guide ' if position != 0 else '')) 'guide ' if position != 0 else ''))
text = self.node(guides, 'text', x=x, y=self.view.height + 5) text = self.node(guides, 'text',
x=x,
y=y + .5 * self.graph.label_font_size + 5)
if self.graph.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.graph.x_label_rotation, x, y)
text.text = label text.text = label
def y_axis(self, labels): def y_axis(self, labels):
@ -102,37 +109,58 @@ class Svg(object):
return return
axis = self.node(self.plot, class_="axis y") axis = self.node(self.plot, class_="axis y")
# import pdb; pdb.set_trace()
if 0 not in [label[1] for label in labels]: if 0 not in [label[1] for label in labels]:
self.node(axis, 'path', self.node(axis, 'path',
d='M%f %f h%f' % (0, self.view.height, self.view.width), d='M%f %f h%f' % (0, self.view.height, self.view.width),
class_='line') class_='line')
for label, position in labels: for label, position in labels:
guides = self.node(axis, class_='guides') guides = self.node(axis, class_='guides')
x = -5
y = self.view.y(position) y = self.view.y(position)
self.node(guides, 'path', self.node(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_='%sline' % ( class_='%sline' % (
'guide ' if position != 0 else '')) 'guide ' if position != 0 else ''))
text = self.node(guides, 'text', x=-5, y=y) text = self.node(guides, 'text',
x=x,
y=y + .35 * self.graph.label_font_size)
if self.graph.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.graph.y_label_rotation, x, y)
text.text = label text.text = label
def legend(self, titles): def legend(self, titles):
legend = self.node( if not self.graph.show_legend:
self.graph_node, class_='legend', return
legends = self.node(
self.graph_node, class_='legends',
transform='translate(%d, %d)' % ( transform='translate(%d, %d)' % (
self.graph.margin.left + self.view.width + 10, self.graph.margin.left + self.view.width + 10,
self.graph.margin.top + 10)) self.graph.margin.top + 10))
for i, title in enumerate(titles): for i, title in enumerate(titles):
self.node(legend, 'rect', x=0, y=i * 15, legend = self.node(legends, class_='legend')
width=8, height=8, class_="color-%d" % i, self.node(legend, 'rect',
x=0,
y=1.5 * i * self.graph.legend_box_size,
width=self.graph.legend_box_size,
height=self.graph.legend_box_size,
class_="color-%d" % i,
).text = title ).text = title
self.node(legend, 'text', x=15, y=i * 15).text = title # Serious magical numbers here
self.node(legend, 'text',
x=self.graph.legend_box_size + 5,
y=1.5 * i * self.graph.legend_box_size
+ .5 * self.graph.legend_box_size
+ .3 * self.graph.legend_font_size
).text = title
def title(self): def title(self):
self.node(self.graph_node, 'text', class_='title', if self.graph.title:
x=self.graph.margin.left + self.view.width / 2, self.node(self.graph_node, 'text', class_='title',
y=10).text = self.graph.title x=self.graph.margin.left + self.view.width / 2,
y=self.graph.title_font_size + 10
).text = self.graph.title
def serie(self, serie): def serie(self, serie):
return self.node( return self.node(
@ -153,7 +181,7 @@ class Svg(object):
self.node(serie_node, 'path', self.node(serie_node, 'path',
d='M%s L%s' % (origin, svg_values), class_='line') d='M%s L%s' % (origin, svg_values), class_='line')
def bar(self, serie_node, serie, values): def bar(self, serie_node, serie, values, stack_vals=None):
"""Draw a bar graph for a serie""" """Draw a bar graph for a serie"""
# value here is a list of tuple range of tuple coord # value here is a list of tuple range of tuple coord
@ -168,66 +196,36 @@ class Svg(object):
width = X - x width = X - x
padding = .1 * width padding = .1 * width
inner_width = width - 2 * padding inner_width = width - 2 * padding
bar_width = inner_width / len(self.graph.series)
bar_padding = .1 * bar_width
bar_inner_width = bar_width - 2 * bar_padding
offset = serie.index * bar_width + bar_padding
height = self.view.y(0) - y height = self.view.y(0) - y
if stack_vals == None:
bar_width = inner_width / len(self.graph.series)
bar_padding = .1 * bar_width
bar_inner_width = bar_width - 2 * bar_padding
offset = serie.index * bar_width + bar_padding
shift = 0
else:
offset = 0
bar_inner_width = inner_width
shift = stack_vals[i][int(height < 0)]
stack_vals[i][int(height < 0)] += height
x = x + padding + offset x = x + padding + offset
y_txt = y + height / 2
if height < 0:
y = y + height
height = -height
y_txt = y + height / 2
bar = self.node(bars, class_='bar')
self.node(bar, 'rect',
x=x,
y=y,
rx=self.graph.rounded_bars * 1,
ry=self.graph.rounded_bars * 1,
width=bar_inner_width,
height=height,
class_='rect')
self.node(bar, 'text',
x=x + bar_inner_width / 2,
y=y_txt,
).text = str(values[i][1][1])
def stackbar(self, serie_node, serie, values, stack_vals):
"""Draw a bar graph for a serie"""
# value here is a list of tuple range of tuple coord
def view(rng):
"""Project range"""
return (self.view(rng[0]), self.view(rng[1]))
bars = self.node(serie_node, class_="bars")
view_values = map(view, values)
for i, ((x, y), (X, Y)) in enumerate(view_values):
# x and y are left range coords and X, Y right ones
width = X - x
padding = .1 * width
inner_width = width - 2 * padding
height = self.view.y(0) - y
x = x + padding
y_txt = y + height / 2
shift = stack_vals[i][int(height < 0)]
stack_vals[i][int(height < 0)] += height
if height < 0: if height < 0:
y = y + height y = y + height
height = -height height = -height
y_txt = y + height / 2
y_txt = y + height / 2 + .3 * self.graph.values_font_size
bar = self.node(bars, class_='bar') bar = self.node(bars, class_='bar')
self.node(bar, 'rect', self.node(bar, 'rect',
x=x, x=x,
y=y - shift, y=y - shift,
rx=self.graph.rounded_bars * 1, rx=self.graph.rounded_bars * 1,
ry=self.graph.rounded_bars * 1, ry=self.graph.rounded_bars * 1,
width=inner_width, width=bar_inner_width,
height=height, height=height,
class_='rect') class_='rect')
self.node(bar, 'text', self.node(bar, 'text',
x=x + inner_width / 2, x=x + bar_inner_width / 2,
y=y_txt - shift, y=y_txt - shift,
).text = str(values[i][1][1]) ).text = str(values[i][1][1])
return stack_vals return stack_vals

14
pygal/util.py

@ -1,5 +1,5 @@
from decimal import Decimal from decimal import Decimal
from math import floor from math import floor, pi
def round_to_int(number, precision): def round_to_int(number, precision):
@ -18,3 +18,15 @@ def round_to_scale(number, precision):
if precision < 1: if precision < 1:
return round_to_float(number, precision) return round_to_float(number, precision)
return round_to_int(number, precision) return round_to_int(number, precision)
def cut(list_, index=0):
if isinstance(index, int):
cut = lambda x: x[index]
else:
cut = lambda x: getattr(x, index)
return map(cut, list_)
def rad(deg):
return pi * deg / 180.

Loading…
Cancel
Save