Browse Source

Graph beutification + pie working

pull/8/head
Florian Mounier 13 years ago
parent
commit
59a73c6f05
  1. 40
      pygal/bar.py
  2. 109
      pygal/css/bar.css
  3. 111
      pygal/css/graph.css
  4. 78
      pygal/css/pie.css
  5. 77
      pygal/graph.py
  6. 59
      pygal/pie.py
  7. 59
      test/moulinrouge/__init__.py
  8. 2
      test/moulinrouge/static/css.css
  9. 1
      test/moulinrouge/templates/index.jinja2
  10. 2
      test/moulinrouge/templates/svgs.jinja2
  11. 2
      test/tests.py

40
pygal/bar.py

@ -90,38 +90,6 @@ class VerticalBar(Bar):
""" Vertical bar graph """
top_align = top_font = 1
def add_defs(self, defs):
"""
Override and place code to add defs here. TODO: what are defs?
"""
for id in range(12):
idn = 'light%d' % (id + 1)
light = node(defs, 'linearGradient', {
'id': idn,
'x1': 0,
'x2': '50%',
'y1': 0,
'y2': '100%'})
node(light, 'stop',
{'class': 'upGradientLight %s' % idn, 'offset': 0})
node(light, 'stop',
{'class': 'downGradientLight %s' % idn, 'offset': '100%'})
shadow = node(defs, 'linearGradient', {
'id': 'shadow',
'x1': 0,
'x2': '100%',
'y1': 0,
'y2': 0})
node(shadow, 'stop',
{'offset': 0, 'stop-color': '#aaa', 'stop-opacity': 0.7})
node(shadow, 'stop',
{'offset': '1%', 'stop-color': '#fff', 'stop-opacity': 1})
node(shadow, 'stop',
{'offset': '99%', 'stop-color': '#fff', 'stop-opacity': 1})
node(shadow, 'stop',
{'offset': '100%', 'stop-color': '#aaa', 'stop-opacity': .7})
def get_x_labels(self):
return self.get_field_labels()
@ -168,7 +136,7 @@ class VerticalBar(Bar):
left += bar_width * dataset_count
rect_group = node(self.graph, "g",
{'class': 'bar'})
{'class': 'bar vbar'})
node(rect_group, 'rect', {
'x': left,
'y': top,
@ -229,7 +197,9 @@ class HorizontalBar(Bar):
# left is 0 if value is negative
left = (abs(min_value) + min(value, 0)) * unit_size
node(self.graph, 'rect', {
rect_group = node(self.graph, "g",
{'class': 'bar hbar'})
node(rect_group, 'rect', {
'x': left,
'y': top,
'width': length,
@ -237,6 +207,6 @@ class HorizontalBar(Bar):
'class': 'fill fill%s' % (dataset_count + 1),
})
self.make_datapoint_text(
self.make_datapoint_text(rect_group,
left + length + 5, top + y_mod, value,
"text-anchor: start; ")

109
pygal/css/bar.css

@ -17,112 +17,3 @@
.downGradientLight {
stop-opacity: 0.9;
}
.key, .fill {
fill-opacity: 0.9;
stroke: #fff;
stroke-width: 2px;
-webkit-transition: 250ms;
}
.fill:hover {
stroke: #ddd;
fill-opacity: 0.7;
}
.key1, .fill1 {
fill: url(#light1);
}
.key2, .fill2 {
fill: url(#light2);
}
.key3, .fill3 {
fill: url(#light3);
}
.key4, .fill4 {
fill: url(#light4);
}
.key5, .fill5 {
fill: url(#light5);
}
.key6, .fill6 {
fill: url(#light6);
}
.key7, .fill7 {
fill: url(#light7);
}
.key8, .fill8 {
fill: url(#light8);
}
.key9, .fill9 {
fill: url(#light9);
}
.key10, .fill10 {
fill: url(#light10);
}
.key11, .fill11 {
fill: url(#light11);
}
.key12, .fill12 {
fill: url(#light12);
}
.light1 {
stop-color: #2a4269;
}
.light2 {
stop-color: #38588e;
}
.light3 {
stop-color: #476fb2;
}
.light4 {
stop-color: #698bc3;
}
.light5 {
stop-color: #00ccff;
}
.light6 {
stop-color: #ff00ff;
}
.light7 {
stop-color: #00ffff;
}
.light8 {
stop-color: #ffff00;
}
.light9 {
stop-color: #cc6666;
}
.light10 {
stop-color: #663399;
}
.light11 {
stop-color: #339900;
}
.light12 {
stop-color: #9966FF;
}

111
pygal/css/graph.css

@ -83,3 +83,114 @@
font-size: %(key_font_size)dpx;
font-weight: normal;
}
.key, .fill {
fill-opacity: 0.9;
stroke: #fff;
stroke-width: 2px;
-webkit-transition: 250ms;
}
.fill:hover {
stroke: #ddd;
fill-opacity: 0.7;
}
.key1, .fill1 {
fill: url(#light1);
}
.key2, .fill2 {
fill: url(#light2);
}
.key3, .fill3 {
fill: url(#light3);
}
.key4, .fill4 {
fill: url(#light4);
}
.key5, .fill5 {
fill: url(#light5);
}
.key6, .fill6 {
fill: url(#light6);
}
.key7, .fill7 {
fill: url(#light7);
}
.key8, .fill8 {
fill: url(#light8);
}
.key9, .fill9 {
fill: url(#light9);
}
.key10, .fill10 {
fill: url(#light10);
}
.key11, .fill11 {
fill: url(#light11);
}
.key12, .fill12 {
fill: url(#light12);
}
.light1 {
stop-color: #2a4269;
}
.light2 {
stop-color: #38588e;
}
.light3 {
stop-color: #476fb2;
}
.light4 {
stop-color: #698bc3;
}
.light5 {
stop-color: #00ccff;
}
.light6 {
stop-color: #ff00ff;
}
.light7 {
stop-color: #00ffff;
}
.light8 {
stop-color: #ffff00;
}
.light9 {
stop-color: #cc6666;
}
.light10 {
stop-color: #663399;
}
.light11 {
stop-color: #339900;
}
.light12 {
stop-color: #9966FF;
}

78
pygal/css/pie.css

@ -2,80 +2,10 @@
fill: #000000;
text-anchor:middle;
font-size: %(datapoint_font_size)spx;
font-family: "Arial", sans-serif;
font-weight: normal;
fill-opacity: 0;
}
/* key - MUST match fill styles */
.key1,.fill1{
fill: #ff0000;
fill-opacity: 0.7;
stroke: none;
stroke-width: 1px;
}
.key2,.fill2{
fill: #0000ff;
fill-opacity: 0.7;
stroke: none;
stroke-width: 1px;
}
.key3,.fill3{
fill-opacity: 0.7;
fill: #00ff00;
stroke: none;
stroke-width: 1px;
}
.key4,.fill4{
fill-opacity: 0.7;
fill: #ffcc00;
stroke: none;
stroke-width: 1px;
}
.key5,.fill5{
fill-opacity: 0.7;
fill: #00ccff;
stroke: none;
stroke-width: 1px;
}
.key6,.fill6{
fill-opacity: 0.7;
fill: #ff00ff;
stroke: none;
stroke-width: 1px;
}
.key7,.fill7{
fill-opacity: 0.7;
fill: #00ff99;
stroke: none;
stroke-width: 1px;
}
.key8,.fill8{
fill-opacity: 0.7;
fill: #ffff00;
stroke: none;
stroke-width: 1px;
}
.key9,.fill9{
fill-opacity: 0.7;
fill: #cc6666;
stroke: none;
stroke-width: 1px;
}
.key10,.fill10{
fill-opacity: 0.7;
fill: #663399;
stroke: none;
stroke-width: 1px;
}
.key11,.fill11{
fill-opacity: 0.7;
fill: #339900;
stroke: none;
stroke-width: 1px;
}
.key12,.fill12{
fill-opacity: 0.7;
fill: #9966FF;
stroke: none;
stroke-width: 1px;
.pie:hover .dataPointLabel {
fill-opacity: 0.9;
fill: #000000;
}

77
pygal/graph.py

@ -8,6 +8,7 @@ The base module for `pygal` classes.
from operator import itemgetter
from itertools import islice
from logging import getLogger
import os
from lxml import etree
@ -16,6 +17,8 @@ from pygal.util.boundary import (calculate_right_margin, calculate_left_margin,
calculate_bottom_margin, calculate_top_margin,
calculate_offsets_bottom)
log = getLogger('pygal')
def sort_multiple(arrays):
"sort multiple lists (of equal size) "
@ -145,6 +148,9 @@ class Graph(object):
Raises ValueError when no data set has
been added to the graph object.
"""
log.info("Burning %s graph" % self.__class__.__name__)
if not self.data:
raise ValueError("No data available")
@ -154,10 +160,12 @@ class Graph(object):
self.start_svg()
self.calculate_graph_dimensions()
self.foreground = etree.Element("g")
self.draw_graph()
self.draw_titles()
self.draw_legend()
self.draw_data()
self.graph.append(self.foreground)
data = etree.tostring(
@ -195,12 +203,12 @@ class Graph(object):
})
#Axis
node(self.back, 'path', {
node(self.foreground, 'path', {
'd': 'M 0 0 v%s' % self.graph_height,
'class': 'axis',
'id': 'xAxis'
})
node(self.back, 'path', {
node(self.foreground, 'path', {
'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width),
'class': 'axis',
'id': 'yAxis'
@ -224,18 +232,23 @@ class Graph(object):
if style:
e.set('style', style)
def x_label_offset(self, width):
return 0
def draw_x_labels(self):
"Draw the X axis labels"
if self.show_x_labels:
self.xlabels = node(self.graph, 'g', {'class': 'xLabels'})
labels = self.get_x_labels()
count = len(labels)
if not self.show_x_labels:
return
labels = enumerate(iter(labels))
start = int(not self.step_include_first_x_label)
labels = islice(labels, start, None, self.step_x_labels)
map(self.draw_x_label, labels)
self.draw_x_guidelines(self.field_width(), count)
log.debug("Drawing x labels")
self.xlabels = node(self.graph, 'g', {'class': 'xLabels'})
labels = self.get_x_labels()
count = len(labels)
labels = enumerate(iter(labels))
start = int(not self.step_include_first_x_label)
labels = islice(labels, start, None, self.step_x_labels)
map(self.draw_x_label, labels)
self.draw_x_guidelines(self.field_width(), count)
def draw_x_label(self, label):
label_width = self.field_width()
@ -289,6 +302,8 @@ class Graph(object):
"Draw the Y axis labels"
if not self.show_y_labels:
return
log.debug("Drawing y labels")
self.ylabels = node(self.graph, 'g', {'class': 'yLabels'})
labels = self.get_y_labels()
count = len(labels)
@ -339,6 +354,7 @@ class Graph(object):
"Draw the X-axis guidelines"
if not self.show_x_guidelines:
return
log.debug("Drawing x guidelines")
self.xguidelines = node(self.graph, 'g', {'class': 'xGuideLines'})
# skip the first one
for count in range(1, count):
@ -352,6 +368,7 @@ class Graph(object):
"Draw the Y-axis guidelines"
if not self.show_y_guidelines:
return
log.debug("Drawing y guidelines")
self.yguidelines = node(self.graph, 'g', {'class': 'yGuideLines'})
for count in range(1, count):
start = self.graph_height - label_height * count
@ -362,6 +379,7 @@ class Graph(object):
def draw_titles(self):
"Draws the graph title and subtitle"
log.debug("Drawing titles")
if self.show_graph_title:
self.draw_graph_title()
if self.show_graph_subtitle:
@ -390,6 +408,7 @@ class Graph(object):
text.text = self.graph_title
def draw_x_title(self):
log.debug("Drawing x title")
y = self.graph_height + self.border_top + self.x_title_font_size
if self.show_x_labels:
y_size = self.x_label_font_size + 5
@ -406,6 +425,7 @@ class Graph(object):
text.text = self.x_title
def draw_y_title(self):
log.debug("Drawing y title")
x = self.y_title_font_size
if self.y_title_text_direction == 'bt':
x += 3
@ -428,6 +448,7 @@ class Graph(object):
def draw_legend(self):
if not self.key:
return
log.debug("Drawing legend")
group = node(self.root, 'g')
@ -457,9 +478,37 @@ class Graph(object):
"""
Override and place code to add defs here. TODO: what are defs?
"""
for id in range(12):
idn = 'light%d' % (id + 1)
light = node(defs, 'linearGradient', {
'id': idn,
'x1': 0,
'x2': '50%',
'y1': 0,
'y2': '100%'})
node(light, 'stop',
{'class': 'upGradientLight %s' % idn, 'offset': 0})
node(light, 'stop',
{'class': 'downGradientLight %s' % idn, 'offset': '100%'})
shadow = node(defs, 'linearGradient', {
'id': 'shadow',
'x1': 0,
'x2': '100%',
'y1': 0,
'y2': 0})
node(shadow, 'stop',
{'offset': 0, 'stop-color': '#aaa', 'stop-opacity': 0.7})
node(shadow, 'stop',
{'offset': '1%', 'stop-color': '#fff', 'stop-opacity': 1})
node(shadow, 'stop',
{'offset': '99%', 'stop-color': '#fff', 'stop-opacity': 1})
node(shadow, 'stop',
{'offset': '100%', 'stop-color': '#aaa', 'stop-opacity': .7})
def start_svg(self):
"Base SVG Document Creation"
log.debug("Creating root node")
svg_ns = 'http://www.w3.org/2000/svg'
nsmap = {
None: svg_ns,
@ -490,8 +539,9 @@ class Graph(object):
' include default stylesheet if none specified '))
style = node(defs, 'style', type='text/css')
style.text = ''
opts = dict(Graph.__dict__)
opts.update(self.__dict__)
opts = self.__dict__.copy()
opts.update(Graph.__dict__)
opts.update(self.__class__.__dict__)
for stylesheet in self.stylesheet_names:
with open(
os.path.join(os.path.dirname(__file__), 'css',
@ -507,6 +557,7 @@ class Graph(object):
'class': 'svgBackground'})
def calculate_graph_dimensions(self):
log.debug("Computing sizes")
self.border_right = calculate_right_margin(self)
self.border_top = calculate_top_margin(self)
self.border_left = calculate_left_margin(self)

59
pygal/pie.py

@ -46,11 +46,11 @@ class Pie(Graph):
"""
"if true, displays a drop shadow for the chart"
show_shadow = True
show_shadow = False
"Sets the offset of the shadow from the pie chart"
shadow_offset = 10
show_data_labels = False
show_data_labels = True
"If true, display the actual field values in the data labels"
show_actual_values = False
@ -116,21 +116,21 @@ class Pie(Graph):
pairs = itertools.izip_longest(self.data, data_descriptor['data'])
self.data = list(itertools.starmap(robust_add, pairs))
def add_defs(self, defs):
"Add svg definitions"
node(
defs,
'filter',
id='dropshadow',
width='1.2',
height='1.2',
)
node(
defs,
'feGaussianBlur',
stdDeviation='4',
result='blur',
)
# def add_defs(self, defs):
# "Add svg definitions"
# node(
# defs,
# 'filter',
# id='dropshadow',
# width='1.2',
# height='1.2',
# )
# node(
# defs,
# 'feGaussianBlur',
# stdDeviation='4',
# result='blur',
# )
def draw_graph(self):
"Here we don't need the graph (consider refactoring)"
@ -146,7 +146,6 @@ class Pie(Graph):
def keys(self):
total = sum(self.data)
percent_scale = 100.0 / total
def key(field, value):
result = [field]
@ -177,10 +176,6 @@ class Pie(Graph):
transform = 'translate(%s %s)' % (xoff, yoff)
self.graph.set('transform', transform)
wedge_text_pad = 5
wedge_text_pad = (20 * int(self.show_percent) *
int(self.show_data_labels))
total = sum(self.data)
max_value = max(self.data)
@ -202,11 +197,13 @@ class Pie(Graph):
radius, radius, x_start, y_start, radius, radius,
percent_greater_fifty, x_end, y_end)
wedge_group = node(self.foreground, "g",
{'class': 'pie'})
wedge = node(
self.foreground,
wedge_group,
'path', {
'd': path,
'class': 'fill%s' % (index + 1)}
'class': 'fill fill%s' % (index + 1)}
)
translate = None
@ -267,19 +264,7 @@ class Pie(Graph):
ty -= (mcr * self.expand_gap)
label_node = node(
self.foreground,
'text',
{
'x': tx,
'y': ty,
'class': 'dataPointLabel',
'style': 'stroke: #fff; stroke-width: 2;'
}
)
label_node.text = label
label_node = node(
self.foreground,
wedge_group,
'text',
{
'x': tx,

59
test/moulinrouge/__init__.py

@ -3,7 +3,22 @@ from flask import Flask, Response, render_template, url_for
from log_colorizer import make_colored_stream_handler
from logging import getLogger, INFO, WARN, DEBUG
from moulinrouge.data import labels, series
from pygal.bar import VerticalBar
from pygal.bar import VerticalBar, HorizontalBar
from pygal.pie import Pie
import string
import random
def random_label():
chars = string.letters + string.digits + u' àéèçêâäëï'
return ''.join(
[random.choice(chars)
for i in range(
random.randrange(4, 30))])
def random_value():
return random.randrange(0, 15, 1)
def generate_vbar(**opts):
@ -25,12 +40,46 @@ def create_app():
getLogger('werkzeug').addHandler(handler)
getLogger('werkzeug').setLevel(INFO)
getLogger('pygal').addHandler(handler)
getLogger('pygal').setLevel(INFO)
getLogger('pygal').setLevel(DEBUG)
@app.route("/")
def index():
return render_template('index.jinja2')
@app.route("/all-<type>.svg")
def all_svg(type):
width, height = 800, 600
series = random.randrange(1, 10)
data = random.randrange(1, 10)
labels = [random_label() for i in range(data)]
if type == 'vbar':
g = VerticalBar(labels)
elif type == 'hbar':
g = HorizontalBar(labels)
elif type == 'pie':
series = 1
g = Pie({'fields': labels})
g.width, g.height = width, height
for i in range(series):
values = [random_value() for i in range(data)]
g.add_data({'data': values, 'title': random_label()})
return Response(g.burn(), mimetype='image/svg+xml')
@app.route("/all")
def all():
width, height = 800, 600
svgs = [url_for('all_svg', type=type) for type in
('vbar', 'hbar', 'pie')]
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
@app.route("/rotation[<int:angle>].svg")
def rotation_svg(angle):
return generate_vbar(
@ -40,8 +89,12 @@ def create_app():
@app.route("/rotation")
def rotation():
width, height = 375, 245
svgs = [url_for('rotation_svg', angle=angle)
for angle in range(0, 91, 5)]
return render_template('svgs.jinja2', svgs=svgs)
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
return app

2
test/moulinrouge/static/css.css

@ -4,8 +4,6 @@ html, body, section, figure {
}
embed {
width: 375px;
height: 245px;
float: left;
border: 1px solid #ccc;
}

1
test/moulinrouge/templates/index.jinja2

@ -1,5 +1,6 @@
{% extends '_layout.jinja2' %}
{% block section %}
<a href="{{ url_for('all') }}">All types</a>
<a href="{{ url_for('rotation') }}">Rotations test</a>
{% endblock section %}

2
test/moulinrouge/templates/svgs.jinja2

@ -3,7 +3,7 @@
{% block section %}
{% for svg in svgs %}
<figure>
<embed src="{{ svg }}" type="image/svg+xml" />
<embed src="{{ svg }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" />
<figcaption></figcaption>
</figure>
{% endfor %}

2
test/tests.py

@ -5,4 +5,4 @@ from moulinrouge import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True, threaded=True, port=21112)
app.run(debug=True, threaded=True, host='0.0.0.0', port=21112)

Loading…
Cancel
Save